Skip to content

Instantly share code, notes, and snippets.

@sporto
Last active November 17, 2024 14:52
Show Gist options
  • Save sporto/7e4c9ef9dc352d1389a8 to your computer and use it in GitHub Desktop.
Save sporto/7e4c9ef9dc352d1389a8 to your computer and use it in GitHub Desktop.
CanJS: Communication between components

Introduction

The intention of this document is to explore patterns of communication between can components and discuss possible improvements and newer patterns.

We should aim for:

  • Making common scenarios easier
  • Establishing best practices and canonical patterns
  • Having as little surprise as possible, it should be clear from looking at the code exactly what the intention is
  • Aligning with future standards, specifically try to align with web components as much as possible
  • Avoiding tight coupling between components, they should know as little as possible about their collaborators
  • Making components easy to test, trying to keep the scope oblivious of the DOM

Communication from parent to children

Use case: We want to change the state on a sub component

This is currently supported by using view binding e.g.

<client-editor client="{client}" />

Use case: We want to send a message to a child component

Binding works well and it is easy to understand, but there are many situation where we have nothing to pass to the subcomponent. For example we just want to tell our sub-compontent to do something e.g. refresh.

DOM methods

One possible way to do this is attaching methods to the sub-component DOM.

In the sub-component:

// when the component is inserted
// we attache a method to its DOM element
 events: {
    inserted: function ($ele) {
      var comp = this;
      $ele.get(0).refresh = function (args) {
       	// ... do something with comp
      }
    }
  }

In the parent:

// when we want to send a message to the child component
// we find its element and then call the attached method
events: {
    '._btn_refresh click': function () {
  	  	var $comp = $('clients-chooser', this.element);
      	$comp.get(0).refresh(args);
    }
  }

[http://jsbin.com/hodosa/2/edit]

Possible improvements

It would be nice to have an streamlined way to create these methods on the DOM elements e.g.

  can.Component.extend({
  	scope: {},
  	events: {},
  	domMethods: {
  		refresh: function (args) {
  			// ...
  		}
  	}
  });

Also, at the moment it is possible to get a reference to the scope on a component e.g.

 $('child-component').scope();

What if we could get a reference to the component object and just call a method on that?

 $('child-component').component().refresh(arg);

This would me totally unecessary the need to attach methods to the DOM.

Use case: We want to broadcast a message to all children

Calling a method on a child components is nice we have exactly one sub component we want to target. But what if there are many or they are nested? One possible way is to be able to broadcast an event to all sub elements.

Not sure if this is possible at the moment.

Communication from child to parent

Use case: We want to change the state on the parent controller

E.g We have a datepicker sub-component.

Using model bindings is perfect for this. We can simply pass the model we want to change and allow the sub-component to change it directly.

<date-picker data="{booking.startsAt}" />

<date-picker data="{startsAt}" />

Use case: We want to send a message back to the parent

Again, there are many times when we don't have a model to bind, we just want to get a result back from the sub-component.

Triggering DOM events

This is a well understood way of doing things. e.g.

In children component:

 events: {
    'a click': function ($el, event) {
      var val = $el.html();
      // we simply trigger a DOM event with the value we want
      this.element.trigger('onSelected', val);
      return false;
    }
  }

In parent component:

   events: {
    'clients-list onSelected': function (ele, event, val) {
	    // we receive the event using a normal listener
	    // and do something with it
     	this.scope.attr('choosen', val);
    }
  }

[http://jsbin.com/nuzew/1/edit]

There are a couple of issues with this approach:

  • It is easy to introduce bugs if we change the name of the events in one place and not in the other. Is it possible to use constants?
  • Events bubble all the way up, so potentially being catched by more things that what we want. We have to be careful with the naming of events or stop propagation.

Using a callback

A very well understood pattern in JS is to pass callbacks to objects, broadly used in jquery and jquery plugins. It would be nice to support something like this in can component.

e.g.

<clients-list on-selected="{onSelectedCallback}" />

At the moment this doesn't work because can cannot distinguish what is the intention here, e.g. to pass a callback or resolve the function.

A possible solution would be to introduce a stache convention for this:

<clients-list on-selected="{*onChosen}" />

Using can-event

Another possible way is to use can-event at the moment to replicate the callback approach.

<clients-list can-selected="onChosen" />

This looks a lot like the callback example above but note the can- prefix.

In the child component:

  events: {
    'a click': function ($ele, event) {
      var val = $ele.html();
	      // ATM crafting an event object is necessary
			$(this.element).trigger({
            type: 'selected',
            val: val
          });
          // BUT it would be nice to simply do this:
          $(this.element).trigger('selected', val);
    }
  }

In the parent component:

scope: {
    chosen: null,
    onChosen: function(context, elm, event) {
	    // ATM we need to extra the argument from the event object
      this.attr('chosen', event.val );
    }
  }

// BUT it would be nice to simply do this:

scope: {
    chosen: null,
    onChosen: function(context, elm, event, val) {
      this.attr('chosen', val);
    }
  }

[http://jsbin.com/koxobamo/22/edit]

Calling a method on the parent scope

Note that there it is possible to call a method directly on the parent scope. but this should be avoided.

Use case: We want to broadcast an event to the outside world

E.g. a child component broadcast that the current user name has been changed.

This is well supported by Triggering DOM events as explained above.

Communication between sibling components

Use case: We want to change the state on a sibling component

This is well supported by using model bindings, i.e. binging the same model in both components:

<booking-editor>
	<client-editor client="{client}" />
	<client-finder current-client="{client}" />
</booking-editor>

Use case: We want to pass events between sibling components without model bindings

E.g. we want to tell all sibling components that the current client has changed.

Probably a combination of two previous patterns is the best for doing this:

  • First broadcast the change from one children initiating the change to the parent
  • Then broadcast the change from the parent to all the rest of the children

This one is interesting as it might potentially create an infinite loop of events.

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