Multiple Instances of the WordPress Media Uploader

Ever had to implement the WordPress media uploader in a plugin? This article by Tom McFarlin is a good primer that walks you through the basics.

But what if you wanted to go a step further and have multiple media uploaders on the same page, each triggered by a separate button click, and each populating a text field with the URL of the selected media file? How can you do that without repeating a bunch of code?

Let’s assume we have the following PHP that generates the HTML for our text fields and buttons:

<!-- First Custom Image -->
<input class="text-input" type="url" name="book_review[goodreads][url]"
  value="<?php echo esc_url( $options['goodreads']['url'] ); ?>">
<a class="custom-image button-secondary" href="#">
  <?php esc_html_e( 'Set Custom Image', 'book-review' ) ?>
</a>

<!-- Second Custom Image -->
<input class="text-input" type="url" name="book_review[barnes_noble][url]"
  value="<?php echo esc_url( $options['barnes_noble']['url'] ); ?>">
<a class="custom-image button-secondary" href="#">
  <?php esc_html_e( 'Set Custom Image','book-review' ) ?>
</a>

The Javascript

We start by defining a Javascript function that will be responsible for showing the media uploader. I’m only going to focus on the Javascript here, so please refer back to Tom’s post for details on how to enqueue it in PHP:

(function ($) {
  "use strict";

  var fileFrame = null;

  function showMediaUploader(e) {
    // Create the media frame, if applicable.
    if (!fileFrame) {
      fileFrame = wp.media.frames.file_frame = wp.media({
        title: media_uploader.title,  // Translated string
        button: {
          text: media_uploader.button_text  //Translated string
        },
        multiple: false
      });

    fileFrame.on("select", function() {
      // TODO: Handle a file being selected.
    });

    fileFrame.open();
  }
})(jQuery);

Next, we need to attach the event handler to our buttons so that the handler will fire when either of the buttons are clicked. We can do that using the custom-image CSS class that has been assigned to both of the buttons. Assuming that the container for my page has a class of book-review-admin then, using jQuery, we can bind those buttons to the showMediaUploader function like so:

(function ($) {
  "use strict";

  var fileFrame = null;

  $(function() { // Wait for the DOM to load.
    $(".book-review-admin .custom-image").on("click", showMediaUploader);
  });
  ...
})(jQuery);

Now when either of the buttons are clicked, the media file uploader will be shown.

Distinguishing Between Buttons

The only thing left to do is populate the text field with the URL of the selected media file. To do that, we need to be able to distinguish between the buttons so that we can populate the right text box.

You may think that we can just use the Javascript this keyword to access the button inside of the select event handler. After all, if you were to add the statement console.log(this); as the first line in showMediaUploader, it would return the button element. However, our event handler is actually a closure, and by the time it runs, the context of this has changed – it now refers to fileFrame.

One of the nice features of a closure is that it is able to reference the variables of its parent function. That means we can do something like this:

function showMediaUploader(e) {
  var self = this;
  ...

  fileFrame.on("select", function() {
    console.log(self); // The button element.
  });

  fileFrame.open();
}

Adding the Event Handler

Now that we are able to distinguish between the buttons, we can create a new function that takes the button as a parameter, and sets the contents of the text box to the URL of the selected file. In this case, since our text box is the preceding sibling of the button, we can use the jQuery prev() function to select it, and the val() function to set its value:

function setCustomImage(btn) {
  var attachment = fileFrame.state().get("selection").first().toJSON();
  $(btn).prev().val(attachment.url);
}

And then we can just call that function from our event handler, passing the button element as a parameter:

fileFrame.on("select", function() {
  setCustomImage(self);
});

One Final Gotcha

There’s one final thing we need to do. Currently, whenever either of the buttons are clicked, a new select event handler is attached to fileFrame. So if the user clicked both of the buttons, two event handlers would be created. This means that when a file was selected, both of those event handlers would fire and both text boxes would be set to the same URL! Definitely not what you want.

To fix this, we need to remove any event handlers that may already have been added to fileFrame before adding a new one:

function showMediaUploader(e) {
  ...

  // Remove any existing event handlers first.
  fileFrame.off("select");
  fileFrame.on("select", function() {
    setCustomImage(self);
  });
...
}

You should now be able to add multiple instances of the WordPress media uploader, and have them all be handled by the existing Javascript!