Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

define observe handlers in Ractive.extend() #405

Closed
WaltzingPenguin opened this issue Jan 22, 2014 · 8 comments
Closed

define observe handlers in Ractive.extend() #405

WaltzingPenguin opened this issue Jan 22, 2014 · 8 comments

Comments

@WaltzingPenguin
Copy link

Same idea as #360, just extended to include observe as well. i.e., I would like to be able to write:

Ractive.extend({
    "observe": {
        "foo": funcFoo,
        "bar": funcBar
    }
});

instead of

Ractive.extend({
    "init": function() {
        this.observe("foo", funcFoo);
        this.observe("bar", funcBar);
    }
});
@codler
Copy link
Member

codler commented Jan 23, 2014

+1

@Rich-Harris
Copy link
Member

+1 from me as well. Going to give it a post-0.4.0 milestone as I'm currently trying to wrangle the issue list down to the last few bugs standing in between us and the 0.4.0 release.

@Rich-Harris Rich-Harris added this to the post-0.4.0 milestone Mar 19, 2014
@briancray
Copy link

Lurker here, but wanted to quickly ask: Would this default to init: false or true? As an aside, I think the current default to true is unexpected. No other event system that I have used defaults to firing upon instantiation.

@browniefed
Copy link

@briancray default to init: true unless it was built something like the code below which would give you all the options. I think it could be implemented many ways but you should be able to apply all potential options as if you were calling this.observe. I agree it's slightly odd but it's not necessarily an event system like on/fire/off. You're observing data and asking when it changes. Well it's going from undefined => {}. So if you're asking the system to tell you when data changes then it should tell you every time it does, even initially. At least that's my justification I'm going to go out on a limb and say when @Rich-Harris was developing it that he wanted observe to always fire then someone didn't want that. So to prevent a breaking change threw on init: false option.

{
    "observe": {
        "foo": {fn: funcFoo, init: false},
        "bar": funcBar
    }
}

//this method allows you to bind to the same keypath but call multiple functions.
{
    "observe": [
        {fn: funcFoo, init: false, keypath: 'foo'},

        {fn: funcBar, init: true, keypath: 'bar'}
    ]
}

@Rich-Harris
Copy link
Member

@briancray I actually started implementing default observe handlers (along with default event handlers - #360) recently, and came to the view that it might not be as good an idea as it first seemed. The questions it raises about inheritance (from one component to another that extends from it) and so on get surprisingly hairy. The issue about init is a good indicator that there's a mismatch between concepts - a given instance has a list of observers and event handlers, not a map (despite the fact that there's a convenient ractive.observe(map) syntax) - for example you can have several observers observing the same keypath. Leaving aside the inheritance problems, we could tackle this by implementing something like @browniefed's second syntax suggestion, but at that point there's really no benefit over this.observe(...) inside an init() method.

In #360, there was some good discussion about whether event handlers on a child component should override handlers for the same event on a parent component, or whether you should be able to call _super inside a handler, or whether you could stop the propagation of an event by returning false (but then you have to have a clearly defined order in which the handlers fire...) and so on. The fact that there was no clear 'right' answer meant that whatever we ended up with would be a source of confusion.

So for all these reasons my enthusiasm for this and #360 have dampened over time.

As for the init: true default behaviour, @browniefed hits the nail on the head - it's not actually an event system, but a data flow system. The mechanism is the same, but the idea is subtly different - it's closer to the world of RxJS and Bacon.js than traditional pub/sub (which in my experience scales poorly), but without using Rx/Bacon primitives. It's designed to enable a form of reactive programming, where any given input state can lead to exactly one output state - to achieve that kind of idempotency the observer has to be called with the initial value.

With pub/sub based systems it's not uncommon to see stuff like this:

// set up some form of data-binding...
model.on( 'change:foo', function ( foo ) {
  view.renderFoo( foo );
});

// ...and initialise to the current value
view.renderFoo( model.get( 'foo' ) );

Observers avoid that kind of redundancy (and because of their clearer intent, it's easier to guard against things like circularity). But of course it's sometimes desirable to use observers in other ways, which is why the init option exists.

@briancray
Copy link

"It's designed to enable a form of reactive programming, where any given input state can lead to exactly one output state." Doesn't data binding achieve this alone? If pure data binding was enough, then observe wouldn't be necessary. It seems your example is an argument for data binding, not for the difference between observe and pub sub.

Correct me if I'm wrong but maybe I understand after I've thought about it a little more: You've designed observe with the intention that it alters a value as it flows from Ractive to template, not necessarily that it causes an event to happen? If that's true, I understand. However, that would feel more like the purpose of computed values.

If it is intended to function that way though, I'd recommend a change to your documentation at http://docs.ractivejs.org/latest/ractive-observe. Right now there isn't any mention of how it affects data flow (can you return a new newValue?). Additionally, your example uses observe as an event listener to trigger alert( 'item ' + index + ' status changed from ' + oldValue + ' to ' + newValue );. I understand that's to make it obvious what the callback parameters mean, but it gives the wrong impression as to the intention for observe. I could be wrong, but I think people will see at as a type of event listener until you clearly state that it's for hooking into data flow to change the output state.

@Rich-Harris
Copy link
Member

Thanks @briancray.

If pure data binding was enough

Yes, I agree. Unfortunately pure data binding isn't enough - there are occasions when you need to do something in a more imperative fashion, and ractive.observe() is the hook that allows you to do that without forgoing the benefits of reactivity. For example you might have a layout that depends on knowing the height of a list:

var ul = ractive.find( 'ul' );
ractive.observe( 'listItems', function () {
  this.set( 'listHeight', ul.offsetHeight );
}, { defer: true }); // defer until after DOM updates so offsetHeight is correct

Another example - you might have a visualisation with thousands of points. In that situation, data-binding might be a) unnecessary and b) prohibitively expensive, so you drop out of the abstraction and construct the DOM manually. But you still want the visualisation to respond to application state:

ractive.observe( 'selected', function ( newId, oldId ) {
  if ( oldId ) {
    $( '[data-id="' + oldId + '"]' ).removeClass( 'selected' );
  }

  if ( newId ) {
    $( '[data-id="' + newId + '"]' ).addClass( 'selected' );
  }
});

These are both forms of data binding, but ones that involve colouring outside the lines a bit. Speaking of 'outside the lines' there's a third possibility, which is that you have a Ractive component somewhere on the page (maybe a form component of some kind) and need to alter a different part of the page (which isn't controlled by Ractive, or belongs to a completely separate instance) reactively. In my experience the observe hook is the neatest and most bugproof way to achieve that.

You've designed observe with the intention that it alters a value as it flows from Ractive to template

Validation is one possible use, certainly, though not the original motivation. You have to call this.set() inside the observer for it to work; it's not ideal. I thought aloud here about maybe having a ractive.validate() method that uses the same mechanism but with a purpose-built API (i.e. return the value, no init or defer options).

There's definitely overlap in what observers and computed values can do, though computed values aren't ideal for validation since you have to create get() and set() methods for them to be writable. For calculating things like area from width and height they're great; for ensuring that width is a positive number, less so.

The comment about the documentation is fair, it could be explained better, particularly as the observe pattern isn't as widely used as traditional pub/sub. I'll raise an issue on the docs repo.

@briancray
Copy link

Thanks for the responses Rich. And don't get me wrong, Ractive's API is simple to understand and I like the focus on eliminating the unexpected. This was just one area that caused some confusion for me until I read the documentation a few times.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants