Search code examples
angularjsangularjs-directiveangularjs-scope

In a reusable component Angular directive which wraps another directive, how can I make a model "pass through"?


In Angular, I'm creating a directive for a reusable component that wraps ui-select (to automate integration with REST services). My directive will be invoked roughly like this:

<rest-backed-selector selected-model="vm.selection"
                      service="abp.services.app.someservice"
                      on-select="vm.onSelect()">

In accordance with best practices for reusable components, this directive will isolate its scope (I'm omitting ancillary stuff like templateUrl for clarity):

    app.directive(
      'restBackedSelector',
      [ function () {
          return {
            scope: {
              selectedModel: '=',
              service: '@',
              onSelect: '&'
            }
          };
      ]);

Now here's the problem: $scope.selectedModel needs to be passed, in turn, to ui-select via the template:

<ui-select ng-model="selectedModel" ...>

This won't work because passing a model from the top level of $scope will break the binding when the ui-select controller changes its value, due to that well-known gotcha of Angular scope inheritance.

What's the recommended way of working around this?

Here's a demonstration of the problem: http://plnkr.co/edit/XjGuXSjWFEfG4eyZL6sR?p=preview

Changes made by selecting an item in the dropdown are not reflected in the scope of the directive nor the top-level app controller. One partial workaround is to uncomment paged-select-box.js line 26, which will explicitly update the outer scopes by handling the on-select event. However, even then, changes originating in the outer scopes (such as hitting the reset button) won't be reflected in the ui-select scope.


Solution

  • When you have a hierarchy of directives that inherit properties (or pass down properties) your first reflex should be to not use a scope property but a bindToController property.

    The benefits are:

    • You don't need to worry about isolated scopes.
    • You're following best modern practice
    • Attributes are automatically bound to the directive's controller so you get a clean object dot notation.
    • The pain you were having with scope magically goes away

    From the plunker:

    controllerAs: 'vm',
    scope: {},
    bindToController: {
        selection: '=',
        requestFormat: '&',
        itemFormat: '&'
    }
    

    And with controllerAs the template needs to follow:

    inner selection: {{ vm.selection.full_name }}
    
    <ui-select ng-model="vm.selection"
               on-select="vm.onSelect($item)">
        <ui-select-match placeholder="Enter search term">{{ vm.itemFormat({ item: $select.selected }) }}</ui-select-match>
        <ui-select-choices repeat="item in vm.items"
                           refresh="vm.requestFirstPage($select.search)">
            <span ng-bind-html="vm.itemFormat({ item: item })"></span>
        </ui-select-choices>
    </ui-select>