2

Could anyone help me solve a scoping issue when compiling a directive within ng-repeat?

https://plnkr.co/edit/y6gfpe01x3ya8zZ5QQpt?p=preview

The custom directive input-by-type can replace a <div> with the appropriate <input> based on a variable type - this works fine until used within an ng-repeat.

As you can see on the plnkr example, the directive works as expected until it is used within ng-repeat.

var app = angular.module('plunker', []);

app.controller('MainCtrl', function($scope) {
    $scope.data = {};
    $scope.inputs = [
        { name: 'Some Text', type: 'text',   id: 1 },
        { name: 'EMail',     type: 'email',  id: 2 },
        { name: 'Age',       type: 'number', id: 3 }
    ];
});

app.directive('inputByType', ['$compile', '$interpolate', function($compile, $interpolate){
    return {
        restrict: 'A', // [attribute]
        require: '^ngModel',
        scope: true,
        compile: function(element, attrs, transclude){
            var inputs = {
                text:    '<input type="text"  name="'+attrs.name+'" ng-model="'+attrs.ngModel+'" ng-disabled="'+attrs.ngDisabled+'" ng-required="'+attrs.ngRequired+'" placeholder="...">',
                email:   '<input type="email" name="'+attrs.name+'" ng-model="'+attrs.ngModel+'" ng-disabled="'+attrs.ngDisabled+'" ng-required="'+attrs.ngRequired+'" placeholder="...@...">',
                number:  '<input type="number" name="'+attrs.name+'" ng-model="'+attrs.ngModel+'" ng-disabled="'+attrs.ngDisabled+'" ng-required="'+attrs.ngRequired+'" placeholder="###">',
            };
            return function(scope){
                var type = $interpolate(attrs.inputByType)(scope); // Convert input-by-type="{{ some.type }}" into a useable value
                var html = inputs[type] || inputs.text;
                var e = $compile(html)(scope);
                element.replaceWith(e);
                console.log(type, html, element, e);
            };
        },
    };
}]);

If I manually reference inputs[0] to compile the input-by-type directive, it works just fine:

<label>
    {{ inputs[0].name }}
    <div input-by-type="{{ inputs[0].type }}" name="myInputA" ng-model="data.A" ng-required="true"></div>
</label>

However, the moment I wrap this in an ng-repeat block, the compile fails with some unexpected outputs:

<label ng-repeat="input in inputs">
    {{ input.name }}
    <div input-by-type="{{ input.type }}" name="myInput{{ $index }}" ng-model="data[input.id]" ng-required="true"></div>
</label>

Expected Output:

Expected


Actual Output:

Actual

oodavid
  • 2,148
  • 2
  • 23
  • 26

2 Answers2

2

The postLink function is missing element and attrs parameters:

app.directive('inputByType', ['$compile', '$interpolate', function($compile, $interpolate){
    return {
        restrict: 'A', // [attribute]
        require: '^ngModel',
        scope: true,
        // terminal: true,
        compile: function(element, attrs, transclude){
            var inputs = {
                text:    '<input type="text"  name="'+attrs.name+'" ng-model="'+attrs.ngModel+'" ng-disabled="'+attrs.ngDisabled+'" ng-required="'+attrs.ngRequired+'" placeholder="...">',
                email:   '<input type="email" name="'+attrs.name+'" ng-model="'+attrs.ngModel+'" ng-disabled="'+attrs.ngDisabled+'" ng-required="'+attrs.ngRequired+'" placeholder="...@...">',
                number:  '<input type="number" name="'+attrs.name+'" ng-model="'+attrs.ngModel+'" ng-disabled="'+attrs.ngDisabled+'" ng-required="'+attrs.ngRequired+'" placeholder="###">',
                // image upload (redacted)
                // file upload (redacted)
                // date picker (redacted)
                // color picker (redacted)
                // boolean (redacted)
            };
            //return function(scope){
            //USE postLink element, attrs
            return function postLinkFn(scope, element, attrs) {
                var type = $interpolate(attrs.inputByType)(scope); // Convert input-by-type="{{ some.type }}" into a useable value
                var html = inputs[type] || inputs.text;
                var e = $compile(html)(scope);
                element.replaceWith(e);
                console.log(type, html, element, e);
            };
        },
    };
}]);

By omitting element and attrs parameters, the postLink function created a closure and used the element and attrs arguments of the compile function. Even though the $compile service was invoking the postLink function with the proper arguments, they were being ignored and the compile phase versions were used instead.

This causes problems for ng-repeat because it clones the element in order to append it to new DOM elements.

georgeawg
  • 48,608
  • 13
  • 72
  • 95
  • I was aware of the postLink arguments however it had not dawned on me that they could differ depending on the phase. Thank you. – oodavid Mar 09 '17 at 08:51
0

@georgeawg's answer is correct, however I encountered a second issue that I will outline below with a solution.

Issue: The ngModel would not act as expected (the $pristine / $dirty etc. properties would not be available, nor would they propagate to the container formCtrl).

To fix this, I followed the advice on this answer: https://stackoverflow.com/a/21687744/1122851 and changed the manner in which postLink was compiling the element, like so:

var type = $interpolate(attrs.inputByType)(scope);
var html = inputs[type] || inputs.text;
var template = angular.element(html);
element.replaceWith(template);
$compile(template)(scope);

I then realised that require: 'ngModel', scope: true and terminal: true were no longer needed (they were relics from my various tests anyhow). The final code:

app.directive('inputByType', ['$compile', '$interpolate', function($compile, $interpolate){
    return {
        restrict: 'A', // [attribute]
        compile: function(element, attrs, transclude){
            var inputs = {
                text:    '<input type="text"  name="'+attrs.name+'" ng-model="'+attrs.ngModel+'" ng-disabled="'+attrs.ngDisabled+'" ng-required="'+attrs.ngRequired+'" placeholder="...">',
                email:   '<input type="email" name="'+attrs.name+'" ng-model="'+attrs.ngModel+'" ng-disabled="'+attrs.ngDisabled+'" ng-required="'+attrs.ngRequired+'" placeholder="...@...">',
                number:  '<input type="number" name="'+attrs.name+'" ng-model="'+attrs.ngModel+'" ng-disabled="'+attrs.ngDisabled+'" ng-required="'+attrs.ngRequired+'" placeholder="###">',
                // image upload (redacted)
                // file upload (redacted)
                // date picker (redacted)
                // color picker (redacted)
                // boolean (redacted)
            };
            return function postLinkFn(scope, element, attrs) {
                var type = $interpolate(attrs.inputByType)(scope); // Convert input-by-type="{{ some.type }}" into a useable value
                var html = inputs[type] || inputs.text;
                var template = angular.element(html);
                element.replaceWith(template);
                $compile(template)(scope);
            };
        },
    };
}]);

Demo: https://plnkr.co/edit/ZB5wlTKr0g5pXkRTRmas?p=preview

Community
  • 1
  • 1
oodavid
  • 2,148
  • 2
  • 23
  • 26