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.