Templated Event Bindings

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.