Start with a plan
Interesting fact, Google said that Benjamin Franklin may have been the originator on that quote, but don’t quote me on that. I also learned recently that Hemingway may have never said, “Write drunk. Edit Sober.” For sure he didn’t follow that advice. I feel a bit out of sorts with all of these new revelations.
In any case, the point is that you need to start with a plan. You already know what you want your plugin to do, but what options do you want to make for the plugin users? The way I see it, there are two schools of thought on this.
- Add all the options
- Add minimal to no options
Each of these approaches are based on different underlying opinions on what a user wants. If you go ask a user if they want more options, they will probably say, “yes”. We like the idea of control so we want more ways to control the output.
A lot of studies in UX/UI seem to point towards users actually preferring fewer and more intuitive options. A great example is the iOS. People love it because it is simple and easy to use. Sure, you are much more limited in what you can do, but most users just don’t care so long as it works.
Regardless of which approach you follow, I highly recommend outlining the options, then reviewing them to ask if they make sense. I tend to add all the options to my initial outline then I start asking myself how I can condense the options. A few lucky people have been able to get in on some early testing for the Genesis Simple Share plugin. I hope they really like the super simple admin UI. When it started there were several options for each sharing icon and then some additional general options. In the end the sharing icons were mostly given a single option, “enable this icon.” The interface was cleaned up and I think it is much easier for people to use.
For “power” users who might want to have more complex control it is possible to use template functions to do a lot more.
I personally think that is how we should approach the admin. If you can make your plugin work well without user controlled settings, skip this article. Of course, most of the time you need to have at least some options, but consider making a minimalist admin page. Include only what is necessary, especially for your initial release.
As a closing example, I run sound for my church. I was watching videos to learn more about how to balance the sound because I noticed that often, the live mix seemed muddy and harsh. One sound technician said “always cut first.” What that means is, when mixing sound you need to find the frequencies that sound bad and lower those for each instrument and vocalist first. This creates places for other sounds to fill and makes it a lot easier to bring up a few things to round out the sound than to have too much stacked in a small frequency range. That is what makes muddy and harsh sounding music. The next week I did a lot of cuts and did very little else to change the mix. The music was significantly better than the prior week.
I think we, as developers, have a habit of filling the voids because we can but that results in overwhelming option screens that are hard to use. Try cutting options first, then add back anything you absolutely need.
Add the Page
Now that you have an idea of what options you will need it is time to build your page. There are quite a few good tutorials on working with the Genesis admin class. This was introduced in Genesis 1.8 and has made it very easy to build a consistent admin experience across plugins. This is important because a consistent experience means users will be more likely to know how your admin screen works. After all, they have already seen similar screens.
That said, if you can come up with an intuitive way to get your options on page then take time to work on added styling and js. I’ve been seeing some users that have done some really interesting things lately like the Genesis Design Palette Pro plugin by Andrew Norcross.
Here is what the basic class looks like with the bare essentials in place. I’ll fill those in as I explain the various parts in more detail.
<?php /** * Creates the plugin admin page. * * * @category Genesis Boilerplate * @package Admin * @author copyblogger * @license http://www.opensource.org/licenses/gpl-license.php GPL-2.0+ */ /* Prevent direct access to the plugin */ if ( !defined( 'ABSPATH' ) ) { die( "Sorry, you are not allowed to access this page directly." ); } /** * Registers a new admin page, providing content and corresponding menu items * * @category Genesis Boilerplate * @package Admin * * @since 0.1.0 */ class Genesis_Boilerplate_Boxes extends Genesis_Admin_Boxes { /** * Create an admin menu item and settings page. * * @since 0.1.0 * */ function __construct() { /* defines the setting value. You will use genesis_get_option( 'option', 'genesis_boilerplate' ); to retrieve "option" from this field later */ $settings_field = 'genesis_boilerplate'; //allows you to set defaults. Look for a full example below $default_settings = array(); //define where your page can be found $menu_ops = array( 'submenu' => array( /** Do not use without 'main_menu' */ 'parent_slug' => 'genesis', //loads under "genesis" menu 'page_title' => __( 'Genesis Boilerplate Settings', 'genesis-boilerplate' ), //shows on page 'menu_title' => __( 'Boilerplate', 'genesis-boilerplate' ) //shows in the menu ) ); /** Just use the defaults most of the time other tutorials can show you how to get advanced here */ $page_ops = array(); //creates the page $this->create( 'genesis_boilerplate_settings', $menu_ops, $page_ops, $settings_field, $default_settings ); //loads the sanitizer. Look for details below. add_action( 'genesis_settings_sanitizer_init', array( $this, 'sanitizer_filters' ) ); } /** * Register each of the settings with a sanitization filter type. * * @since 0.9.0 * * @uses genesis_add_option_filter() Assign filter to array of settings. * * @see \Genesis_Settings_Sanitizer::add_filter() Add sanitization filters to options. */ function sanitizer_filters() { } /** * Loads required scripts. * * @since 0.1.0 * */ function scripts() { } /** * Register meta boxes. * * * @since 0.1.0 * */ function metaboxes() { } } new Genesis_Boilerplate_Boxes;
Load CSS and Javascript
I previously wrote on the Genesis Admin Class so I’m not going to spend much time explaining the basic class, but since I’m encouraging users to work on including CSS and JS if they have come up with a good solution I want to focus on how to do that.
I’m assuming a specific file structure with these examples. It isn’t the only way to do things, but it is how I tend to build a plugin
- genesis-boilerplate/
- plugin.php
- lib/
- admin.php
- front-page.php
- functions.php
- css/
- admin.css
- style.css
- js/
- admin.js
- plugin.js
If your file structure is different you may need to amend the code used to load the Javascript and Styles.
If you are manually building your admin page you have to build a custom action to load the styles and you should check the page hook to make sure you are loading the scripts and styles only on your settings page. I’m going to repeat myself here
Only load your scripts and styles on your admin page
I can’t stress this enough. One of the biggest problems other plugins cause is when they load scripts or styles universally in the dashboard. I’ve even seen many plugins that are fine on their own but they end up breaking other plugins because they’ve used common class names and changed them. We recently had to change our clear class because a plugin was styling .clear and gave it some strange markup. The markup probably made sense in the plugin admin page but it broke our admin page. That should never happen and in the github issue where I fixed the issue I said:
To test, enable a plugin that breaks the admin screen because of stupid developers. Our classes are different and shouldn’t be broken.
Please don’t be a stupid developer. Make sure your scripts are loaded only on your admin page.
Fortunately, we are using the Genesis Admin Class. All you need to do is add a scripts()
method and it will load your scripts correctly and only on your admin page. The Genesis class automatically loads this method so you do not need to use an action to load it on the right hook or check the page hook to make it load only on your admin page. This is all done automatically. You get to be a smart developer and it is super easy. That makes you some kind of dragon ninja warrior kind of developer.
So, here is the example code you can use. I’ve added a lot of comments to help with explaining it.
/** * Loads required scripts. * * @since 0.1.0 * */ function scripts() { //adding some common scripts here. You may or may not need them wp_enqueue_script( 'common' ); wp_enqueue_script( 'wp-lists' ); wp_enqueue_script( 'postbox' ); //use wp_enqueue_script() and wp_enqueue_style() to load scripts and styles wp_enqueue_script( 'genesis-boilerplate-admin-js', //make sure you namespace your ID and pick a unique and descriptive name plugins_url( 'js/admin.js', __FILE__ ), //adjust if needed. This automatically builds the right URL based on the file structure above array( 'jquery' ), //I'm assuming the file needs jQuery. '0.1.0' //use versions so if you have to update people get the right version ); //This is enqueueing the style. It is very similar to the script function above but geared for styles wp_enqueue_style( 'genesis-boilerplate-admin-css', plugins_url( 'css/admin.css', __FILE__ ), array(), '0.1.0' ); }
Add Options
There are many ways to add options. I’ve built an admin builder class which I talk about in the other post I wrote. I’ve moved away from that because it is large and I don’t usually need all those options. Instead I use a few methods and build my options out using those.
The reason for building those option methods is I’ve found that I tend to use the exact same HTML with a few small changes over and over. It causes very long files and that makes things difficult to read. Check out how clean the code ends up when you take time to build a method to handle repeating code.
/** * Register meta boxes. * * * @since 0.1.0 * */ function metaboxes() { /* This loads the functions that display the boxes on the admin page. Make sure you use names that are unique and descriptive. */ add_meta_box( 'genesis_boilerplate_general_settings' , __( 'General' , 'boilerplate' ), array( $this, 'general' ) , $this->pagehook, 'main' ); add_meta_box( 'genesis_boilerplate_advanced_settings', __( 'Advanced', 'boilerplate' ), array( $this, 'advanced' ), $this->pagehook, 'main' ); } /** * Create General settings metabox output * * * @since 0.1.0 * */ function general() { $id = 'general'; //I use an ID to link common options together ?> <div class="wrap gb-clear"> <br /> <table class="form-table"> <!--this is using table markup to build out the relationship for labels and options--> <tbody> <?php //I created a method for building a select field. It is clean and easy to use. Feel free to steal and adapt it $this->select_field( $id . '_size', __( 'Size', 'genesis-boilerplate' ), array( 'small' => __( 'Small' , 'genesis-boilerplate' ), 'medium' => __( 'Medium', 'genesis-boilerplate' ), 'tall' => __( 'Box' , 'genesis-boilerplate' ), ) ); //I used a wrapper method here. There were a lot of options in the full file this plugin was for so this saved space $this->position( $id ); //I also have another wrapper method that automatically builds a multicheck from all post types. Another handy thing to steal. $this->post_type_checkbox( $id ); ?> </tbody> </table> </div> <?php } /** * Create Advanced settings metabox output * * * @since 0.1.0 * */ function advanced() { $id = 'advanced'; //This method builds a checkbox $this->checkbox( $id . '_checkbox', __( 'Enable This Option?', 'genesis-boilerplate' ) ); //here is an example of adding a text field directly to the screen without a method. ?><p> <label for="<?php echo $this->get_field_id( $id . '_text' ); ?>"><?php _e( 'Enter text here:', 'genesis-boilerplate' ); ?></label> <input type="text" name="<?php echo $this->get_field_name( $id . '_text' ); ?>" id="<?php echo $this->get_field_id( $id . '_text' ); ?>" value="<?php echo esc_attr( $this->get_field_value( $id . '_text' ); ?>" size="27" /> </p><?php //an alternate solution to the abover would be a text_field() method //$this->text_field( $id . '_text', __( 'Enter text here:', 'genesis-boilerplate' ) ); } /** * Outputs select field to select position for the icon * * @since 0.1.0 * * @param string $id ID base to use when building select box. * */ function position( $id ){ $this->select_field( $id . '_position', __( 'Display Position' , 'genesis-boilerplate' ), array( 'off' => __( 'Select display position to enable.' , 'genesis-boilerplate' ), 'before_content' => __( 'Before the Content' , 'genesis-boilerplate' ), 'after_content' => __( 'After the Content' , 'genesis-boilerplate' ), 'both' => __( 'Before and After the Content' , 'genesis-boilerplate' ), ) ); } /** * Outputs text field * * @since 0.1.0 * * @param string $id ID to use when building select box. * @param string $name Label text for the select field. * @param array $option Array key $option=>$title used to build select options. * */ function text_field( $id, $label ){ printf( '<label for="%s">%s</label><input type="text" name="%s" id="%s" value="%s" size="27" />', $this->get_field_id( $id ), $label, $this->get_field_name( $id ), $this->get_field_id( $id ), esc_attr( $this->get_field_value( $id ) ); } /** * Outputs select field * * @since 0.1.0 * * @param string $id ID to use when building select box. * @param string $label Label text for the select field. * @param array $option Array key $option=>$title used to build select options. * */ function select_field( $id, $label, $options = array() ){ $current = $this->get_field_value( $id ); ?> <tr valign="top"> <th scope="row"><label for="<?php echo $this->get_field_id( $id ); ?>"><?php echo $label ?></label></th> <td><select name="<?php echo $this->get_field_name( $id ); ?>" class="<?php echo 'genesis_boilerplate_' . $id; ?>" id="<?php echo $this->get_field_id( $id ); ?>"> <?php if ( ! empty( $options ) ) { foreach ( (array) $options as $option => $title ) { printf( '<option value="%s"%s>%s</option>', esc_attr( $option ), selected( $current, $option, false ), esc_html( $title ) ); } } ?> </select></td> </tr><?php } /** * Outputs checkbox fields for public post types. * * @since 0.1.0 * * @param string $id ID base to use when building checkbox. * */ function post_type_checkbox( $id ){ $post_types = get_post_types( array( 'public' => true, ) ); printf( '<tr valign="top"><th scope="row">%s</th>', __( 'Enable on:', 'genesis-boilerplate' ) ); echo '<td>'; foreach( $post_types as $post_type ) $this->checkbox( $id . '_' . $post_type, $post_type ); $this->checkbox( $id . '_show_archive', __( 'Show on Archive Pages', 'genesis-boilerplate' ) ); echo '</td></tr>'; } /** * Outputs checkbox field. * * @since 0.1.0 * * @param string $id ID to use when building checkbox. * @param string $label Label text for the checkbox. * */ function checkbox( $id, $label ){ printf( '<label for="%s"><input type="checkbox" name="%s" id="%s" value="1"%s /> %s </label> ', $this->get_field_id( $id ), $this->get_field_name( $id ), $this->get_field_id( $id ), checked( $this->get_field_value( $id ), 1, false ), $label ); echo '<br />'; }
Of course, I pulled out most of the options this file had so there is probably a net loss or maybe a wash. With more options it can save you significant space and it makes it easier to change the markup on several elements at once.
Add Defaults
Once you have your options built you should decide what the default values are.
Why bother with defaults?
If people will be setting up options, then why should you give them defaults? Remember, the goal is to make things easy for a user. If they can turn on a plugin and have it work out of the box that is a better experience. The options page should let them set important values that cannot be decided for them or customize the feel of your plugin to quite their needs. You are the expert though, so give them a head start by providing as many default values as you can based on your experience.
Start by filling in the options that you would pick. That will give you a good idea of which values you would recommend. Now use that to fill in the defaults. This goes up in the __construct()
method.
/* If themes or other plugins should be able to change your defaults use a filter here. Otherwise just use the array without the filter */ $default_settings = apply_filters( 'genesis_boilerplate_defaults', array( 'general_size' => 'medium', 'general_position' => 'before_content', 'general_post' => 1, 'advanced_checkbox' => 0, 'advanced_text' => __( 'Default Text', 'genesis_boilerplate' ), //if you define default text internationalize it ) );
Sanitize Your Options
Another important step here is to sanitize the values. If you do not sanitize your content it opens your plugin up to exploits. Sanitizing the saved option also means you can know with greater certainty what type of value will be returned on the front end of the site. The number one rule of sanitizing options is to make sure the option uses the least possible filter. A checkbox should be validated with only the possible option, typically 1 or 0. Select boxes and most text fields can be no HTML.
Fortunately Genesis makes this very easy too. There is a class that runs various sanitization filters that you can quickly access in the Genesis Admin Class.
The example code here has those filters in what I deem to be the order of risk. If you can use a higher filter for your option it is safer to use that filter. The lower the filter is, the more risk there is in using it.
/** * Register each of the settings with a sanitization filter type. * * @since 0.9.0 * * @uses genesis_add_option_filter() Assign filter to array of settings. * * @see \Genesis_Settings_Sanitizer::add_filter() Add sanitization filters to options. */ function sanitizer_filters() { //since I'm building some checkboxes programatically I have to add validation programatically. $one_zero = array( 'general_post', 'advanced_checkbox', ); //this gets the post type list $post_types = get_post_types( array( 'public' => true, ) ); //I add the post types to the hard coded options so they can all be filtered foreach( $post_types as $post_type ){ $one_zero[] = 'general_' . $post_type; } //use for checkboxes genesis_add_option_filter( 'one_zero', $this->settings_field, $one_zero ); //only allows integers. Great for text fields that are just for numbers genesis_add_option_filter( 'absint', $this->settings_field, array( ) ); //used to validate a URL genesis_add_option_filter( 'url', $this->settings_field, array( ) ); //if you don't NEED html in your text field use this option for text genesis_add_option_filter( 'no_html', $this->settings_field, array( 'general_size', //these are select fields so there shouldn't ever be HTML 'general_position', //if HTML shows up here someone is trying something tricky ) ); //uses wp_kses to allow some HTML but nothing easily exploitable genesis_add_option_filter( 'safe_html', $this->settings_field, array( 'advanced_text', ) ); //allows full HTML but only for users with unfiltered HTML priviledge genesis_add_option_filter( 'requires_unfiltered_html', $this->settings_field, array( ) ); }
Once again I find myself with a very long post and still much more I’d like to say.
I’m going to stop now though and turn it over to you:
<
ul>
<
ul>
Elise says
I got here by googling “genesis simple share plugin” with the quotes in… It’s been hard to locate information on the topic. I’ve been trying to find the plugin for two solid days (first saw it on the Genesis blog), and judging by this article I guess it’s on it’s way? I absolutely adore the way it looks, by far the prettiest share plugin I’ve ever seen. The only thing I’d wish for more on the plugin is the option to have a “total social shares” number that adds up all of the shares together. Anyway, wish I had gotten in on the early testing, but I’m definitely not famous enough for that. ;). Will be so excited when it’s finally released, however. Nice job on it, seriously.