Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What is the preferred pattern for re-binding jQuery-style UI interfaces after AJAX load?

This always gets me. After initializing all lovely UI elements on a web page, I load some content in (either into a modal or tabs for example) and the newly loaded content does not have the UI elements initialized. eg:

$('a.button').button(); // jquery ui button as an example
$('select').chosen(); // chosen ui as another example
$('#content').load('/uri'); // content is not styled :(

My current approach is to create a registry of elements that need binding:

var uiRegistry = {
  registry: [],
  push: function (func) { this.registry.push(func) },
  apply: function (scope) {
    $.each(uiRegistry.registry, function (i, func) {
      func(scope);
    });
  }
};

uiRegistry.push(function (scope) {
  $('a.button', scope).button();
  $('select', scope).chosen();
});

uiRegistry.apply('body'); // content gets styled as per usual

$('#content').load('/uri', function () {
  uiRegistry.apply($(this)); // content gets styled :)
});

I can't be the only person with this problem, so are there any better patterns for doing this?

like image 285
stephenfrank Avatar asked Feb 25 '13 18:02

stephenfrank


1 Answers

My answer is basically the same as the one you outline, but I use jquery events to trigger the setup code. I call it the "moddom" event.

When I load the new content, I trigger my event on the parent:

parent.append(newcode).trigger('moddom');

In the widget, I look for that event:

$.on('moddom', function(ev) {
  $(ev.target).find('.myselector')
})

This is oversimplified to illustrate the event method.

In reality, I wrap it in a function domInit, which takes a selector and a callback argument. It calls the callback whenever a new element that matches the selector is found - with a jquery element as the first argument.

So in my widget code, I can do this:

domInit('.myselector', function(myelement) {
  myelement.css('color', 'blue');
})

domInit sets data on the element in question "domInit" which is a registry of the functions that have already been applied.

My full domInit function:

window.domInit = function(select, once, callback) {
  var apply, done;
  done = false;
  apply = function() {
    var applied, el;
    el = $(this);
    if (once && !done) {
      done = true;
    }
    applied = el.data('domInit') || {};
    if (applied[callback]) {
      return;
    }
    applied[callback] = true;
    el.data('domInit', applied);
    callback(el);
  };
  $(select).each(apply);
  $(document).on('moddom', function(ev) {
    if (done) {
      return;
    }
    $(ev.target).find(select).each(apply);
  });
};

Now we just have to remember to trigger the 'moddom' event whenever we make dom changes.

You could simplify this if you don't need the "once" functionality, which is a pretty rare edge case. It calls the callback only once. For example if you are going to do something global when any element that matches is found - but it only needs to happen once. Simplified without done parameter:

window.domInit = function(select, callback) {
  var apply;
  apply = function() {
    var applied, el;
    el = $(this);
    applied = el.data('domInit') || {};
    if (applied[callback]) {
      return;
    }
    applied[callback] = true;
    el.data('domInit', applied);
    callback(el);
  };
  $(select).each(apply);
  $(document).on('moddom', function(ev) {
    $(ev.target).find(select).each(apply);
  });
};

It seems to me browsers should have a way to receive a callback when the dom changes, but I have never heard of such a thing.

like image 150
Julian Avatar answered Sep 18 '22 18:09

Julian