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

Allow components to fire events on their ancestors? #1099

Closed
wants to merge 1 commit into from

Conversation

evs-chris
Copy link
Contributor

This is more of a proposal for discussion than a pull request.

When deeply nesting components, it is possible to manually bubble events up the component stack by having a boilerplate event handler that fires an event. There is already a notation for referencing up-hierarchy for references, so does it make sense to do the same for events?

This is mostly useful for components that act as containers, and as such, have a content partial as part of their template.

Given the following component

var cmp = Ractive.extend({
  template: '<div>{{> content }}</div>'
});

this would allow you to turn

var ractive = new Ractive({
  components: { cmp: cmp },
  template: 'other stuff <cmp on-clicked="clicked"><button on-click="clicked">Click Me</button></cmp> more other stuff',
  el: '#main'
});

ractive.on('clicked', function(e) { console.log('clicked'); });

into the slightly more friendly

var ractive = new Ractive({
  components: { cmp: cmp },
  template: 'other stuff <cmp><button on-click="../clicked">Click Me</button></cmp> more other stuff',
  el: '#main'
});

ractive.on('clicked', function(e) { console.log('clicked'); });

Granted, this short example doesn't show off the benefit very well, but when you get three or four levels deep with multiple events, it really starts to clear some fog. I suppose it may improve performance slightly as well since it skips intermediate events, but that wasn't really an avenue I was trying to explore.

@evs-chris
Copy link
Contributor Author

I haven't verified yet, but this also retains the original event and context, whereas I don't think manually chaining does.

@martypdx
Copy link
Contributor

@evs-chris biggest issue I have with this is that it removes the normal dependency inversion that you get with pub-sub. Event publishers are explicitly coupled with the subscriber.

@evs-chris
Copy link
Contributor Author

I don't really have a problem with the explicit coupling in this case because I don't view the sub-components as independent pieces. I suppose it could be used in other cases that would be confusing though.

In #782, you suggested that namespaced events would address some component finding issues. That would also address this issue and be a more generally useful construct, I think. I don't think I would namespace by component though, as that could cause name conflicts. So how about if an event name is namespaced (one or more . in the name), then have fire walk up the component hierarchy to the first handler that is found for the event. That way, the publisher is decoupled from the subscriber, there aren't any intermediate proxys required, the original context isn't lost, and the use-case in #782 should be addressable by having the component fire events with a supplied namespace or a default.

var cmp = Ractive.extend({
  template: 'complex html with a <button on-click="someAction">that fires</button> some internal logic that fires a relevent event upward and a <button on-click="{{eventNamespace}}.bippy">that is set to to fire from the template</button>',
  init: function() {
    this.on('someAction', function() {
      if (relevant) this.fire((this.get('eventNamespace') || 'defaultNamespace') + '.bar');
    }
  }
});

Ractive.components.cmp = cmp;

var ractive = new Ractive({
  template: 'some html with <containerComponent><cmp eventNamespace="foo" /></containerComponent>'
});

ractive.on('foo.bar', function() { console.log('foo has barred'); });
ractive.on('foo.bippy', function() { console.log('foo has bippied'); });

The only caveat to this approach that I have found thus far is that changing eventNamespace after the component is rendered does not change the event name that is fired. What do you think?

@martypdx
Copy link
Contributor

@evs-chris:

I don't think I would namespace by component though, as that could cause name conflicts.

How so? I like the idea of being able to specify a namespace, but I was thinking the default would just be the name, because externally that's how the parent view already "knows" the component:

Ractive.components.widget = Widget
new Ractive({
  ...
  init: function(){
    this.on('widget.selected', function(){..})
  }
})

Adding eventNamespace would override the event prefix. It also means you could do something like:

{{#items:i}}<widget eventNamespace='{{"widget"+i}}'/>{{/}}

and thus only listen to events on say the first item.

I like being able to set it on the template, though should it be possible to set as an option during .extend({})?

And just to be clear, my thought was that the namespace would not be additive and would be consistent as the event bubbled, as opposed to changing with each layer as it bubbled. (i.e. no widget1.widget2.widget3.selected, and even if widget is three levels deep, it still bubbles as widget.selected is doesn't transform into widget3.selected)

As to context, I agree the original context should be preserved. I was wondering if there should be a contextStack that was a hashmap of the context of the components as it bubbled up:

this.on('widget.selected', function(e){
  e.context // event context from originating element
  e.contextStack.widget // context of widget as seen from its parent
  e.contextStack.widget1 // context of widget1 as seen from its parent
  e.contextStack.widget2 // context of widget2 as seen from its parent
})

Sometimes you want the component context and you don't care about the specifics of the event implementation context. Maybe this is overkill, or better solved by improving component events.

init: function() {
this.on('someAction', function() {
if (relevant) this.fire((this.get('eventNamespace') || 'defaultNamespace') + '.bar');
}

Not entirely sure what you meant here. Are you looking for a cancel bubble type funtionality?

@evs-chris
Copy link
Contributor Author

@martypdx

How so? I like the idea of being able to specify a namespace, but I was thinking the default would just be the name, because externally that's how the parent view already "knows" the component:

I was looking at this as entirely opt-in where namespaces are never automatically specified. Either the component-maker has arranged for namespaced events and has a way to modify them i.e. with a data element or extends option that they process in beforeInit. Or the component user is building a deeply nested template and wants to be able to address individual sub-components at the common and always-available top level.

That last part is where I've had some trouble. Having nested child components that blink in and out of existence with state changes can make using on in code challenging with a big side of boilerplate.

Also, if namespaces were automatically assigned, how would parents proxy them in the template? I don't think <foo on-widget.selected="..." /> would work out?

And just to be clear, my thought was that the namespace would not be additive and would be consistent as the event bubbled, as opposed to changing with each layer as it bubbled. (i.e. no widget1.widget2.widget3.selected, and even if widget is three levels deep, it still bubbles as widget.selected is doesn't transform into widget3.selected)

As to context, I agree the original context should be preserved. I was wondering if there should be a contextStack that was a hashmap of the context of the components as it bubbled up:

I wouldn't really consider additive or automatically nesting namespaces to be worth their complexity and overhead. At least, not for my use-cases. The same goes for bubbling beyond the first layer that can handle the event. I would make further bubbling opt-in only as well if at all, perhaps with an additional sigil like @ or % prefixed on the name. Bubbling might get confusing around the recently added jQuery-like bubble handling, though.

As far as tracking context stacks, the code I have already written is only concerned with the source component and the component with the handler (parent and child, the same as the code attached to this request). I haven't delved deeply enough into ractive to figure out how to build a context stack, but I am only interested in the components on either end of the event at this point.

Not entirely sure what you meant here. Are you looking for a cancel bubble type funtionality?

No, that was a poor example meant to indicate a number of simple components that work together inside a conglomerate parent, which fires events upward when an appropriate state has been reached.

@codler
Copy link
Member

codler commented Aug 13, 2014

As long as it will still work and doesnt break to create a "Tab UI"-component. Discussed a little #879 (comment)

@evs-chris
Copy link
Contributor Author

As long as it will still work and doesnt break to create a "Tab UI"-component. Discussed a little #879 (comment)

That's actually my primary use-case at this point. I have a form builder that can spit out nested tab boxes and binding events deeper into the component hierarchy is tedious.

@evs-chris
Copy link
Contributor Author

On further examination, my version of namespaced events is untenable since they can't be addressed in templates and there isn't a simple way to supply a namespace outside of direct use within a template. I think bubbling #1116 would be a better solution.

@evs-chris evs-chris closed this Aug 15, 2014
@fskreuz fskreuz mentioned this pull request Aug 15, 2014
@evs-chris evs-chris deleted the ancestor-events branch September 19, 2014 01:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants