angularjs pass ngModel from wrapper directive to wrapped directive

All we need is an easy explanation of the problem, so here it is.

I’m new to Angular and still painfully wrapping my head around custom directives.

I’d like to reuse this bit of HTML

<ui-select ng-model="model.selectedLanguages" multiple search-enabled="true" theme="select2" style="width: 300px;">
    <ui-select-match placeholder="Pick one...">{{$item.name}}</ui-select-match>
    <ui-select-choices repeat="lang.id as lang in langs |filter: { name : $select.search }">
        <div ng-bind-html="lang.name | highlight: $select.search" ></div> 
    </ui-select-choices>
</ui-select>

by wrapping it into my custom directive:

<language-picker ng-model="model.selectedLanguages"/>

something like this:

app.directive('languagePicker', function() {
            return {
                template : '<ui-select ng-model="**PARENT'S NGMODEL**" multiple search-enabled="true" theme="select2" style="width: 300px;"><ui-select-match >{{$item.name}}</ui-select-match><ui-select-choices repeat="lang.id as lang in langs | filter: { name : $select.search }"><div ng-bind-html="lang.name | highlight: $select.search"></div></ui-select-choices></ui-select>',
                restrict : 'E',
                require : 'ngModel',
                replace : true
                    ....
            };
        });

But how do I pass the ngModel from my language-picker to the ui-select directive ?

UPDATE

Using the suggestions below, I got it work with ui-select, but the outer model doesn’t get updated at all,see plnkr.co/edit/Y43dmMGIc5GxM9fLoNPW, probably because it’s child scope and parent scope remains the same?

UPDATE 2

I got it to work in a convoluted way that looks horrible to me, because I’ve no idea why it “works” in the first place (see the weird stuff happening in the controller):

app.directive('languagePicker', function(LanguageService) {
            return {
                templateUrl : 'LanguagePickerTpl.html',
                restrict : 'E',
                scope : {
                    languages : '='
                }, 
                controller : function($scope, LanguageService) {
                    console.log($scope);
                    $scope.langs = LanguageService.get();
                    $scope.model = $scope;
                }

            };
        })

template:

<ui-select ng-model="model.languages" multiple search-enabled="true" 
  theme="select2" style="width: 300px;">
    <ui-select-match>{{$item.name}}</ui-select-match>
    <ui-select-choices repeat="lang.id as lang in langs | filter: { name : $select.search }">
      <div ng-bind-html="lang.name | highlight: $select.search"></div>
    </ui-select-choices>
  </ui-select>

I would be very happy if anyone could explain what’s going on (the “working” example is here
http://plnkr.co/edit/B53F9sc7UGkj0uxUpC17
)

How to solve :

I know you bored from this bug, So we are here to help you! Take a deep breath and look at the explanation of your problem. We have many solutions to this problem, But we recommend you to use the first method because it is tested & true method that will 100% work for you.

Method 1

The ng-model has some special handling, see here under the heading “Custom Control Example”. The steps are:

  1. I suggest you use isolated scope; it makes the interface to your component clearer and saves you from side-effects. In this case you want to pass the list of available options (languages):

    scope: {
        langs: '='
    }
    

    Usage would be:

    <language-picker ng-model="model.selectedLanguages" langs="langs"/>
    
  2. Your directive requires (maybe optionally) the ngModel:

    require: ['ngModel']
    
  3. You override ngModel‘s $render method, e.g.:

    link: function(scope,elem,attrs,ctrls) {
        var ngModelCtrl = ctrls[0];
        ngModelCtrl.$render = function() {
            ...
        };
    }
    

    The logic of render is responsible for transferring the model value (the one here: <language-picker ng-model="model.selectedLanguages"/>, i.e. model.selectedLanguages) to the view. The simplest thing I can think of, is to use isolated scope and transfer the outer model value to a variable of the isolated scope as:

        ngModelCtrl.$render = function() {
            scope.innerSelection = ngModelCtrl.$viewValue;
        };
    

    Bind this variable in the template as:

    <ui-select ng-model="innerSelection" ...>
        ...
    </ui-select>
    
  4. Last you have to make sure that changes to the inner selection will be propagated to the outer model:

        // still inside link()
        scope.$watch('innerSelection', function(newval, oldval) {
            if( newval != oldval ) { // skip the first time
                ngModelCtrl.$setViewValue(newval);
            }
        });
    

This solution may be a bit more involved than others, but lets you use all the features of the ngModel, e.g. validation, parsing/formatting (i.e. data conversion).

Method 2

You need to use the “equals” syntax on you directive’s scope. This will keep hold of the values populated in the parent scope.
So your directive becomes:

app.directive('languagePicker', function() {
        return {
            template : '<ui-select ng-model="**PARENT'S NGMODEL**" multiple search-enabled="true" theme="select2" style="width: 300px;"><ui-select-match >{{$item.name}}</ui-select-match><ui-select-choices repeat="lang.id as lang in langs | filter: { name : $select.search }"><div ng-bind-html="lang.name | highlight: $select.search"></div></ui-select-choices></ui-select>',
            restrict : 'E',
            require : 'ngModel',
            replace : true,
            scope: {
                ngModel: "=ngModel"
            }
            ...
        };
    });

I am confident this will work for you 🙂
Let me know if it does not!

Method 3

You can pass model.selectedLanguages as a property to the directive such as:

< language-picker language="model.selectedLanguage" />

And inside the directive use the language property.
You can decide upon which scope you need.

I also recommend using a templateUrl to the html document with that bit of code in it. It really makes life much easier.
Then you can modify the html document to simply put ng-model=”language” after editing your directive as suggested.

There really are plenty of examples in the official documentation.
https://docs.angularjs.org/guide/directive

Method 4

I got it working in two different ways:

Given the following "call":

      <div
        ng-model="myData"
        required
        boolean-select
      ></div>

Either using "replace: true":

  .directive('booleanSelect', function booleanSelect() {
    return {
      restrict: 'A',
      template: '<select ' +
        'class="form-control" ' +
        'ng-options="v.value as v.label for v in yesNoOptions" ' +
        'ng-required="required"><option value="">Select</option></select>',
      replace: true,
      link: function ($scope, $element, $attrs) {
        $scope.required = $attrs.hasOwnProperty('required');

        $scope.yesNoOptions = [{
          value: true,
          label: 'Yes',
        }, {
          value: false,
          label: 'No',
        }];
      },
    };
  })

You don’t need to put ng-model in the template as it’s, well, replaced "inline", merging attributes from the "caller", so the caller’s ng-model will be used.

Or, this way:

  .directive('booleanSelect', function booleanSelect() {
    return {
      restrict: 'A',
      template: '<select ' +
        'class="form-control" ' +
        'ng-model="ngModel" ' +
        'ng-options="v.value as v.label for v in yesNoOptions" ' +
        'ng-required="required"><option value="">Select</option></select>',
      scope: {
        ngModel: '=ngModel',
      },
      link: function ($scope, $element, $attrs) {
        $scope.required = $attrs.hasOwnProperty('required');
        $scope.yesNoOptions = [{
          value: true,
          label: 'Yes',
        }, {
          value: false,
          label: 'No',
        }];
      },
    };
  })

Here we don’t replace, but we get the ngModel without requiring the controller at all, just as a scope value. Then we can use it in the template. We can’t use replace because there would be a conflict of ng-model.

I personally prefer the first option! (and one less element in the dom).

Note: Use and implement method 1 because this method fully tested our system.
Thank you 🙂

All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0

Leave a Reply