Tutorial: Adding a custom meta field as a dropdown menu in WordPress

One of my favorite things about devel­op­ing the LightSpeed site was the inter­est­ing cus­tom CMS fea­tures I needed to cre­ate to accom­plish the fea­tures that we wanted.

I knew I wanted to have a cus­tom post type for the Issues so that I could aggre­gate all con­tent from an issue in a sin­gle post, and also store things like buy link urls for the var­i­ous e-​​issues all in a sin­gle place, rather than on each con­tent post itself. The idea being, the edi­tor cre­ates an issue, which stores links, cover image, mast­head image, artist link, etc.. When I am writ­ing tem­plate code for an indi­vid­ual post, I can grab the value of a meta field for the id of the issue and have all of that, or I can just query the lat­est issue for set­tings such as which mast­head graphic to use.

To make this work, I would need a way on the indi­vid­ual con­tent entries to link them to the issue posts.  This to be as user-​​friendly as pos­si­ble, so sim­ply using a nor­mal meta field and requir­ing some­thing like a post ID in it wasn’t going to cut it.  So I trolled the web and exper­i­mented until I worked up the fol­low­ing code to accom­plish the cus­tom select menus.

All of the code below goes in the functions.php file. This code has been cob­bled together from var­i­ous sources, pri­mar­ily this tuto­r­ial. If you want a more detailed walk-​​through, take a look at that. *ETA: I’m told the orig­i­nal of that tuto­r­ial can actu­ally be found at wefunc​tion​.com. It’s a bet­ter run down of the indi­vid­ual parts.

First, we cre­ate an array of arrays defin­ing the new cus­tom meta boxes that would appear. In the actual site, I imple­mented one of these for authors as well as issues (rather than cre­at­ing WordPress users to cre­ate a data object for authors), but I’ve removed that to sim­plify what I’ve pre­sented here.

$new_meta_boxes =
		array(
			"issueslist" => array(
				"type" => "select",
				"std" => "",
				"name" => "assignissue",
				"title" => "Set Issue",
				"description" => "select the issue for the content",
				"category" => 4
			)
		);

Our meta box array defines a type of field (text, select, textarea), a name, the title asso­ci­ated with it, a descrip­tion, and a cat­e­gory for the related con­tent. In this case, cat­e­gory 4 is assigned to all Issue posts types. I’m cur­rently using Magic Fields to accom­plish cus­tom write pan­els, which means I have not con­verted over to using cus­tom post types as intro­duced fully in WordPress 3.0. To sort con­tent out for my cus­tom write pan­els, I assign cat­e­gories to them, and then I exclude these cat­e­gories from most loops. They only dis­play when I write a cus­tom loop for them.

Next, we write a func­tion to loop through our $new_​meta_​boxes array and cre­ate the HTML used in the admin/​. One thing I noticed going through this code–I’m not entirely sure why I left the new_​meta_​boxes array out­side of the new_​meta_​boxes() func­tion. Oh, and yes, I don’t use camel­Case for nam­ing my func­tions and vari­ables. I find it’s eas­ier to screw up cap­i­tal­iza­tion than it is to leave out under­scores, so it saves me time on cod­ing. Your mileage may of course differ.

function new_meta_boxes() {
		global $post, $new_meta_boxes;
		foreach($new_meta_boxes as $meta_box) {
			$args=array(
			  'category' => $meta_box['category'],
			  'orderby' => 'title',
			  'showposts' => '9999',
			  'order' => 'ASC',
			);
			$args_future= array(
			  'category' => $meta_box['category'],
			  'orderby' => 'title',
			  'showposts' => '9999',
			  'order' => 'ASC',
			  'post_status' => 'future'
			);
			$getposts = get_posts($args);
			$getpostsFuture = get_posts($args_future);
			echo'<input type="hidden" name="'.$meta_box['name'].'_noncename" id="'.$meta_box['name'].'_noncename" value="'.wp_create_nonce( plugin_basename(__FILE__) ).'" />';
			echo'<h2>'.$meta_box['title'].'</h2>';
			if( $meta_box['type'] == "text" ) {
				$meta_box_value = get_post_meta($post->ID, $meta_box['name'].'_value', true);
				if($meta_box_value == "")
					$meta_box_value = $meta_box['std'];
				echo'<input type="text" name="'.$meta_box['name'].'_value" value="'.$meta_box_value.'" size="55" />';
			} elseif ( $meta_box['type'] == "select" ) {
				echo'<select name="'.$meta_box['name'].'_value">';
				echo'<option value="">'.$meta_box['title'].'</option>';
				foreach ($getposts as $option) {
					if ( get_post_meta($post->ID, $meta_box['name'].'_value', true) == $option->ID ) {
						$sel =  ' selected="selected"';
					} else {
						$sel = '';}
					echo'<option value="'.$option->ID.'"'. $sel .'>'. $option->post_title.'</option>';
				}
				foreach ($getpostsFuture as $option) {
					if ( get_post_meta($post->ID, $meta_box['name'].'_value', true) == $option->ID ) {
						$sel =  ' selected="selected"';
					} else {
						$sel = '';}
					echo'<option value="'.$option->ID.'"'. $sel .'>'. $option->post_title.'</option>';
				}
				echo'</select>';
			}
			echo'<p><label for="'.$meta_box['name'].'_value">'.$meta_box['description'].'</label></p>';
		}
	}

To explain what’s hap­pen­ing here: we’re using get_​posts to cre­ate an array of posts based on our set­tings from the meta boxes array. I’ve put together two post arrays here because we wanted to be able to set sched­uled issues as well as posted ones. By default, sched­uled posts aren’t queried, and I couldn’t fig­ure out how to use a sin­gle get_​posts() to get both future posts and pub­lished ones. Simple enough– I just tack on another fore­ach loop for the future posts to the bot­tom of the select. Still, it would have been bet­ter to just do one. I need to sort this out at some point.

Line 19 is a hid­den field with nonces for secu­rity, and comes into play in our save_​postdata() func­tion that we’ll get to later on.

Line 20 is our header for the select box.

Lines 27–43 deal with the cre­ation of the select boxes. For each of the get_​posts arrays, we loop through and echo out a title inside of an option and an ID as the value, so that we ulti­mately can save the value of select box to the meta field on the post, and call it using get_​post_​meta in our templates.

We also have some basic if else code check­ing to see what the type of meta box is, as I antic­i­pated maybe need­ing to add some reg­u­lar text fields, but never actu­ally needed them.

It’s not pretty and prob­a­bly not as effi­cient as it could be, but it works.

Next, we cre­ate the func­tion that we’ll actu­ally call when we add the action using WordPress hooks. Add meta_​box has a lot of para­me­ters that you can’t pass when you use add_​action, so you have to cre­ate a wrap­per func­tion like this. And it lets use one add_​action to cre­ate all the meta fields.

function create_meta_box() {
	global $theme_name;
	if (function_exists('add_meta_box') ) {
		add_meta_box( 'new-meta-boxes', 'Author Info', 'new_meta_boxes', 'post', 'normal', 'high' );
	}
}

In ret­ro­spect, I’m not sure why this has the $theme_​name global here. I should have com­mented that require­ment so I would know why in the world I have it here. Ah well. And here’s how we hook our func­tion into the admin so we get to see our hand­i­work in the WordPress admin:

add_action('admin_menu', 'create_meta_box');

All of the above will gen­er­ate our dis­play, but it doesn’t actu­ally save any­thing. This next bit of code, I took mostly unmod­i­fied from the other tuto­r­ial. This func­tion does all the nec­es­sary secu­rity checks– the nonce, check­ing the user to make sure they have the per­mis­sions to mod­ify the data, and finally adding the data, or updat­ing it, or delet­ing it, depend­ing on what’s there already. When you’re work­ing with small bud­gets, you learn to use what you can and save time, so as to save your clients money.

function save_postdata( $post_id ) {
	global $post, $new_meta_boxes;
	foreach($new_meta_boxes as $meta_box) {
		// Verify
		if ( !wp_verify_nonce( $_POST[$meta_box['name'].'_noncename'], plugin_basename(__FILE__) )) {
			return $post_id;
		}
		if ( 'page' == $_POST['post_type'] ) {
			if ( !current_user_can( 'edit_page', $post_id ))
			return $post_id;
		} else {
			if ( !current_user_can( 'edit_post', $post_id ))
			return $post_id;
		}
		$data = $_POST[$meta_box['name'].'_value'];
		if(get_post_meta($post_id, $meta_box['name'].'_value') == "")
		add_post_meta($post_id, $meta_box['name'].'_value', $data, true);
		elseif($data != get_post_meta($post_id, $meta_box['name'].'_value', true))
		update_post_meta($post_id, $meta_box['name'].'_value', $data);
		elseif($data == "")
		delete_post_meta($post_id, $meta_box['name'].'_value', get_post_meta($post_id, $meta_box['name'].'_value', true));
	}
}

All that’s left is to hook this in with add_​action to the save_​post hook, so that when a post is saved, the data in our cus­tom meta boxes are saved as well.

add_action('save_post', 'save_postdata');

So any­where I needed to get the value of the issues box, I just use the fol­low­ing code:

$post_author = get_post_meta($post->ID, 'assignauthor_value', true);

That gives me a vari­able I can then use to query against in a cus­tom loop, and then grab other cus­tom fields using get() which is pro­vided by the Magic Fields plugin.

So that’s the code in a nut­shell. Questions and com­ments welcome!

    Posted on:

    5 Responses

    1. JLeuze says:

      The LightSpeed site is really sharp, I was won­der­ing how you did some of these details on the site.

      I haven’t use Magic Fields before, actual cus­tom post types work more like pages, but it seems like cus­tom posts with Magic Fields are still tech­ni­cally posts and work mostly like posts, is that right?

      Custom meta boxes make things so much eas­ier for the end user, I’ve been using them more and more in my projects. That’s a great tuto­r­ial, I read that one too and learned a lot. By the way, that tuto­r­ial was scraped from wefunc​tion​.com, the orig­i­nal might be a tad dif­fer­ent than the copy.

      Have you tried cus­tom tax­onomies? Do you think there are any advan­tages to using meta boxes for authors and other con­trib­u­tors? I’ve been think­ing about using cus­tom tax­onomies on StarShipSofa for pod­cast authors, nar­ra­tors, and artists so that read­ers could select a con­trib­u­tor and see every pod­cast they have con­tributed to.

      • Thanks for that– it sucks when the orig­i­nal shows up lower than the copy in google searches.

        Magic Fields can work from page or a post to cre­ate its cus­tom write pan­els, so it can do both, really.

        I haven’t tried cus­tom tax­onomies yet, as I haven’t found a good use. In my case, the rea­son I went with an entire cus­tom post type for the authors is because I wanted to be able to store biog­ra­phy info, face­book, twit­ter, etc all in a sin­gle place, and I don’t think I would get that out of cus­tom tax­onomies here any­way. I can see how they will be use­ful in dif­fer­ent cir­cum­stances though.

        • JLeuze says:

          Cool, Magic Fields sounds pretty handy, I’ll have to check it out.

          I’m sure adding new authors every month as cus­tom posts is a lot less of a headache than some of the alter­na­tives! Yeah, cus­tom tax­onomies seem to be most use­ful when used in com­bi­na­tion with cus­tom post types.

    2. […] find­ing good tuto­ri­als on the use of check­boxes or radio but­tons are a bit more dif­fi­cult to find. Jeremiah Tolbert gives a good tuto­r­ial on a drop­down selection […]

    Leave a Reply