Update widget form after drag-and-drop (WP save bug)

I’ve posted a bug-report about this a few months ago (on WordPress trac (Widget Instance Form Update Bug)) and I thought I’d try writing about it here too. Maybe someone has a better solution to this issue than me.

Basically the problem is that if you drop a widget into a sidebar, the widget form doesn’t get updated until you manually press save (or reload the page).

Read More

This makes unusable all the code from the form() function that relies on the widget instance ID to do something (until you press the save button). Any stuff like ajax requests, jQuery things like colorpickers and so on will not work right away, because from that function it would appear that the widget instance has not yet been initialized.

A dirty fix would be to trigger the save button automatically using something like livequery:

$("#widgets-right .needfix").livequery(function(){
  var widget = $(this).closest('div.widget');
  wpWidgets.save(widget, 0, 1, 0);
  return false;
});

and add the .needfix class in form() if the widget instance doesn’t look initialized:

 <div <?php if(!is_numeric($this->number)): ?>class="needfix"<?php endif; ?>
   ...
 </div>

One drawback of this solution is that if you have lots of widgets registered, the browser will eat lots of CPU, because livequery checks for DOM changes every second (tho I didn’t specifically test this, it’s just my assumption 🙂

Any suggestions for a better way to fix the bug?

Related posts

Leave a Reply

4 comments

  1. I did battle with a similar situation recently. Ajax in widgets is no joke! Need to write some pretty crazy code to get things to work across instances. I’m not familiar with live query, but if you say it checks the DOM every second, I might have a less intense solution for you:

    var get_widget_id = function ( selector ) {
        var selector, widget_id = false;
        var id_attr = $( selector ).closest( 'form' ).find( 'input[name="widget-id"]' ).val();
        if ( typeof( id_attr ) != 'undefined' ) {
            var parts = id_attr.split( '-' );
            widget_id = parts[parts.length-1];
        }
        return parseInt( widget_id );
    };
    

    You can pass this function a selector or jQuery object and it will return the instance ID of the current instance. I could find no other way around this issue. Glad to hear I’m not the only one 🙂

  2. I don’t like answering my own question, but I feel this is the best solution so far:

    $('#widgets-right').ajaxComplete(function(event, XMLHttpRequest, ajaxOptions){
    
      // determine which ajax request is this (we're after "save-widget")
      var request = {}, pairs = ajaxOptions.data.split('&'), i, split, widget;
    
      for(i in pairs){
        split = pairs[i].split('=');
        request[decodeURIComponent(split[0])] = decodeURIComponent(split[1]);
      }
    
      // only proceed if this was a widget-save request
      if(request.action && (request.action === 'save-widget')){
    
        // locate the widget block
        widget = $('input.widget-id[value="' + request['widget-id'] + '"]').parents('.widget');
    
        // trigger manual save, if this was the save request 
        // and if we didn't get the form html response (the wp bug)
        if(!XMLHttpRequest.responseText)
          wpWidgets.save(widget, 0, 1, 0);
    
        // we got an response, this could be either our request above,
        // or a correct widget-save call, so fire an event on which we can hook our js
        else
          $(document).trigger('saved_widget', widget);
    
      }
    
    });
    

    This will fire the widget-save ajax request, just after a widget-save request has completed (if there was no response with the form html).

    It needs to be added in the jQuery(document).ready() function.

    Now, if you want to easily re-attach your javascript functions to the new DOM elements added by the widget form function simply bind them to the “saved_widget” event:

    $(document).bind('saved_widget', function(event, widget){
      // For example: $(widget).colorpicker() ....
    });
    
  3. Ran into this recently and it seems that in the traditional “widgets.php” interface any javascript initialization should be run directly for existing widgets (those in the #widgets-right div), and indirectly via the widget-added event for newly added widgets; whereas in the customizer “customize.php” interface all widgets – existing and new – are sent the widget-added event so can just be initialized there. Based on this the following is an extension of the WP_Widget class which makes it easy to add javascript initialization to a widget’s form by overriding one function, form_javascript_init():

    class WPSE_JS_Widget extends WP_Widget { // For widgets using javascript in form().
        var $js_ns = 'wpse'; // Javscript namespace.
        var $js_init_func = ''; // Name of javascript init function to call. Initialized in constructor.
        var $is_customizer = false; // Whether in customizer or not. Set on 'load-customize.php' action (if any).
    
        public function __construct( $id_base, $name, $widget_options = array(), $control_options = array(), $js_ns = '' ) {
            parent::__construct( $id_base, $name, $widget_options, $control_options );
            if ( $js_ns ) {
                $this->js_ns = $js_ns;
            }
            $this->js_init_func = $this->js_ns . '.' . $this->id_base . '_init';
            add_action( 'load-widgets.php', array( $this, 'load_widgets_php' ) );
            add_action( 'load-customize.php', array( $this, 'load_customize_php' ) );
        }
    
        // Called on 'load-widgets.php' action added in constructor.
        public function load_widgets_php() {
            add_action( 'in_widget_form', array( $this, 'form_maybe_call_javascript_init' ) );
            add_action( 'admin_print_scripts', array( $this, 'admin_print_scripts' ), PHP_INT_MAX );
        }
    
        // Called on 'load-customize.php' action added in constructor.
        public function load_customize_php() {
            $this->is_customizer = true;
            // Don't add 'in_widget_form' action as customizer sends 'widget-added' event to existing widgets too.
            add_action( 'admin_print_scripts', array( $this, 'admin_print_scripts' ), PHP_INT_MAX );
        }
    
        // Form javascript initialization code here. "widget" and "widget_id" available.
        public function form_javascript_init() {
        }
    
        // Called on 'in_widget_form' action (ie directly after form()) when in traditional widgets interface.
        // Run init directly unless we're newly added.
        public function form_maybe_call_javascript_init( $callee_this ) {
            if ( $this === $callee_this && '__i__' !== $this->number ) {
                ?>
                <script type="text/javascript">
                jQuery(function ($) {
                    <?php echo $this->js_init_func; ?>(null, $('#widgets-right [id$="<?php echo $this->id; ?>"]'));
                });
                </script>
                <?php
            }
        }
    
        // Called on 'admin_print_scripts' action added in constructor.
        public function admin_print_scripts() {
            ?>
            <script type="text/javascript">
            var <?php echo $this->js_ns; ?> = <?php echo $this->js_ns; ?> || {}; // Our namespace.
            jQuery(function ($) {
                <?php echo $this->js_init_func; ?> = function (e, widget) {
                    var widget_id = widget.attr('id');
                    if (widget_id.search(/^widget-[0-9]+_<?php echo $this->id_base; ?>-[0-9]+$/) === -1) { // Check it's our widget.
                        return;
                    }
                    <?php $this->form_javascript_init(); ?>
                };
                $(document).on('widget-added', <?php echo $this->js_init_func; ?>); // Call init on widget add.
            });
            </script>
            <?php
        }
    }
    

    An example test widget using this:

    class WPSE_Test_Widget extends WPSE_JS_Widget {
        var $defaults; // Form defaults. Initialized in constructor.
    
        function __construct() {
            parent::__construct( 'wpse_test_widget', __( 'WPSE: Test Widget' ), array( 'description' => __( 'Test init of javascript.' ) ) );
            $this->defaults = array(
                'one' => false,
                'two' => false,
                'color' => '#123456',
            );
            add_action( 'admin_enqueue_scripts', function ( $hook_suffix ) {
                if ( ! in_array( $hook_suffix, array( 'widgets.php', 'customize.php' ) ) ) return;
                wp_enqueue_script( 'wp-color-picker' ); wp_enqueue_style( 'wp-color-picker' );
            } );
        }
    
        function widget( $args, $instance ) {
            extract( $args );
            extract( wp_parse_args( $instance, $this->defaults ) );
    
            echo $before_widget, '<p style="color:', $color, ';">', $two ? 'Two' : ( $one ? 'One' : 'None' ), '</p>', $after_widget;
        }
    
        function update( $new_instance, $old_instance ) {
            $new_instance['one'] = isset( $new_instance['one'] ) ? 1 : 0;
            $new_instance['two'] = isset( $new_instance['two'] ) ? 1 : 0;
            return $new_instance;
        }
    
        function form( $instance ) {
            extract( wp_parse_args( $instance, $this->defaults ) );
            ?>
            <div class="wpse_test">
                <p class="one">
                    <input class="checkbox" type="checkbox" <?php checked( $one ); disabled( $two ); ?> id="<?php echo $this->get_field_id( 'one' ); ?>" name="<?php echo $this->get_field_name( 'one' ); ?>" />
                    <label for="<?php echo $this->get_field_id( 'one' ); ?>"><?php _e( 'One?' ); ?></label>
                </p>
                <p class="two">
                    <input class="checkbox" type="checkbox" <?php checked( $two ); disabled( $one ); ?> id="<?php echo $this->get_field_id( 'two' ); ?>" name="<?php echo $this->get_field_name( 'two' ); ?>" />
                    <label for="<?php echo $this->get_field_id( 'two' ); ?>"><?php _e( 'Two?' ); ?></label>
                </p>
                <p class="color">
                    <input type="text" value="<?php echo htmlspecialchars( $color ); ?>" id="<?php echo $this->get_field_id( 'color' ); ?>" name="<?php echo $this->get_field_name( 'color' ); ?>" />
                </p>
            </div>
            <?php
        }
    
        // Form javascript initialization code here. "widget" and "widget_id" available.
        function form_javascript_init() {
            ?>
                $('.one input', widget).change(function (event) { $('.two input', widget).prop('disabled', this.checked); });
                $('.two input', widget).change(function (event) { $('.one input', widget).prop('disabled', this.checked); });
                $('.color input', widget).wpColorPicker({
                    <?php if ( $this->is_customizer ) ?> change: _.throttle( function () { $(this).trigger('change'); }, 1000, {leading: false} )
                });
            <?php
        }
    }
    
    add_action( 'widgets_init', function () {
        register_widget( 'WPSE_Test_Widget' );
    } );
    
  4. I think something exists in WordPress 3.9 that might help you. It’s the widget-updated callback. Use it like this (coffeescript):

    $(document).on 'widget-updated', (event, widget) ->
        doWhatINeed() if widget[0].id.match(/my_widget_name/)