Oneupweb : Create a Custom WordPress Theme Settings Menu

One of the coolest thing about WordPress is the ability to easily add menus and options for you and your users to easily configure what’s going on in your blog. In this post we will cover putting a system in place to easily create these menus using the WordPress Option API. Essentially we want to see something like this:
Custom settings menu So let’s get crackin!

Setup

Like all theme code, our menu business will take place in the notorious functions.php file. To start your custom settings menu on the road to success we are going to setup two very important variables. Add these to your functions.php file.

$settings_prefix = "mytheme_settings_";
$settings_fields = array();

The $settings_prefix variable will contain the prefix you are going to use on all of your option names and functions that power the menu (You can easily change mytheme to be the name of your theme). The $settings_fields variable will store all the different types of options and layout for your menu (Basically, “Is this a text field, is this a dropdown, etc…”).

Adding the menu to the admin screen

Certainly declaring two variables won’t do much. We need to do some plumbing to get the menus added and doing something. This can be accomplished with three functions. Below are two variables, let’s create the following three functions:

add_action('admin_menu',$settings_prefix . 'menu');
function mytheme_settings_menu() {
    global $settings_prefix;
    add_submenu_page('options-general.php','MyTheme Settings','MyTheme Settings','edit_posts','mytheme-settings',$settings_prefix . 'render_fields');
    add_action('admin_init',$settings_prefix . 'register');
}

function mytheme_settings_register() {
    global $settings_fields,$settings_prefix;
    foreach ( $settings_fields as $field ) {
        if ( isset($field['name']) ) {
            register_setting($settings_prefix . 'group',$field['name']);
        }
    }
}

function mytheme_render_fields() {
    //dont worry....we will cover this in a minute
}

Don’t worry if this doesn’t immediately make sense. We are going to cover each of these functions more in depth.

mytheme_settings_menu

You will notice that this is the function that is being called by the WordPress hook admin_menu. This is the function that calls the other two functions and adds our custom menu to the settings panel in WordPress. You’ll notice we declare our global variable $settings_prefix so we can use it inside of our function. Followed by this we call the built-in WordPress function add_submenu_page();
This function accepts 6 arguments: $parent_slug,$page_title,$menu_title,$capability,$menu_slug,$function. Lets look at what we passed into our function.

  1. $parent_slug: ‘options-general.php’. This is the slug for the Settings tab. This ensures our menu is listed under the settings tab.
  2. $page_title: ‘MyTheme Settings’. You guessed it, this is what will be displayed in the title bar of the browser.
  3. $menu_title: ‘MyTheme Settings’. The label of the menu that will be displayed under Settings.
  4. $capability: ‘edit_posts’. This ensures that anyone who is already authorized to edit posts will be able to use our handy menu.
  5. $menu_slug: ‘mytheme-settings’. The page slug. Slugs are used as unique identifiers
  6. $function: using our prefix this translates to the mytheme_render_fields function. this is the function that is in charge of outputting the html for our menu

The last thing we do inside of our menu function is add an action to the admin_init hook. We pass it our mytheme_settings_register function which we will describe in more detail next.

mytheme_settings_register

Before we get any further, let’s fill our $settings_fields with some data so we can see just how we are going to be populating our option table.

$settings_fields = array(
    array(
        'type' => 'start'
    ),
    array(
        'type' => 'icon'
    ),
    array(
        'type' => 'title',
        'value' => 'OneUpWeb Settings'
    ),
    array(
        'type' => 'form-start'
    ),
    array(
        'type' => 'section-start',
        'heading' => 'My Section'
    ),
    array(
        'type' => 'text',
        'name' => $settings_prefix . 'my-text-option',
        'label' => 'This is a text option',
        'description' => 'This text field will control some configurable option',
        'std' => 'Some text'
    ),
    array(
        'type' => 'textarea',
        'name' => $settings_prefix .'my-textarea-option',
        'label' => 'This is a text area option',
        'description' => 'This textarea will control some other configurable option',
        'std' => 'Multiple lines of text go here'
    ),
    array(
        'type' => 'section-end'
    ),
    array(
        'type' => 'form-end'
    ),
    array(
        'type' => 'end'
    )
);

To get an idea of what’s going on, this collection of arrays is in charge of creating our option types as well as instructing our option panel where to start useful structures like grouped sections and the actual form. Using the above array to describe our option panel we will get the following:

The job of the mytheme_settings_register function is to register the name part of these arrays so that WordPress can validate and save the options (no database table creation necessary!)
The key to the function are these lines:

foreach ( $settings_fields as $field ) {
      if ( isset($field['name']) ) {
          register_setting($settings_prefix . 'group',$field['name']);
      }
}

Basically we loop through all of our fields and if it is a field that has a name option (meaning it is a form input) we register that name to a group that is unique to our theme (a la the settings prefix). Now all that is left is to output our menu!

mytheme_settings_render_fields

What we do here is loop through our settings_fields and based on the type of field, we output something. The html we are going to output is going to be friendly with what the rest of the WordPress admin panels use.

function mytheme_settings_render_fields() {
    global $settings_fields,$settings_prefix;
    foreach ( $settings_fields as $field ) {
        switch( $field['type'] ) {
            case 'start':?>
                <div class="wrap"><?php
                break;
            case 'end':?>
                </div><!-- .wrap --><?php
                break;
            case 'icon':?>
                <div id="icon-options-general" class="icon32">
                    <br />
                </div><?php
                break;
            case 'title':?>
                <h2><?php echo $field['value']; ?></h2><?php
                break;
            case 'form-start':?>
                <form method="post" action="options.php">
                <?php
                settings_fields($settings_prefix . 'group');
                do_settings_sections($settings_prefix . 'group');
                break;
            case 'form-end':?>
                    <p class="submit">
                        <input type="submit" class="button-primary" value="Save Changes" />
                    </p>
                </form><?php
                break;
            case 'section-start':?>
                <h3><?php echo $field['heading']; ?></h3>
                <table class="form-table"><tbody><?php
                break;
            case 'section-end':?>
                </tbody></table><?php
                break;
            case 'text':?>
                 <tr valign="top">
                    <th scope="row">
                        <?php $for = $field['name']; ?>
                        <label for="<?php echo $for; ?>"><?php echo $field['label']; ?></label>
                    </th>
                    <td>
                        <input type="text" class="regular-text" value="<?php if ( $opt = get_option($field['name']) ) { echo $opt; } else { echo $field['std']; } ?>" name="<?php echo $field['name']; ?>" id="<?php echo $field['name']; ?>" />
                        <?php if ( isset($field['description']) ): ?>
                        <span class="description"><?php echo $field['description']; ?></span>
                        <?php endif; ?>
                    </td>
                 </tr><?php
                break;
            case 'textarea':?>
                <tr valign="top">
                    <th scope="row">
                        <?php $for = $field['name']; ?>
                        <label for="<?php echo $for; ?>"><?php echo $field['label']; ?></label>
                    </th>
                    <td>
                        <textarea class="large-text" rows="" cols="" name="<?php echo $field['name']; ?>" name="<?php echo $field['name']; ?>"><?php if ( $opt = get_option($field['name']) ) { echo stripslashes($opt); } else { echo $field['std']; }  ?></textarea>
                        <?php if ( isset($field['description']) ): ?>
                        <span class="description"><?php echo $field['description']; ?></span>
                        <?php endif; ?>
                    </td>
                </tr><?php
                break;
        }
    }
}

Most of this should be pretty self-explanatory. The only thing to note is what we are doing for the form-start type. First note that the action is options.php. This is the script that will handle saving and validating your options. This file knows to save your custom options based on the next two lines of code:

settings_fields($settings_prefix . 'group');
do_settings_sections($settings_prefix . 'group');

If you don’t call these bad mamma-jammas, your options will not save.

Putting it all together!

$settings_fields = array(
    array(
        'type' => 'start'
    ),
    array(
        'type' => 'icon'
    ),
    array(
        'type' => 'title',
        'value' => 'My Theme Settings'
    ),
    array(
        'type' => 'form-start'
    ),
    array(
        'type' => 'section-start',
        'heading' => 'My Section'
    ),
    array(
        'type' => 'text',
        'name' => $settings_prefix . 'my-text-option',
        'label' => 'This is a text option',
        'description' => 'This text field will control some configurable option',
        'std' => 'Some text'
    ),
    array(
        'type' => 'textarea',
        'name' => $settings_prefix .'my-textarea-option',
        'label' => 'This is a text area option',
        'description' => 'This textarea will control some other configurable option',
        'std' => 'Multiple lines of text go here'
    ),
    array(
        'type' => 'section-end'
    ),
    array(
        'type' => 'form-end'
    ),
    array(
        'type' => 'end'
    )
);

add_action('admin_menu',$settings_prefix . 'menu');
function mytheme_settings_menu() {
    global $settings_prefix;
    add_submenu_page('options-general.php','MyTheme Settings','MyTheme Settings','edit_posts','mytheme-settings',$settings_prefix . 'render_fields');
    add_action('admin_init',$settings_prefix . 'register');
}

function mytheme_settings_register() {
    global $settings_fields,$settings_prefix;
    foreach ( $settings_fields as $field ) {
        if ( isset($field['name']) ) {
            register_setting($settings_prefix . 'group',$field['name']);
        }
    }
}

function mytheme_settings_render_fields() {
    global $settings_fields,$settings_prefix;
    foreach ( $settings_fields as $field ) {
        switch( $field['type'] ) {
            case 'start':?>

<?php break; case 'icon':?> <?php break; case 'title':?>

&lt?php echo $field['value']; ?>

<?php break; case 'form-start':?>
<?php settings_fields($settings_prefix . 'group'); do_settings_sections($settings_prefix . 'group'); break; case 'form-end':?> <?php break; case 'section-start':?>

<?php echo $field['heading']; ?>

<?php break; case 'section-end':?>
<?php break; case 'text':?> <?php $for = $field['name']; ?> <?php if ( isset($field['description']) ): ?> <?php echo $field['description']; ?> <?php endif; ?> <?php break; case 'textarea':?> <?php $for = $field['name']; ?> <?php if ( isset($field['description']) ): ?> <?php echo $field['description']; ?> <?php endif; ?> <?php break; } } }

FINISHED

I hope that wasn’t too much to munch on. I would love to hear how you have tweaked the script to account for other input types and functionality.