Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

angularjs pass ngModel from wrapper directive to wrapped directive

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 )

like image 564
AunAun Avatar asked Apr 28 '15 09:04

AunAun


2 Answers

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).

like image 188
Nikos Paraskevopoulos Avatar answered Nov 15 '22 14:11

Nikos Paraskevopoulos


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!

like image 38
JDTLH9 Avatar answered Nov 15 '22 14:11

JDTLH9