JavaScript WordPress like hooks

An implementation of a Backbone based JavaScript hooking system.

Hooks

One of the most powerful features of WordPress is the Hooks API; this feature, taken as granted in the PHP side of WordPress is missing, to my knowledge, on the JavaScript side of things.
While JavaScript allows for independent and self-contained functions and modules to be written (and managed with a tool like Bower) the PHP and JavaScript ensemble contextualized in the plugin mechanism WordPress relies on is not that fluid.
In a client project I’m using various plugins needing, on the Admin side of WordPress, some JavaScript driven interaction and I’ve set to solve a common problem: conditional visibility of a row generated by the Custom Meta Boxes 2 plugin.

An easy enough task

The task itself if trivial in JavaScript terms and there are many ways it can be dealt with efficiently; I’ve grown fond of Backbone and have done anything but the smallest task relying on it and on CoffeeScript, an example solution is the one below

(($) ->

    class RowToggle extends Backbone.View
        @$row: null
        events:
            click: 'toggleTargetVisibility'
        showRow: =>
            @$row.show()
        hideRow: =>
            @$row.hide()
        toggleTargetVisibility: =>
            if @el.checked then @showRow() else @hideRow()
        initialize: ->
            target = @$el.data 'toggle-target'
            @$row = $('.cmb-row.cmb2-id-' + target)
            @toggleTargetVisibility

    $(document).ready ->
        _.each $('input[type="checkbox"][data-toggle-target]'), (el) ->
            new RowToggle
                el: $(el)

)(jQuery)

on the PHP side of things I’m registering the toggling checkbox and adding a custom attribute

add_action('cmb2_init', 'add_my_metabox');
function add_my_metabox() {
    $mb = new_cmb2_box( [
        'id'           => 'post_settings',
        'title'        => 'Settings',
        'object_types' => [ 'post' ],
        'context'      => 'normal',
        'priority'     => 'high',
        'show_names'   => true
    ] );
    $mb->add_field( [
        'name'       => 'A visibility toggle',
        'id'         => 'toggle',
        'type'       => 'checkbox',
        'default'    => 'on',
        'attributes' => [
            'data-toggle-target' => 'toggled-setting'
        ]
    ] );
    $mb->add_field( [
        'name'             => 'Toggled setting',
        'id'               => $this->prefix . 'toggled_setting',
        'type'             => 'text'
    ] );
}

which will yield this result A simple checkbox visibility toggle

A hooked start

I’d like to create a re-usable solution I could rely on any time I’ve to deal with plugins that follow common patterns; to give an example I’ve used Custom Meta Boxes 2 for some time now and have written the same visibility toggling JavaScript code all the times. Packaging it in a plugin could probably be a more long-term and energy-efficient solution.
As plugins go I know first hand what the intricacies of asynchronous running might be and I’d like very much to be able to rely on a hook mechanism in the JavaScript context. I’ve built a simple one

(($)->
    hookBus = window.hookBus = window.hookBus || _.extend {}, Backbone.Events

    runHook = (hookName,p) ->
        hook = hookName + ':' + p
        try
            hookBus.trigger hook
        catch error
            message = 'Exception in ' + hook + ' - ' + error.message
            if console then console.log message

    $(document).ready ->
        # 0 to 10 included
        priorities = _.range 11
        runHook 'initBase', p for p in priorities
        runHook 'initFunctional', p for p in priorities
        runHook 'initUi', p for p in priorities
)(jQuery)

There is a measure of protection built-in it where exceptions thrown during a hook run will shut down the hook but graciously fail and possibly log; it’s draft code but still works.

WordPress hooking

To have the possibility to hook in that code I’ve laid out a basic structure using the Script Utils to make my life easier (a similar result can be obtained with some additional work)

function js_backbone_utilities_load() {
    $script_manager = Scripts::instance( plugins_url( '/js', __FILE__ ) );

    // always register the main one
    $src  = $script_manager->get_src( '/js-backbone-main.js' );
    $deps = array( 'jquery', 'underscore', 'backbone' );
    wp_register_script( 'js-backbone-main', $src, $deps, null, true );

    // register the other ones
    $scripts = array(
        'cmb2-checkbox-toggle' => '/cmb2-checkbox-toggle.js'
    );
    foreach ( $scripts as $handle => $script ) {
        $src = $script_manager->get_src( $script );
        wp_register_script( $handle, $src, $deps, null, true );
    }
}

function js_backbone_main_enqueue() {
    wp_enqueue_script( 'js-backbone-main' );
}

add_action( 'admin_init', 'js_backbone_utilities_load' );
add_action( 'admin_enqueue_scripts', 'js_backbone_main_enqueue', 999 );

and in my plugin in need of the cmb2-checkbox-toggle script I’m hooking as usual

add_action( 'admin_init', 'my_plugin_scripts' );
function my_plugin_scripts(){
    add_action( 'admin_enqueue_scripts', 'q_admin_scripts');
}

function q_admin_scripts(){
    // hook with priority < 999
    wp_enqueue_script('cmb2-checkbox-toggle');
}

Hooking into JavaScript

With little modification the cmb2-checbox-toggle script will now hook in one of the JavaScript hooks to run allowing me a degree of certainety about what will run when

(($) ->

    hookBus = window.hookBus = window.hookBus || _.extend {},Backbone.Events

    class RowToggle extends Backbone.View
        @$row: null
        events:
            click: 'toggleTargetVisibility'
        showRow: =>
            @$row.show()
        hideRow: =>
            @$row.hide()
        toggleTargetVisibility: =>
            if @el.checked then @showRow() else @hideRow()
        initialize: ->
            target = @$el.data 'toggle-target'
            @$row = $('.cmb-row.cmb2-id-' + target)
            @toggleTargetVisibility

    # hook in the hookBus to init
    hookBus.on 'initUi:5', ->
        _.each $('input[type="checkbox"][data-toggle-target]'), (el) ->
            new RowToggle
                el: $(el)

)(jQuery)

An example use case of such a thing might be one where I need to set some default values and then have the visibility handling script run; I could pack the logic in a single, ad hoc, JavaScript file but I do not see the point in re-inventing the wheel every time. Plus this mechanism saves me the hassle of having to carefully plan JavaScript run times and printing: supposing the hook defining script will be queued last and any other JavaScript component will rely on it to run the order scripts are queued on the page is irrelevant.

GitHub

The refined version of the code is packed in the WP Script Utils plugin.