mikeric / rivets

Lightweight and powerful data binding.
http://rivetsjs.com
MIT License
3.23k stars 310 forks source link

Create support for callback when a model has been bound #337

Open roryprimrose opened 10 years ago

roryprimrose commented 10 years ago

I want to be able to fire off some code at the point that an element has been created and added to the DOM either because a model has been bound or because a model has been changed that results in a DOM change.

The specific example here is I have a form that can exist multiple times because of binding to a collection. Each form instance needs to be configured with bootstrapValidator.js when it is created and added to the DOM. There does not appear to be any hooks currently in rivets to make this happen.

I'm thinking that something like rv-on-bound='callback' would be handy for this type of scenario. Other possible naming could be rv-on-bind or rv-on-init. Perhaps even drop the -on.

symeonb commented 10 years ago

+1 on this idea

brunoorsolon commented 10 years ago

+1 for this

jbrown0824 commented 10 years ago

+1 as well

singggum3b commented 10 years ago

+1 for this

vvo commented 10 years ago

Needing this for another usecase: I use JWPlayer and it will fail if the DOM node is detached.

jhnns commented 9 years ago

I don't think that this callback should be notated in HTML. Aren't you able to handle this event in your application code? You should know when a) a rivets view is created and b) when a model has changed.

symeonb commented 9 years ago

In theory jhnns you do, but you dont know when rivets has finished doing its stuff.

jbrown0824 commented 9 years ago

Exactly what symeonb said. I know when the model updated, but I don't know when rivets has completed re-rendering the view. it takes upwards of hundreds of milliseconds for it to render. I can't apply event listeners until AFTER the render has completed, not simply after the model has updated.

jhnns commented 9 years ago

But that would mean that rivets is somehow "batching" the rendering tasks. I don't know the source code exactly but afaik the rendering should be done by setTimeout(fn, 0) (which doesn't mean that the rendering takes 0ms of course :wink:)

symeonb commented 9 years ago

So solution to this is myview = rivets.Bind(el,data); setTimeout(applyMylistenersFunction,0);

then the function to apply the listeners is attached to the end of the queue

roryprimrose commented 9 years ago

Setting timeouts is a fragile hack IMHO. Rivets is responsible for updating the view which is ultimately the browser DOM. Some scripts can only be applied once the DOM has been changed which as jbrown0824 indicates is not the same time as when the model was updated.

Callback support in this regard will provide the facility to execute a function after the DOM changes have been completed due to a change to the model. The beauty of this implementation is that the callback would be scoped to the DOM element that was impacted by the model change scope.

roryprimrose commented 9 years ago

I have tried a few workarounds to solve this issue.

var originalValueRoutine = rivets.binders.value.routine;

rivets.binders.value.routine = function (el, value) {
    var result = originalValueRoutine(el, value);

    // Raise a binding changed event
    $(el).trigger("bindingchange");

    return result;
};

There is an assumption that the DOM changes have completed synchronously by the original binder function. There is also a major restriction that this only affects the value binder, not all the other binders.

Another attempt I've tried is using a formatter as a trigger point.

rivets.formatters.setHeight = function () {
    resizeElements();

    setTimeout(resizeElements, 100);

    return '';
}

The view binding can then use this at the bottom of the html template for the model.

{ model | setHeight }

This has been ok but ended up needing a timeout callback anyway because it didn't always work.

This is all hacky and a solution baked into Rivets would solve these issues.

brunoorsolon commented 9 years ago

I got it to work by doing a nasty trick: I binded rivets to a input hidden that is on the very end of the DOM and started a setInterval to check that input value every 100ms and after it has value, execute my script and clear the interval. It looks ugly, but worked in my scenario.

symeonb commented 9 years ago

setTimeout(f,0) is quite a necessary step on many occasions, certainly not a hack. There is a great description from DVK here http://stackoverflow.com/questions/779379/why-is-settimeoutfn-0-sometimes-useful (Note the accepted answer is not the description by DVK) However, yes, a callback would be nice :)

jhnns commented 9 years ago

@symeonb yes, setTimeout(f,0) is less hacky as most people think. If you know how the browser works it's pretty much predictable.

But you got me wrong guys. All what I was trying to say is that rivets doesn't change the DOM asynchronously. If you bind a new model, you can instantly query the DOM for layout changes:

console.log(el.offsetHeight);
rivets.bind(el, model);
console.log(el.offsetHeight);

The only problem is that when a model changes you usually can't predict in which order the event listeners will fire. If you bound rivets before your own, it's pretty much likely that rivets will apply its changes first but that depends on the implementation of the event emitter. So in practice I see a use-case for this kind of callback, but only because the order of event listeners is unknown.

symeonb commented 9 years ago

Yes jhnns - i have just taken a look at the rivets code (dont know why i did not do that before ;) ) and it is not asynchronous, so it is just event listener order that is the issue. Phew

sebastianconcept commented 9 years ago

+1 Sounds handy!

roryprimrose commented 9 years ago

Hey @mikeric I'm keen to get your feedback on this one.

jhnns commented 9 years ago

When does rv-on-enter fire? When the DOM element is created? Or when the DOM element is in the document? And when does rv-on-exit fire?

mikeric commented 9 years ago

The specific example here is I have a form that can exist multiple times because of binding to a collection. Each form instance needs to be configured with bootstrapValidator.js when it is created and added to the DOM. There does not appear to be any hooks currently in rivets to make this happen.

@roryprimrose Is there a reason why you can't hook into this within the bind and unbind functions of your binder? (whatever binder is initializing bootstrapValidator on your form). That's what they're there for.

I'm not sure why you would want an arbitrary rv-on-bound on the entire element for this (not specific to a certain binder), but maybe I'm misunderstanding the use-case here?