PHP Repeater
/**
* 1. Register the meta box and enqueue scripts
*/
function register_repeater_meta_box() {
add_meta_box(
'repeater_meta_box',
'Repeatable Items',
'render_repeater_meta_box',
'your_post_type'
);
// Only load on our post edit screen
global $pagenow;
if ($pagenow === 'post.php' || $pagenow === 'post-new.php') {
wp_enqueue_script('jquery-ui-sortable');
wp_enqueue_script('repeater-js', get_template_directory_uri() . '/js/repeater.js', ['jquery'], '1.0', true);
}
}
add_action('add_meta_boxes', 'register_repeater_meta_box');
/**
* 2. Render the repeater field container
*/
function render_repeater_meta_box($post) {
wp_nonce_field('repeater_nonce', 'repeater_nonce');
// Get existing items
$items = get_post_meta($post->ID, '_repeater_field', true);
$items = is_array($items) ? $items : [['title' => '', 'description' => '']];
echo '<div class="repeater-container" data-template="repeater-template">';
foreach ($items as $index => $item) {
echo '<div class="repeater-row">';
echo '<input type="text" name="repeater_field['.$index.'][title]" value="'.esc_attr($item['title']).'" placeholder="Title">';
echo '<textarea name="repeater_field['.$index.'][description]" placeholder="Description">'.esc_textarea($item['description']).'</textarea>';
echo '<button class="remove-row">Remove</button>';
echo '</div>';
}
echo '</div>';
echo '<button class="add-row">Add Item</button>';
// Template for new rows (hidden by default)
echo '<script type="text/template" id="repeater-template">';
echo '<div class="repeater-row">';
echo '<input type="text" name="repeater_field[__INDEX__][title]" value="" placeholder="Title">';
echo '<textarea name="repeater_field[__INDEX__][description]" placeholder="Description"></textarea>';
echo '<button class="remove-row">Remove</button>';
echo '</div>';
echo '</script>';
}
/**
* 3. Save the repeater data
*/
function save_repeater_meta_box($post_id) {
if (!isset($_POST['repeater_nonce']) || !wp_verify_nonce($_POST['repeater_nonce'], 'repeater_nonce')) {
return;
}
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return;
if (!current_user_can('edit_post', $post_id)) return;
// Sanitize and save
if (isset($_POST['repeater_field'])) {
$clean_items = [];
foreach ($_POST['repeater_field'] as $item) {
$clean_items[] = [
'title' => sanitize_text_field($item['title']),
'description' => wp_kses_post($item['description'])
];
}
update_post_meta($post_id, '_repeater_field', $clean_items);
} else {
delete_post_meta($post_id, '_repeater_field');
}
}
add_action('save_post', 'save_repeater_meta_box');
Javascript
jQuery(document).ready(function($) {
$('.repeater-container').on('click', '.remove-row', function(e) {
e.preventDefault();
$(this).closest('.repeater-row').remove();
});
$('.add-row').on('click', function(e) {
e.preventDefault();
var container = $(this).prev('.repeater-container');
var template = container.data('template');
var index = container.find('.repeater-row').length;
var html = $('#' + template).html().replace(/__INDEX__/g, index);
container.append(html);
});
// Make rows sortable
$('.repeater-container').sortable({
handle: '.repeater-row',
axis: 'y'
});
});
CSS to make it pretty
.repeater-container {
margin-bottom: 15px;
}
.repeater-row {
padding: 15px;
margin-bottom: 10px;
border: 1px solid #ddd;
background: #f9f9f9;
position: relative;
}
.repeater-row input[type="text"],
.repeater-row textarea {
width: 100%;
margin-bottom: 10px;
}
.remove-row {
color: #a00;
cursor: pointer;
background: none;
border: none;
padding: 0;
}
.add-row {
background: #0073aa;
color: white;
border: none;
padding: 5px 10px;
cursor: pointer;
}
Front end usage note
$items = get_post_meta(get_the_ID(), '_repeater_field', true);
if ($items) {
echo '<ul class="repeater-items">';
foreach ($items as $item) {
echo '<li>';
echo '<h3>'.esc_html($item['title']).'</h3>';
echo '<div>'.wp_kses_post($item['description']).'</div>';
echo '</li>';
}
echo '</ul>';
}