2

My objective - Directive dir2 replaces itself with directive dir1 which in turn replaces with input.

However during dir1 replacement by input I get parent is null exception in replaceWith function.

Fiddle for the same

var app = angular.module("myapp",[]);

function MyCtrlr($scope){
    $scope.vars = {val:"xyz"};
}

app.directive("dir2", function($compile){
    return {
        restrict : 'E',
        replace : true,
        compile :function(el, attrs) {
            var newhtml =  '<dir1 field="' + attrs.field + '" />';
            return function(scope, el, attrs) {
                console.log('dir2 parent = ' + el.parent());
                el.replaceWith($compile(newhtml)(scope));
            }
        }
    }
});

app.directive("dir1", function($compile){
    return {
        restrict : 'E',
        replace : true,
        compile :function(el, attrs) {
            return function(scope, el, attrs) {
                console.log('dir1 parent = ' + el.parent());
                console.log(scope.field);
                el.replaceWith($compile('<input type="text" ng-model="' + attrs.field + '.val" />')(scope));
            }
        }
    }
});

3 Answers 3

1

Basically you are getting the error message because the compilation process happens in two phases: compile and link. As your directives are being compiled at the same time (1st phase),when the dir2 finishes its compilation the DOM element of the dir1 is not ready yet for manipulation.

So I've changed dir1 to use the link phase of the process (2nd phase).

Like this dir2 have the chance to be completed and created the DOM element(template) used by dir1

http://plnkr.co/edit/GrOPkNaxOxcXFDZfDwWh

 <!doctype html>
 <html lang="en" ng-app="myApp">
 <head>
 <meta charset="UTF-8">
 <title>Document</title>

 <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.8/angular.min.js"></script>

 <script>

    var app = angular.module("myApp",[]);

    function MyCtrlr($scope){
        $scope.vars = {val:"xyz"};
    }


    app.directive("dir2", function($compile){
        return {
            restrict : 'E',
            replace : true,
            compile :function(el, attrs) {
                var newhtml =  '<dir1 field="' + attrs.field + '" />';
                return function(scope, el, attrs) {
                    console.log('dir2 parent = ' + el.parent());
                    el.replaceWith($compile(newhtml)(scope));
                }
            }
        }
    });

    app.directive("dir1", function($compile){
        return {
            restrict : 'E',
            replace : true,
            template: '<input type="text" ng-model="field" />',
            scope: {
                field: '='
            },
            link: function(scope, el, attrs) {
                    console.log('dir1 parent = ' + el.parent());
                    console.log(scope.field);
                }
        }
    });

 </script>

 </head>
 <body>
 <div ng-app="myapp">
     Testing
 <div ng-controller = "MyCtrlr">
     <span ng-bind="vars.val"></span>
     <dir2 field="vars"></dir2>
 </div>
 </div>
 </body>
 </html>
Sign up to request clarification or add additional context in comments.

6 Comments

When I try your plunker it doesn't work, the input text has [object Object] in it and the two way data binding is broken.
Also, both directives are still using the link phase. When you return a function from the compile function, that is a postLink function and will be executed during the linking phase.
Hummm... I noticed that but I didn't want to change your code! I thought you really wanted to have the [object Object] in the input tex :) It's a quick fix. just add .val to the variable. here it is the updated plunk (plnkr.co/edit/GrOPkNaxOxcXFDZfDwWh). The two way data binding is working as expected.
I'm not the OP, but I see that your solution is almost identical to mine. I just chose dir1 to have a template instead of dir2.
Actually passing object is important, because dir1 is expected to one of the input (text, checkbox), select and their other attributes based on the object properties. I had developed dir1 with that in mind and it works, when you pass parent scope i.e. scope.$parent to the function returned by $compile.
|
0

Here is how you can accomplish what you want to do:

Wokring plunker

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

function MyCtrlr($scope){
    $scope.vars = {val:"xyz"};
}

app.directive("dir2", function($compile){
    return {
        restrict : 'E',
        replace : true,
        template: '<dir1></dir1>',
        link: function(scope, el, attrs) {
        }
    };
});

app.directive("dir1", function($compile){
    return {
        restrict : 'E',
        scope: {
          field: '='
        },
        link: function(scope, el, attrs) {
          scope.model = scope.field;
          el.replaceWith($compile('<input type="text" ng-model="model.val" />')(scope));
        }
    };
});

This preserves the two way data binding, but is rather limited in its use. I am assuming that your use case is a simplification of your problem, otherwise a different approach might be simpler.

I am still working out the details on exactly what is going wrong in your fiddle, will post an edit when I figure that out.

Comments

0

Final Fiddle

Angularjs Chained Directives Replacing Elements

I started with an objective to develop a generic directive to render forms elements for Activiti engine tasks using Angularjs. For which I developed a directive (say dir1) which based on certain properties of form element would render appropriate type html element (input (text, checkbox), select or span) replacing the dir1 element.

The controller that gathers Activiti form is emulated by the following code

function MyCtrlr($scope) {

$scope.v = [{value: 'init0'},

    {value: 'init1'},

    {value: 'init2'},

    {value: 'init3'}

];

$scope.formVals = {

    vals: [{

        id: 'one',

        type: 'string',

        value: 'xyz'

    }, {

        id: 'two',

        type: 'enum',

        value: '2',

        writable:true,

        enumValues: [{

            'id': 1,

            'name': 'ek'

        }, {

            'id': 2,

            'name': 'don'

        }]

    }, {

        id: 'three',

        type: 'enum',

        value: 'abc',

        writable:true,

        enumValues: [{

            'id': 3,

            'name': 'tin'

        }, {

            'id': 4,

            'name': 'chaar'

        }]

    }, {

        id: 'four',

        type: 'enum',

        value: 'abc',

        writable:true,

        enumValues: [{

            'id': 5,

            'name': 'paach'

        }, {

            'id': 6,

            'name': 'sahaa'

        }]

    },

        {id:'five',

            type:'string',

            value:'test',

            writable:true

        }

    ]

};

//$scope.formVals.vals[0].varRef = $scope.v[0];

//$scope.formVals.vals[1].varRef = $scope.v[1];

$scope.formVals.vals[2].varRef = $scope.v[2];

$scope.formVals.vals[3].varRef = $scope.v[3];



$scope.verify = function () {

    alert($scope.v[0].value + '...' + $scope.v[1].value + '...' + $scope.v[2].value + '...' + $scope.v[3].value);

};

}

And the directive dir1 as follows

app.directive('dir1', function ($compile) {

var getTemplate = function(fld, fvarnm, debug) {

    value = ' value="' + fld.value + '"';

    nm = ' name="' + fld.id + '"';

    ngmodel = ' ng-model="' + fvarnm + '.varRef.value"';

    disabled = fld.writable?'':' disabled=disabled';

    switch(fld.type) {

        case 'activitiUser':

        case 'enum':

            template = '<select '

                + nm + disabled

                + (fld.varRef != null?ngmodel:'');

            template += '<option></option>';

            for (e in fld.enumValues) {

                selected = '';

                ev = fld.enumValues[e];

                if ((fld.varRef == null && (fld.value == ev.id)) || (fld.varRef != null) && (fld.varRef.value == ev.id))

                    selected = ' SELECTED ';

                template += '<option value="' + ev.id + '"' +  selected + '>' + ev.name + '</option>';

            }

            template += '</select>';

            break;

        case 'boolean':

            template = '<input type="checkbox"'

                + nm + disabled

                + (fld.varRef != null?ngmodel:value)

                + (fld.value?' CHECKED':'')

                + '></input>';

            break;

        default:

            template = '<input type="text"'

                + nm + disabled

                + (fld.varRef != null?ngmodel:value)

                + ' value-format="' + fld.type + ' '

                + fld.datePattern + '"'

                + '></input>';

    }

    if (fld.varRef != null && typeof(debug) != 'undefined' && debug.toLowerCase() == 'true') {

        template = '<div>' + template

            + '<span ng-bind="' + fvarnm

            + '.varRef.value"></span>' + '</div>';

    }

    return template;

};



return {

    restrict: 'E',

    replace: true,

    scope : {

        field : '='

    },

    link : function(scope, element, attrs) {
        html = getTemplate(scope.field, attrs.field, attrs.debug);
        element.replaceWith($compile(html)(scope.$parent));
    }    

};

});

However when nuances of application on top of Activiti came in picture, I made a decision that I want to give developer an ability to user dir1 for his generic requirements and allow him to develop his own directive chained to dir1 to handle these nuances. About nuances – based on properties of form element application developer would either go for generic rendering provided by dir1 or replace dir2 element with appropriate html element.

I added dir2 as follows -

app.directive('dir2', function ($compile) {

var getTemplate2 = function(scope, el, attrs) {

    html2 = "<dir1 field='" + attrs.field + "'></dir1>";

    if (scope.field.id == 'five') {

        html2 = '<span style="font-weight:bold" ';

        if (typeof(scope.field.varRef) != 'undefined' && scope.field.varRef) {

            html2 += ' ng-bind="f.varRef.value" ';

        } else {

            html2 += ' ng-bind="f.value" ';

        }

        html2 += '></span> ';

    }

    return html2;

};



return {

    restrict: 'E',

    replace : true,

    scope : {

        field : '='

    },

    link: function (scope, el, attrs) {

         var html2 = getTemplate2(scope, el, attrs);

        el.replaceWith($compile(html2)(scope.$parent));

   }

};

});

However I started getting null parent error in replaceWith call in dir1. After lot of disoriented thinking and console logging I realized that the moment html2 was getting compiled at el.replaceWith($compile(html2)(scope.$parent)) statement, dir1 link function was triggering whenever html2 was a dir1 element. At this point the dir1 element did not have any parentNode. Therefore I came up with the following arrangement. In gettemplate2 function html2 default value became html2 = "", i.e. passing parent attribute. In dir1 link function I made the following changes html = getTemplate(scope.field, attrs.field, attrs.debug); scope.dir1el = $compile(html)(scope); if (typeof(attrs.parent) == 'undefined') { element.replaceWith(scope.dir1el); } thus preventing replacement in dir1. The complementary change in dir2 was

        var html2 = getTemplate2(scope, el, attrs);
        if (html2 == null) {
            $compile("<dir1 parent='true' field='" + attrs.field + "'></dir1>")(scope.$parent);
            ne = scope.$$nextSibling.dir1el;
        } else {
            ne = $compile(html2)(scope.$parent);
        }
        el.replaceWith(ne);

Since dir1 and dir2 are sibling directives, I had to access dir1 scope using $$nextSibling. Thus allowing me to replace element in dir2 with one generated by dir1 or dir2 as appropriate.

I also developed an alternate solution using attribute directive dir3, where dir3 would become attribute of dir1. Here dir1 scope becomes parent scope of dir3. And bespoke element in dir3 is replaces element replaces element created by dir1. Thus this solution involves double DOM replacement.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.