3
votes

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.

1
Have you tried using ng-model directly with your directive. You can follow some instructions here nadeau.tv/using-ngmodelcontroller-with-custom-directives - shams.kool
Can you make a plunker to demonstrate your problem? - gyc
Isn't this what "transclude: true" is for in the directive declaration? - datasedai
@gyc yep, edited the question. url is plnkr.co/edit/XjGuXSjWFEfG4eyZL6sR?p=preview - nephtes
@KoolShams Correct me if I'm wrong, but that article only describes the use of NgModelController for parsing form element values into data models and vice versa, that is to say formatting data models to present them as HTML, all of which occurs within a single scope. Can you explain how that applies to my problem, which is preserving a bidirectional model binding between scopes? - nephtes

1 Answers

2
votes

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>