Templated Event Bindings

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.

I was just about to offer my thoughts and they were very similar. With “eventing” the parent now has to listen for the event. But I think I like that. This allows parent components to subscribe to “inserted type” events of the subcomponents that it cares about. This does create a coupling in the fact that unless bit-tabs is listening for bit-panel’s inserted event, it won’t know anything about them. However, on the flip side, calling “addPanel” from bit-panel requires bit-panel to know that there is (or should be) a parent component that exposes the “addPanel” interface. Either way, one component needs to be aware in some sense the other should be out there.

“addPanel” would still be on the viewModel keeping it easy to test. The only part in the “events” object is listening for the event and calling addPanel.

Here’s a quick example of walking the parent viewModels:

var findParentViewModelWithFn = function(el, fnName) {
    while(el.parent().length) {
        if(el.parent().viewModel && el.parent().viewModel()[fnName]) {
            return el.parent().viewModel();
        }
        el = el.parent();
    }
    return undefined;
};

On the subject of coupling, in the bit-tabs example and probably most others, the parent needs to know about the children but not the other way around. In this case, the parent is modifying the child’s state, so having knowledge of its interface is inevitable. What we can prevent is the child knowing about the parent.

With eventing, the child simply declares its interface and the parent merely needs to subscribe to it. You could say the same is happening with scope walking if you consider "has an ancestor with a method named addPanel" to be part of the child interface, and adding an addPanel method would be how you subscribe to the child, but that feels inverted to me.

One concern I have about scope walking is that you could find the wrong viewmodel with the right method name, especially considering that all viewmodel members are basically public which increases the opportunity for this. I think the odds of this happening would be substantially lower with eventing so you don’t have to make crazy unique method names to avoid conflict.

More thoughts…

One of the things I am also doing differently in some of my components that still interface directly with their ancestor viemodels, is that I decrease the child’s knowledge of the parent by having the initial registration method (e.g., addPanel) return the teardown function (e.g., bound removePanel) so the child only needs to know one method name.

Also, I mentioned this a bit previously, but I’ve started to move away from passing the child viewmodel. Instead, I try to pass an object containing just what the parent needs. With bit-tabs, you could simply pass this.viewModel.compute('active') as the entire child interface:

inserted: function() {
  var ctrl = this;
  this.element.trigger({
    type: 'bit-panel-inserted',
    panelApi: this.viewModel.compute('active'),
    makeTeardown: function(teardown) {
        ctrl.teardown = teardown;
    }
  });
}

Bit-tabs could then save panelApi from each event in the panels array and call those computes with true or false to change child states. You could also pass an entire object of methods and computes to save instead.

I’m ok with sending the entire child’s viewModel. It’s just a reference anyway, right? The parent only need worry about that parts of said viewModel that it cares about. However… in the absence of private properties on a viewModel, I think you suggestion might not be a bad one.

With eventing, you only have to worry about a parent conflict if there are more than one parent that is listening for that specific sub-component-inserted event. This seems much less likely to occur than for their to be a parent viewModel that exposes an “addPanel” method. Both are rather unlikely.

Good information thanks for sharing
VMware Engineer