One of the most confounding things to figure out in the world of WordPress plugin development is the repeatable field. They’re just generally obnoxious to write, sadly, and there’s very little real documentation out there about them. I use them regularly in quite a few of my plugins, and even now I have to recheck how to do them each time. That said, I never considered writing a tutorial on it until a friend, and a talented developer in his own right, mentioned that he had recently been unable to make one work. So, here’s a quick run-through on how I do repeatable fields.

If you want to follow along with some working code, you can download a pre-built plugin based on this tutorial here.

Step One: The Meta Box

Writing the actual meta box HTML for a repeatable field isn’t any different than any other field, but there are a few subtle differences. First, you need to register your meta box (assuming you aren’t modifying an existing one):

“`php
/**
* Register new meta box
*
* @return void
*/
function repeatable_demo_add_meta_box() {
add_meta_box( ‘repeatable_demo’, ‘Repeatable Demo’, ‘repeatable_demo_render_meta_box’, ‘post’, ‘normal’, ‘default’ );
}
add_action( ‘add_meta_boxes’, ‘repeatable_demo_add_meta_box’ );
“`

Pretty straightforward, right? Moving on to rendering the meta box.

“`php
/**
* Render the meta box
*
* @global object $post The WordPress object for this post
* @return void
*/
function repeatable_demo_render_meta_box() {
global $post;

$demo_data = get_post_meta( $post->ID, ‘_repeatable_demo_data’, true );
?>

$value ) {
$field1 = isset( $value[‘field1’] ) ? $value[‘field1’] : ”;
$field2 = isset( $value[‘field2’] ) ? $value[‘field2’] : ”;
$args = compact( ‘field1’, ‘field2’ );

repeatable_demo_render_row( $key, $args, $post->ID );
}
} else {
repeatable_demo_render_row( 1, array( ‘field1’ => ”, ‘field2’ => ” ), $post->ID );
}
?>

Field 1 Field 2

Ok, so this function is a bit different than usual… It’s just the same as any other field, except rather than rendering the field directly, we’re passing off the specific data for a given row to a helper function repeatable_demo_render_row() which is going to handle the rendering for us. Additionally, we’re calling that function twice; once in a foreach loop which triggers if there are existing row, and once with a few defaults for when we’re looking at a fresh post with no existing data.

Step Two: The Helper Function

Now that we’ve looked at the basics of the meta box, let’s dig into that little helper function we keep referencing:

“`php
/**
* Render individual meta box rows
*
* @param int $key The key for a given row
* @param array $args The values we are passing to the function
* @param int $post_id The ID of the post we are editing
* @return void
*/
function repeatable_demo_render_row( $key, $args = array(), $post_id ) {
?>

x

This should be somewhat familiar territory. It’s not too different than what we would typically render for any other meta box field. Two things to note though: first, rather than each field having a unique name, we’ve switched the name to a multi-dimensional array where the key of a given row and the field are hierarchical keys. Second, we have one odd-looking row tacked on to the end of our collection of fields. Shouldn’t take long to figure out what it’s for, but I’ll make it simple. It renders a delete button. Repeatable rows would be kind of silly if you could only add, but never delete!

Step Three: Saving the Meta Box

Saving is simple! One thing you might want to remember, though is that copying this (or any other code in this post) directly into a theme or plugin is not the best idea. Even if you rename everything. Because I’m not including any security or sanitization. Seriously. Escape everything, sanitize your data, use nonces; you know the drill.

“`php
/**
* Save post meta when the save_post action is called
*
* @param int $post_id The ID of the post we are saving
* @global object $post The post we are saving
* @return void
*/
function repeatable_demo_save_meta_box( $post_id ) {
global $post;

// All the fields we want to save
$fields = array(
‘_repeatable_demo_data’
);

foreach( $fields as $field ) {
if( isset( $_POST[$field] ) ) {
if( is_string( $_POST[$field] ) ) {
$new = esc_attr( $_POST[$field] );
} else {
$new = $_POST[$field];
}

update_post_meta( $post_id, $field, $new );
} else {
delete_post_meta( $post_id, $field );
}
}
}
add_action( ‘save_post’, ‘repeatable_demo_save_meta_box’ );
“`

This is a stripped-down version of the standard save meta box function that I use for pretty much everything. Nothing fancy, really straightforward, and nothing ‘special’ for repeatable fields. Moving on.

Step Four: JS or Where the Magic Happens

Here’s where things get complicated. Given the length of this particular code block, and the fact that I’m going to want to explain as I go, I’m going to break this down Barney style and do it in little pieces. I’m also going to forgo stuff like loading the Javascript… hopefully, you have at least a basic understanding of how that stuff works.

“`jquery
var Repeatable_Demo_Configuration = {
init: function () {
this.add();
this.remove();
},
“`

Nothing challenging so far. Basic instantiation stuff…

“`jquery
clone_repeatable: function (row) {
var key, highest, clone;

key = highest = 1;
row.parent().find(‘.repeatable-demo-repeatable-row’).each(function () {
var current = $(this).data(‘key’);
if (parseInt(current) > highest) {
highest = current;
}
});

highest += 1;
key = highest;
“`

Now we’re getting into the fun stuff! In a nutshell, all we’re doing here is finding the relevant rows (remember using the repeatable-demo-repeatable-row class earlier?) and calculating out the highest currently used key.

“`jquery
clone = row.clone();
clone.find(‘select’).each(function () {
$(this).val(row.find(‘select[name=”‘ + $(this).attr(‘name’) + ‘”]’).val());
});

clone.attr(‘data-key’, key);
clone.find(‘td input, td select, textarea’).val(”);
clone.find(‘input, select, textarea’).each(function () {
var name = $(this).attr(‘name’);

name = name.replace(/\[(\d+)\]/, ‘[‘ + parseInt(key) + ‘]’);
$(this).attr(‘name’, name).attr(‘id’, name);
});

clone.find(‘.repeatable-demo-remove-repeatable’).css(‘display’, ‘inline-block’);

return clone;
},
“`

Here’s where the actual grunt work gets done. We’re merely cloning the last row, then running through it and clearing all data and updating the name and ID to match the new row (remember that special key we used in the name earlier?).

“`jquery
add: function() {
$(‘body’).on(‘click’, ‘.submit .repeatable-demo-add-repeatable’, function (e) {
e.preventDefault();

var button = $(this),
row = button.parent().parent().prev(‘tr’),
clone = Repeatable_Demo_Configuration.clone_repeatable(row);

clone.insertAfter(row);
});
},
“`

This function does what it looks like: when you click the button that has the proper class, it triggers the clone function. Simple as that.

“`jquery
remove: function() {
$(‘body’).on(‘click’, ‘.repeatable-demo-remove-repeatable’, function (e) {
e.preventDefault();

var row = $(this).parent().parent(‘tr’),
count = row.parent().find(‘tr’).length – 1;

if (count > 1) {
$(‘input, select’, row).val(”);
row.fadeOut(‘fast’).remove();
} else {
$(‘input, select’, row).val(”);
}
});
}
};

Repeatable_Demo_Configuration.init();
“`

Similar to the previous function, this removes a row when the button with the proper class is pressed. However, it also has one extra bit of logic: if the row we’re removing is the very last row, clear the contents rather than removing it. Empty tables don’t look good!

And that’s pretty much it! It’s not as complicated as it looks. However, if you want a more comprehensive example I’d suggest taking a look at the source code for Easy Digital Downloads. After all, that’s where I learned, and my code is just a tweaked version of what Pippin wrote!

If you have any specific questions, please feel free to ask. Writing a technical tutorial this late at night on no sleep while on a trip may well have resulted in some convoluted nonsense, but I hope that I got the point across!

Show Full Content
Previous Let’s Talk About Addons
Next Editor Wars

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *

Close
Close