angularjsangularjs-directiveangularjs-compile

Custom Directive fails to compile within ng-repeat


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


Solution

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