There are plenty of tutorials online about creating custom widgets, but unfortunately, most of them rarely go beyond the basics. Which is why, in today's tutorial, I'll show you how to build a testimonial widget with support for unlimited amount of testimonials.

The issue with repeating fields is that you need to take care of naming of the fields properly, which can be tricky.

There are two possible solutions to solving this issue, and for learning purposes, we'll do it the hard way: with Backbone. It's a lightweight JavaScript framework that lets you write frontend application with ease. Granted, we could use jQuery for that, but bear with me, once we're done, you'll be grateful we didn't.

Create and enqueue the necessary javascript file

Before we get started, some boilerplate work is in order; We need to create a new javascript file (in js folder of your theme), and name it admin-testimonials.js. Then paste the following code into your functions.php:

/**
 * Enqueue admin testimonials javascript
 */
function testimonials_enqueue_scripts() {
  wp_enqueue_script(
    'admin-testimonials', get_template_directory_uri() . '/js/admin-testimonials.js',
    array( 'jquery', 'underscore', 'backbone' )
  );
}
add_action( 'admin_enqueue_scripts', 'testimonials_enqueue_scripts' );

Apart from our newly created file we also require an array of dependencies, in our case we need backbone, which is already bundled with WordPress - pretty neat!

Define the class

Next, we need to create a new file, called class-testimonial-widget.php, put it into your theme's inc directory and require it from your functions.php:

/**
 * Load Testimonial Widget
 */
require get_template_directory() . '/inc/class-testimonial-widget.php';

Open the file, because in it, we will define a new class for our testimonial widget, and put the following code in:

<?php

// Prevent direct access to this file
defined( 'ABSPATH' ) or die( 'Nope.' );

/**
 * Register the widget with WordPress
 */
add_action( 'widgets_init', function(){
  register_widget( 'Testimonial_Widget' );
});

class Testimonial_Widget extends WP_Widget {

}

Before we write any methods in this class, it's worth checking the official Widget API documentation. We need the following methods:

  • __construct() is the constructor method. In it, we need to call the parent (WP_Widget) constructor and pass it an id (or false, and it will generate one automatically), the name of our widget, and an optional array of settings.
  • widget() is the rendering method that is called on the presentational side - what out visitors see.
  • update() - as the name suggests, it's the method, responsible for updating the field values. Sanitizing input should be done here.
  • form() is used to render our wp-admin form, through which we manipulate our widgets' data - the most important part of this tutorial.

Define the constructor

  public function __construct() {
    parent::__construct(
      false,
      'Testimonials',
      array( 'description' => 'My Testimonials Widget' )
    );
  }

In case you didn't know, a constructor is a special method that gets called automatically when the class is instantiated (with a new keyword).

There are plenty of options you can pass to the parent constructor (WP_Widget::__construct), but for the purpose of this tutorial, let's keep things concise. When you save the method, this is how the widgets dashboard looks like:

Testimonial widget button
Our new testimonial widget button

Define the updating method

Next, let's define the method which will be responsible for security. When accepting any kind of input from your users, it's always recommended to properly sanitize data.

This example is a basic one, but it should give you a good starting point:

public function update( $new_instance, $old_instance ) {
  $instance                 = array();
  $instance['header']       = wp_kses_post( $new_instance['header'] );
  $instance['testimonials'] = $new_instance['testimonials'];
  return $instance;
}

Here, we create a new array that will hold the instance header and another array with all our testimonials.

In this case, instance is a unique copy of the widget - because you could have more widgets of the same type in various sidebars. Each would be an instance.

Homework: In this method, I've decided to only sanitize the header, but pass all the testimonials as they are. When you're done with this tutorial, traverse through all the testimonials and apply one of the sanitization functions WordPress supports. Start here.

Define the wp-admin form rendering method

Now to the most important part: the testimonials form. In it, we have to take care of the following responsibilities:

  • fetching data or setting some defaults if no data exists yet
  • display any non-repeating fields (like header) if we have them
  • since we're using Backbone (JavaScript), we also need to define a template to be used for each individual testimonial
  • display a placeholder, which all the testimonials will be appended to.
  • initialize fetching the data once it renders

Copy the following method into the class (explanation below):

public function form( $instance ) {
  // segment #1
  $header = empty( $instance['header'] ) ? 'Testimonials' : $instance['header'];

  $testimonials = isset( $instance['testimonials'] )
    ? array_values( $instance['testimonials'] )
    : array( array( 'id' => 1, 'quote' => '', 'author' => '', 'image' => '' ) );

?>

  <!— segment #2 —>
  <p>
    <label for="<?= $this->get_field_id( 'header' ); ?>">Header</label>
    <input class="widefat" id="<?= $this->get_field_id( 'header' ); ?>" name="<?= $this->get_field_name( 'header' ); ?>" type="text" value="<?= esc_attr( $header ); ?>" />
  </p>

  <!— segment #3 —>
  <script type="text/template" id="js-testimonial-<?= $this->id; ?>">
    <p>
      <label for="<?= $this->get_field_id( 'testimonials' ); ?>-<%- id %>-quote">Quote:</label>
      <textarea rows="4" class="widefat" id="<?= $this->get_field_id( 'testimonials' ); ?>-<%- id %>-quote" name="<?= $this->get_field_name( 'testimonials' ); ?>[<%- id %>][quote]"><%- quote %></textarea>
    </p>
    <p>
      <label for="<?= $this->get_field_id( 'testimonials' ); ?>-<%- id %>-author">Author:</label>
      <input class="widefat" id="<?= $this->get_field_id( 'testimonials' ); ?>-<%- id %>-author" name="<?= $this->get_field_name( 'testimonials' ); ?>[<%- id %>][author]" type="text" value="<%- author %>" />
    </p>
    <p>
      <input name="<?= $this->get_field_name( 'testimonials' ); ?>[<%- id %>][id]" type="hidden" value="<%- id %>" />
      <a href="#" class="js-remove-testimonial"><span class="dashicons dashicons-dismiss"></span>Remove Testimonial</a>
    </p>
  </script>

  <!— segment #4 —>
  <div id="js-testimonials-<?= $this->id; ?>">
    <div id="js-testimonials-list" style="padding: 0px 15px; background: #fafafa;"></div>
    <p>
      <a href="#" class="button" id="js-testimonials-add">Add New Testimonial</a>
    </p>
  </div>

  <!— segment #5 —>
  <script type="text/javascript">
    var testimonialsJSON = <?= json_encode( $testimonials ) ?>;
    myWidgets.repopulateTestimonials( '<?= $this->id; ?>', testimonialsJSON );
  </script>

  <?php
}

Let's go over this rather lengthy method to see what's going on.

In the first segment, we define a header that will be used on the frontend. You could easily add more fields here, for example if you had a slider with these testimonials (such as one on our home page, then you could add some slider configuration options like cycle interval. Segment #2 then just renders those fields.

Segment #3 is where things get exciting. You may have noticed that all output here is wrapped in a <script> tag. Because we use Backbone to render the repetitive fields, it needs a template to apply to each object (the testimonial in our case). We use two fields for this tutorial — quote and author — but you can use as many as you want. Since we want to be able to remove a testimonial, the last element is a button to do just that. You might have noticed it has an empty href parameter; That's because we will use JavaScript to make an asynchronous call to WordPress.

The fourth segment is basically a placeholder for the list of all testimonials to appear. Apart from that, I've also decided to render the New Testimonial button here, but feel free to put it elsewhere.

In the last, fifth segment, we create a JSON object from our existing testimonials, and then call a function that will render them (more on that shortly). We pass it the id of the Widget instance and the JSON object.

If you try to refresh the widget dashboard at this point, the console will report javascript errors since our repopulateTestimonials function doesn't exist yet. Before we define it, let's first…

Define the website rendering method

This is the last method and it's used to display our testimonials in a sidebar:

  public function widget( $args, $instance ) {

    $header = apply_filters( 'widget_title', empty( $instance['header'] ) ? '' : $instance['header'], $instance, $this->id_base ); ?>

    <h3><?= $header ?></h3>
    <?php foreach ( $instance['testimonials'] as $testimonial ): ?>
      <blockquote>
        <p><?= $testimonial['quote'] ?></p>
        <footer>— <?= $testimonial['author'] ?></footer>
      </blockquote>
    <?php endforeach;

  }

Not much explanation is needed here, we apply the standard filter to our header and display it, then we loop through all of the testimonials and display both the quote and it's author.


Now it's time to put on our JavaScript jacket and dig into the beautiful world of Backbone - feel free to close the php file as we won't need it anymore.

The Backbone of our Widget

Remember the admin-testimonials.js we created earlier? We'll make the magic happen here.

If you're unfamiliar with Backbone, fear not, what we'll do here is pretty basic, but if you still don't understand some parts, visit their official documentation - it's just one (although a rather long) page.

The first step we'll do is creating a namespace:

var myWidgets = myWidgets || {};

If you're unfamiliar with this technique, it's a way to organise your code, so it doesn't pollute the global namespace - read more about it in this article.

Now before we start writing any code for our testimonials, it's worth checking out all the requirements:

  • we need a way to save and reference each testimonial.
  • we need to update a testimonial.
  • we need to create a new testimonial or delete an existing one.

In development world, an entity that represents some arbitrary object is usually called a model, and Backbone supports modelling data out of the box:

myWidgets.Testimonial = Backbone.Model.extend({
  defaults: { 'quote':  '', 'author': '' }
});

Here, we defined a Testimonial model, which extends the default behaviour that Backbone configures for model, an example is the defaults key that is not always necessary, but can help us visually identify the schema of our data. First requirement solved.

Now we need to define two Views. One will take care of the individual testimonial and it's behaviour, and the other will make sure the whole list is in order. A View, in JavaSsript/Backbone terminology means some logic that is used to manipulate the HTML output of an element (or a group of them). There's usually some confusion present about this because in most server-side languages, a View is the rendered HTML but the rendering logic is called a controller or a presenter. And the View is then called a template. I know. A mess.

First, we need to create the single view, responsible for each individual testimonial object:

myWidgets.TestimonialView = Backbone.View.extend( {
  className: 'testimonial-widget-child',
  events: {
    'click .js-remove-testimonial': 'destroy'
  },
  initialize: function ( params ) {
    this.template = params.template;
    this.model.on( 'change', this.render, this );
    return this;
  },
  render: function () {
    this.$el.html( this.template( this.model.attributes ) );
    return this;
  },
  destroy: function ( ev ) {
    ev.preventDefault();
    this.remove();
    this.model.trigger( 'destroy' );
  },
} );

Let's go over what this view does:

  • className defines a class to each individual testimonial in our list (should you need to style it)
  • events defines a list of events and corresponding actions that should be triggered upon events occurring.
  • initialize gets called whenever a new testimonial is created (with JavaScript). It expects to receive a template parameter and also attaches a listener in order to re-render it should a change occur.
  • render builds the HTML by joining the data with the template
  • destroy - removes both the HTML and the data associated

Now that we've taken care of an individual testimonial, let's focus on the List view:

myWidgets.TestimonialsView = Backbone.View.extend( {
  events: {
    'click #js-testimonials-add': 'addNew'
  },
  initialize: function ( params ) {
    this.widgetId = params.id;
    this.$testimonials = this.$( '#js-testimonials-list' );
    this.testimonials = new Backbone.Collection( [], { model: myWidgets.Testimonial } );
    this.listenTo( this.testimonials, 'add', this.appendOne );
    return this;
  },
  addNew: function ( ev ) {
    ev.preventDefault();
    var testimonialId = 0;
    if ( ! this.testimonials.isEmpty() ) {
      var testimonialsWithMaxId = this.testimonials.max( function ( testimonial ) {
        return testimonial.id;
      } );
      testimonialId = parseInt( testimonialsWithMaxId.id, 10 ) + 1;
    }
    var model = myWidgets.Testimonial;
    this.testimonials.add( new model( { id: testimonialId } ) );
    return this;
  },
  appendOne: function ( testimonial ) {
    var renderedTestimonial = new myWidgets.TestimonialView( {
      model:    testimonial,
      template: _.template( jQuery( '#js-testimonial-' + this.widgetId ).html() ),
    } ).render();
    this.$testimonials.append( renderedTestimonial.el );
    return this;
  }
} );

The list view is a bit more complex, because it manages the addition of a new testimonial and removal of the existing ones:

  • initialize, as with the single view is automatically called when we instantiate it, and it saves all testimonials, both as a DOM object and as a collection (note the $ character). It also listens to the collection and triggers the add function whenever a new testimonial object is added to the collection.
  • addNew gets triggered whenever we click on the “Add New” button, and that is defined in our events object. It also makes sure to store the id (each being an increment)
  • appendOne, as you might imagine, creates a new single view with the testimonial we pass it and appends it to the DOM.

At this point, we're still missing one essential element, and that's the repopulateTestimonials function we're calling from the PHP template we defined first:

myWidgets.repopulateTestimonials = function ( id, JSON ) {
  var testimonialsView = new myWidgets.TestimonialsView( {
    id: id,
    el: '#js-testimonials-' + id,
  } );
  testimonialsView.testimonials.add( JSON );
};

All we do here is create our list view by passing it the ID (useful in case we used more than one instance of our widget in the same sidebar) and the DOM element we want to append our list to. Finally we push all our existing testimonials into the view.

Save the file and refresh your widgets dashboard, if you see something like this then pat yourself on the back — job well done!

Testimonial widget working example
The final result

If you followed this tutorial along without writing any code, grab this gist to save some time :)

Conclusion

Pretty neat huh? We could use jQuery to achieve the same effect, but Backbone allows us to properly structure our mini Javascript application, which is a huge benefit when things get more complex than your usual hide, show and toggle.

We could complicate things further by adding an attachment field to each testimonial (for avatars), but this tutorial is already long as it is so I might do that in the future — let me know in the comments below.

Lastly, I'd like to thank Primoz Cigler from ProteusThemes for helping me with the code and giving me an idea to write :)Quality: The Codeable Differene

  • Janneman Human

    Very good example! I tried it for my own needs and it works perfectly.

    I have one question, what about reordering the items? How would you handle that?

  • Naresh Devineni

    Hi, Loved this tutorial. But there is a problem. When the widget is added to a sidebar, Apart from the default testimonial, testimonials added with “add new testimonial” button are gone after hitting the save button. Functionality is working as expected only after manually hitting the save button. Only after hitting the save button, testimonials added with “add new testimonial” are saved.

    After some trail and error i figured out that the problem is with widget id, when the template is rendered, __i__ is printed in the javascript template instead of actual widget id. But after hitting the save button, functionality is working as expected with actual widget id. This is my problem. and here is the screenshot.

    https://uploads.disquscdn.com/images/6b34aa12ed46fec27ad677cdf4a57381944b3c3cfb031d019f22b5430ea6e955.png

    The functionality is working as expected in Proteus theme Cargo Press with out hitting the save button, although the code has several improvements like sanitization and code organization.

    Should I run the widget through any filters? Should I include the widget after some other this is loaded?? Would you tell me where the problem is??

  • Naresh Devineni

    Hi, Loved this tutorial. But there is a problem. When the widget is added to a sidebar, Apart from the default testimonial, testimonials added with “add new testimonial” button are gone after hitting the save button. Functionality is working as expected only after manually hitting the save button. Only after hitting the save button, testimonials added with “add new testimonial” are saved.

    After some trail and error i figured out that the problem is with widget id, when the template is rendered, __i__ is printed in the javascript template instead of actual widget id. But after hitting the save button, functionality is working as expected with actual widget id. This is my problem. and here is the screenshot.

    https://uploads.disquscdn.com/images/6b34aa12ed46fec27ad677cdf4a57381944b3c3cfb031d019f22b5430ea6e955.png

    The functionality is working as expected in Proteus theme Cargo Press with out hitting the save button, although the code has several improvements like sanitization and code organization. This is screenshot of the proteus theme widget with widget id rendered properly immediately after adding widget to the sidebar.

    https://uploads.disquscdn.com/images/6f951be58ce49ff335b72e8426cbc604c322a47733a20fe93e082b0ed94aa0d7.png

    Should I run the widget through any filters? Should I include the widget after some other this is loaded?? Would you tell me where the problem is??

    • Naresh Devineni

      One more finding, wordpress is rendering widget field id just fine if mustache template syntax is being used instead of underscore template.

      • Naresh Devineni

        Found the solution, Finally got it to work.Use Mustache or Handlebars template system instead of underscore.js template system. Some how wordpress widget envireonment is not getting along with underscore template system.

  • Mike Hopkins

    Good Day! Thank you for this, works great however when removing a testimonial, the save button remains disabled – I’m able to inspect code and remove the disabled attribute and when using the button again the change/removal of testimonial takes place. How can I fix this in the code? Can’t quite get it right.

    • Mike Hopkins

      Okay so i’ve added the following line $(‘input[type=submit]’).trigger(‘change’); just below the this.model.trigger( ‘destroy’ ); line of code. It seems to be working ok but will need to test further. Maybe the author of the tutorial can clarify?