Templated Event Bindings

In “events” of a can.Component, is it possible to do the following:

"{getSomething} click": function(el, ev) { ..... }

Where “getSomething” is a property on the component’s viewModel, but it is a function that returns a string?

I made pull requests for the components event documentation. Maybe it will help you
https://github.com/pYr0x/canjs/blob/component-doc/component/events.md

I actually ended up answering my own question.

The way to do what I need is to use the define plugin.

Using the events object should almost always be avoided in modern CanJS. There’s almost no reason for it.

And yeah, the way to avoid it is the define plugin or functions combined with call expressions.

{{#each getPointsForPlayer(player.id)}}

Why is the “events” object so out of favor? How do you handle DOM manipulation? How about responding to events?

How do you handle DOM manipulation?

With live-binding.

Why is the “events” object so out of favor? How about responding to events?

It’s out of favor because you should be deriving the behavior of your application’s data (in nicely unit-testable view-models) and letting the view’s live-binding do its thing.

There are a few situations where events is needed, but I’d say 90% of the events object use I encounter is incorrect.

There are two types of events that people tend to be using “events” for:

  • data events like "{viewModel.stats} add" and
  • DOM events like "li click"

DOM events

In general, DOM events should be replaced by ($click)="method()" type view-bindings with the two possible exceptions:

  • Event delegation performance is needed.
  • The document or some other global element is needed to be listened to. (If this is your situation, I’d still take care that there’s not a better global state that might be more useful).

Data Events

This is the most common situation the use of events was incorrect. With the define plugin, data-based-diffing, call expressions, and view-model-event-bindings there’s little reason to be listening on data events yourself.

  • data based diffing - We will diff lists passed to #each in the view so even if entirely new lists are passed, we don’t replace all the elements.
  • call expressions - Allow calling methods like: {{#each getPointsForPlayer(player.id)}}
  • view-model-event-bindings - Allow you to listen to view model events in the view like (added-items)="method()"

Here’s an example, say I had a list of points by player like:

var points = new can.List( [{playerId: 1, value: 2}, {player2: value: 3}, ...])

And I wanted to show a summary of each players points:

Player1: 2, 2, 3, 3
Player2: 3, 3, 2
Player3: 2, 1, 1
...

In the past, I might setup listeners on "{points} add" and "{points} remove" to add and remove point values from some pointsSummary list of lists:

"{viewModel.points} add": function(points, ev, points, index){
  points.forEach((point)=>{
    this.viewModel.attr("pointsSummary")
        .attr(point.attr("playerId")).push(point.attr("value") )
  })
}

Instead, with the define plugin, I can create points summary map and a getPlayerPoints method like:

define: {
  pointsSummary: {
    get: function(){
      var playerSummary = {};
      this.attr("points").each((point) => {
        var playerId = point.attr("playerId"),
            players = playerMap[playerId]
        if(!players) {
          players = playerMap[playerId] = [];
        }
        players.push(point.attr("value"))
      });
      return playerSummary;
    }
  }
},
getPlayerPoints: function(playerId){
  return this.attr("pointsSummary").attr(playerId);
}

Now my template might look like:

{{#each playerIds}}
  Player{{.}}: 
  {{#each getPlayerPoints(.)}} {{.}} {{/each}}
{{/each}}

The benefits of all of this are:

  • .attr("pointsSummary") and getPlayerPoints are easily unit testable.
  • live-binding will automatically update the page if something is added to points and do it in an efficient way because of the data-based-diffing

Conclusion

The big idea here is that you shouldn’t have to do data transforms via bindings in the events object or anywhere else. Model your application’s data and its derivations using the define plugin / prototype methods. Then let the view intelligently update the DOM.

1 Like

Thanks for the detailed response. How greatly things have changed since the days of JavascriptMVC!

Here’s something that bothers me:

can.Component.extend({
	tag:"bit-panel",
	template: panelStache,
	scope: BitPanelVM,
	events: {
		inserted: function(){
			this.element.parent().scope().addPanel( this.scope );
		},
		removed: function(){
			this.element.parent().scope().removePanel( this.scope );
		}
	}
});

The following line:
this.element.parent().scope().addPanel( this.scope );

This is basically creating a tightly coupled relationship between a bit-panel component and a bit-tabs component. If a bit-panel exists and is not within a bit-tabs, this will error.

Is this type of relationship acceptable?

Yes, that’s fine. It’s an interface. As long as the parent provides an addPanel and removePanel method it could be anything.

It is possible to wire this up in the view like ($inserted)=“addParent(%viewModel)”, but I think that is overly complex.

Ok. Makes sense.

So you’d be against the model where bit-tabs looks at the DOM within itself to discover bit-panel elements?

So you’d be against the model where bit-tabs looks at the DOM within itself to discover bit-panel elements?

I’m not against that. It wouldn’t be easy to make work without MutationObservers. If you are in an environment that supports them, I think that’s a fine way of doing it.

If you cannot dynamically add new components at runtime, do you need MutationObservers?

MutationObservers aside, this process of looking at the DOM within itself would happen in bit-tabs’s “inserted” event? Would that be the correct place?

Stache will dynamically insert bit-panels.

The only way of knowing about them in bit-tabs would be with mutation observer.

You can’t use inserted because it doesn’t bubble.

I am guessing you are going to dislike this, but I want to get your thoughts anyway.

Given the following:


  
    CanJS provides the MV*
  
  
    StealJS provides the infrastructure.
  

In the “inserted” of bit-tabs, couldn’t I do the following to grab all panels?
this.element.find( “bit-panel” )

That wouldn’t work if panels are added or removed.

It’s interesting you mention the coupling of bit-tabs and bit-panel. There always has to be some known interface for them to communicate, but one of the nice things about the bit-tabs example is that neither the child nor parent know the tag name of the other, so you could in theory use them with other components.

The only requirements are the interfaces of the child and parent, and the direct parent-child DOM structure. In this case, the parent needs the addPanel and removePanel methods, and the child must be an observable with property active.

Sometimes you may want your components to be farther away. We can remove the DOM structure requirement by doing this.element.closest('bit-tabs') but that results in tighter coupling because the tag name of the parent must be known.

You can avoid that by communicating the insertion of child components with a custom event that sets up all the tear down functions initially, but it’s a little bit tricky. Maybe there’s a better way to do this, but here’s what I came up with:

can.Component.extend({
  tag: 'bit-tabs',
  viewModel: {/*...*/},
  events: {
    'bit-panel-inserted': function(el, ev) {
      var vm = this.viewModel,
          panelVM = ev.viewModel;
      
      vm.addPanel(panelVM);
      
      // parent hands child teardown function
      ev.makeTeardown(function() {
        vm.removePanel(panelVM);
      });

      // allow nested tabs
      ev.stopPropagation();
    }
  }
});

can.Component.extend({
  tag: 'bit-panel',
  viewModel: {/*...*/},
  events: {
    inserted: function() {
      var ctrl = this;
      
      this.element.trigger({
        type: 'bit-panel-inserted',
        viewModel: this.viewModel,

        // parent will call this to hand the child the teardown function
        makeTeardown: function(teardown) {
          // save teardown function on the events control
          ctrl.teardown = teardown;
        }
      });
    },
    removed: function() {
      var ctrl = this;
      ctrl.teardown();
    }
  }
});

Furthermore, you can be more explicit with the interface by not even passing the child viewModel at all. Instead, pass only the functions that the parent will need (e.g., makeActive and makeInactive) which will restrict the parent to only changing as much state as the child allows.

Yes, I agree. I said in an earlier comment that as long as sub components couldn’t be added or removed after initialization, then this approach “might” work.

The reason for this proposed approach is that I want the outer component (bit-tabs) to do some initialization before considering sub components (bit-panels).

Please note that I am not actually referring to bit-tabs and panels. Just using them as an example.

Dylan,
Events are a good idea. I’ve not thought of that.

In the past, keep walking up the parentNode’s can.viewModel() until I find the function I’m looking for.

I like this, but need think about it further…

@justinbmeyer so which approach do you favor?

this.element.parent().scope().addPanel( this.scope );

or dylan’s suggestion of triggering an event in the inserted event of component to allow parent components to listen for?

@thecountofzero I’ve never tried dylans, so I’m unsure. I wouldn’t compare it to:

this.element.parent().scope().addPanel( this.scope );

instead, I’d compare it to walking up the parent viewModels until you find one with an addPanel method. Lets say the comparison is between “walking” and “eventing”

Comparing them I see the following positives and negatives.

Eventing

Eventing is nice because you simply trigger the event and let jQuery’s event system do your walking for you. The only downside is that parents need to listen to this event, and then call their .addPanel method. Now there’s an events object in both places.

Walking

The walking code would be annoying to write.

Conclusion

In the end, I’d probably go the “eventing” approach. Less code is typically better. I’d just make sure there’s as little code in the events object as possible, so most code is in the unit-testable viewModels.