Angularjs directive chaining - angularjs-directive

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));
}
}
}
});

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>

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.

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.

Related

How to angular $watch element height after class change by model?

I've read and tried probably every thread on angular $watch() DOM element height but can't work out how to do this. Any help is greatly appreciated!
I have an angular app that does a simple class name update by changing a model value. Example:
class="theme-{{themeName}}"
When the class updates the DIV changes height.
I want to receive a callback on the height change.
I've tried to use $watch() and $watch(..,,true) and using both angular.element() as well as jquery ( $('foo')... ) but the $digest cycle never even calls the $watch expression.
Update (code example):
'use strict';
angular.module('k2')
.directive('k2', ['$rootScope', '$templateCache', '$timeout', 'lodash' ,'k2i',
function ($rootScope, $templateCache, $timeout, lodash, k2i) {
return {
restrict: 'E',
template: $templateCache.get('k2/templates/k2.tpl.html'),
replace: true,
scope: {
ngShow: '=',
ngHide: '=',
settings: '='
},
link: function(scope, elem, attrs) {
k2i.initK2(scope, scope.settings || {});
scope.$watch(function() {
return $('.k2 .k2-template [k2-name]').height();
}, function(newValue, oldValue, scope) {
respondToChange(newValue, oldValue, scope);
}, true);
scope.$watch(function() {
var kb = document.querySelectorAll('.k2 .k2-template [k2-name]')[0];
var ab = document.querySelectorAll('.k2 .k2-template [k2-name] .k2-acc-bar')[0];
var value = {
kb: 0,
ab: 0
}
if (kb) {
value.kb = kb.clientHeight;
}
if (ab) {
value.ab = ab.clientHeight;
}
return value;
}, function(newValue, oldValue, scope) {
respondToChange(newValue, oldValue, scope);
}, true);
function respondToChange(newValue, oldValue, scope) {
if (newValue === oldValue) return;
if (!scope.k2Pending) return;
var kbNode = document.querySelectorAll('.k2 .k2-template [k2-name="' + scope.k2Pending.name + '"]');
var abNode = document.querySelectorAll('.k2 .k2-template [k2-name="' + scope.k2Pending.name + '"] .k2-acc-bar');
// Ensure required keyboard elements are in the DOM and have height.
if ((kbNode.length > 0 && !scope.k2Pending.requiresAccessoryBar ||
kbNode.length > 0 && abNode.length > 0 && scope.k2Pending.requiresAccessoryBar) &&
(kbNode[0].clientHeight > 0 && !scope.k2Pending.requiresAccessoryBar ||
kbNode[0].clientHeight > 0 && abNode[0].clientHeight > 0 && scope.k2Pending.requiresAccessoryBar)) {
$rootScope.$emit('K2KeyboardInDOM', scope.k2Pending.name, getHeight());
}
};
function getHeight() {
var height = {};
var kbElem = angular.element(document.querySelectorAll('.k2')[0]);
var wasHidden = kbElem.hasClass('ng-hide');
kbElem.removeClass('ng-hide');
height[k2i.modes.NONE] = 0;
height[k2i.modes.ALL] = document.querySelectorAll('.k2 .k2-template [k2-name="' + scope.k2Name + '"]')[0].clientHeight;
height[k2i.modes.ACCESSORY_BAR_ONLY] = document.querySelectorAll('.k2 .k2-template [k2-name="' + scope.k2Name + '"] .k2-acc-bar')[0].clientHeight;
height[k2i.modes.KEYBOARD_KEYS_ONLY] = height[k2i.modes.ALL] - height[k2i.modes.ACCESSORY_BAR_ONLY];
if (wasHidden) {
kbElem.addClass('ng-hide');
}
return height;
};
}
}
}
]);
You should simply watch the 'themeName' variable. Then do your calculations in $timeout. $timeout should be required to wait your manual DOM updates. For ex:
scope.$watch('k2Name', function(newValue, oldValue){
$timeout(function(){
//do what you want
});
});

unable to call click event in template in angularjs directive

In have one common directive which will display in each and every page. Already visited page displaying as a done, So i want click event on already visited page. I added ng-click and wrote function in controller. Can anybody help why it's not working.
html
<div class="row">
<div class="col-sm-12">
<wizard-menu currentPage="searchOffering"></wizard-menu>
</div>
</div>
js
function generateMenuHtml(displayMenuItems, currentPage, businessType) {
var htmlOutput = '';
var indexOfCurrentPage = getIndexOf(displayMenuItems, currentPage, 'pageName');
if (businessType) {
htmlOutput += '<ol class="wizard wizard-5-steps">';
} else {
htmlOutput += '<ol class="wizard wizard-6-steps">';
}
angular.forEach(displayMenuItems, function (value, key) {
var htmlClass = '';
if (indexOfCurrentPage > key) {
htmlClass = 'class="done" ng-click="goToFirstPage()"';
} else if (key === indexOfCurrentPage) {
htmlClass = 'class="current"';
} else {
htmlClass = '';
}
if (key!==1){
htmlOutput += '<li ' + htmlClass + '><span translate="' + value.title + '">' + value.title + '</span></li>';
}
});
htmlOutput += '</ol>';
return htmlOutput;
}
.directive('wizardMenu',['store','WIZARD_MENU', 'sfSelect', function(store, WIZARD_MENU, Select) {
function assignPageTemplate(currentPageValue){
var storage = store.getNamespacedStore(WIZARD_MENU.LOCAL_STORAGE_NS);
var data=storage.get(WIZARD_MENU.LOCAL_STORAGE_MODEL);
var businessTypePath='offeringFilter.businessType.masterCode';
var businessTypeValue = Select(businessTypePath, data);
if(businessTypeValue!=='' && businessTypeValue==='Prepaid'){
template = generateMenuHtml(businessTypePrepaid, currentPageValue, true);
}
else{
template = generateMenuHtml(commonMenu, currentPageValue, true);
}
return template;
}
return {
require: '?ngModel',
restrict: 'E',
replace: true,
transclude: false,
scope: {
currentPage: '='
},
controller: ['$scope', '$state', '$stateParams', function($scope, $state, $stateParams) {
$scope.goToFirstPage = function() {
console.log('inside First Page');
};
}],
link: function(scope,element,attrs){
element.html(assignPageTemplate(attrs.currentpage));
},
template: template
};
}])
I'm unable to call goToFirstPage(). Can anybody tell what is wrong here.
Thanks in advance....
You need to compile the template. If you use Angular directive such as ng-click and you simply append them to the DOM, they won't work out of the box.
You need to do something like this in your link function:
link: function(scope,element,attrs){
element.append($compile(assignPageTemplate(attrs.currentpage))(scope));
},
And don't forget to include the $compile service in your directive.
Hope this helps, let me know!
Documentation on $compile: https://docs.angularjs.org/api/ng/service/$compile

Automatic text detection on contenteditable div using angular js

what is the best way to do following using angular js
when writing text on a contenteditable div need to detect special word like ' {{FULL_NAME}} ' and covert to tag with pre-deifined constant words
example - if some one write
' His name is {{FULL_NAME}} '
should be instantly convert to ' His name is john smith '
Here is a demo plunker: http://plnkr.co/edit/GKYxXiDKv7fBeaE7rrZA?p=preview
Service:
app.factory('interpolator', function(){
var dict = { .... };
return function(str){
return (str || "").replace(/\{\{([^\}]+)\}\}/g, function(all, match){
return dict[match.trim().toLowerCase()] || all;
});
};
});
Directive:
app.directive('edit',[ 'interpolator', function(interpolator){
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, element, attrs, ngModel) {
element.on('blur', function(e) {
scope.$apply(function() {
var content = interpolator(element.text());
element.text(content);
ngModel.$setViewValue(content);
});
});
ngModel.$formatters.push(interpolator);
ngModel.$render = function() {
element.text(ngModel.$viewValue);
ngModel.$setViewValue(ngModel.$viewValue);
};
}
};
}]);
Here's an example using simple DOM methods, based on other answers I've provided on Stack Overflow. It also uses Rangy to save and restore the selection while doing the substitutions so that the caret does not move.
Demo:
http://jsbin.com/zuvevocu/2
Code:
var editorEl = document.getElementById("editor");
var keyTimer = null, keyDelay = 100;
function createKeyword(matchedTextNode) {
var el = document.createElement("b");
el.style.backgroundColor = "yellow";
el.style.padding = "2px";
el.contentEditable = false;
var matchedKeyword = matchedTextNode.data.slice(1, -1); // Remove the curly brackets
matchedTextNode.data = (matchedKeyword.toLowerCase() == "name") ? "John Smith" : "REPLACEMENT FOR " + matchedKeyword;
el.appendChild(matchedTextNode);
return el;
}
function surroundInElement(el, regex, surrounderCreateFunc) {
// script and style elements are left alone
if (!/^(script|style)$/.test(el.tagName)) {
var child = el.lastChild;
while (child) {
if (child.nodeType == 1) {
surroundInElement(child, regex, surrounderCreateFunc);
} else if (child.nodeType == 3) {
surroundMatchingText(child, regex, surrounderCreateFunc);
}
child = child.previousSibling;
}
}
}
function surroundMatchingText(textNode, regex, surrounderCreateFunc) {
var parent = textNode.parentNode;
var result, surroundingNode, matchedTextNode, matchLength, matchedText;
while ( textNode && (result = regex.exec(textNode.data)) ) {
matchedTextNode = textNode.splitText(result.index);
matchedText = result[0];
matchLength = matchedText.length;
textNode = (matchedTextNode.length > matchLength) ?
matchedTextNode.splitText(matchLength) : null;
surroundingNode = surrounderCreateFunc(matchedTextNode.cloneNode(true));
parent.insertBefore(surroundingNode, matchedTextNode);
parent.removeChild(matchedTextNode);
}
}
function updateKeywords() {
var savedSelection = rangy.saveSelection();
surroundInElement(editorEl, /\{\w+\}/, createKeyword);
rangy.restoreSelection(savedSelection);
}
function keyUpHandler() {
if (keyTimer) {
window.clearTimeout(keyTimer);
}
keyTimer = window.setTimeout(function() {
updateKeywords();
keyTimer = null;
}, keyDelay);
}
editorEl.onkeyup = keyUpHandler;
Related:
https://stackoverflow.com/a/5905413/96100
https://stackoverflow.com/a/4026684/96100
https://stackoverflow.com/a/4045531/96100

watch changes on JSON object properties

I'm trying to implement a directive for typing money values.
var myApp = angular.module('myApp', []);
var ctrl = function($scope) {
$scope.amount = '0.00';
$scope.values = {
amount: 0.00
};
};
myApp.directive('currency', function($filter) {
return {
restrict: "A",
require: "ngModel",
scope: {
separator: "=",
fractionSize: "=",
ngModel: "="
},
link: function(scope, element, attrs) {
if (typeof attrs.separator === 'undefined' ||
attrs.separator === 'point') {
scope.separator = ".";
} else {
scope.separator = ",";
};
if (typeof attrs.fractionSize === 'undefined') {
scope.fractionSize = "2";
};
scope[attrs.ngModel] = "0" + scope.separator;
for(var i = 0; i < scope.fractionSize; i++) {
scope[attrs.ngModel] += "0";
};
scope.$watch(attrs.ngModel, function(newValue, oldValue) {
if (newValue === oldValue) {
return;
};
var pattern = /^\s*(\-|\+)?(\d*[\.,])$/;
if (pattern.test(newValue)) {
scope[attrs.ngModel] += "00";
return;
};
}, true);
}
};
});
HTML template:
<div ng-app="myApp">
<div ng-controller="ctrl">
{{amount}}<br>
<input type="text" style="text-align: right;" ng-model="amount" currency separator="point" fraction-size="2"></input>
</div>
</div>
I want to bind the value in my input element to values.amount item in controller but the watch instruction of my directive doesn't work.
How do I leverage two-way-data-binding to watch JSON objects?
To understand problem more precise I've created a jsfiddle.
The task is the following: Add extra zeros to the input element if user put a point. I mean if the value in input element say "42" and user put there a point, so the value now is "42." two extra zeros have to be aded like this "42.00".
My problems:
If I use ng-model="amount" the logic in input element works, but amount value of outer controller doesn't update.
If I use ng-model="values.amount" for binding, neither amount of outer controller nor input element logic works.
I really have to use ng-model="values.amount" instruction, but it doesn't work and I don't know why.
Any ideas?

one column having string data and check box in extjs4.1

in my grid,i should have the ability to have check boxes and String data based on the statuses returned from the webservice.Now i am writing custom renderer function given below :
function customcolumn1(value, metadata, record) {
var completedTime = record.get('nccompletedTime');
var completedByLastName = record.get('nccompletedByLastName');
var completedByFirstName = record.get('nccompletedByFirstName');
if (value == 'Completed') {
return completedTime + " " + completedByLastName + "," + completedByFirstName;
} else if (value == 'Pending') {
return "<input type='checkbox' disabled>";
} else if (value == 'Assigned') {
return "<input type='checkbox'>";
}
}
And the grid is
var SAFjobgrid = Ext.create('Ext.grid.Panel', {
store: store,
columns: [
{
text: "",
width: 30,
renderer: customSeqNumber,
dataIndex: 'sequenceNumber'},
{
text: "Task",
width: 350,
renderer: customTask,
dataIndex: 'label'},
{
text: "Complete",
width: 160,
renderer: customcolumn2,
dataIndex: 'stampActionStatus'},
{
text: "Verified",
width: 160,
renderer: customcolumn,
dataIndex: 'verifyActionStatus'},
{
text: "Non Compliance",
flex: 1,
renderer: customcolumn1,
dataIndex: 'ncActionStatus'}
]
});
Now i want to check the check boxes and capture the stampIds for those records.And when i click on update button which is oustside of the grid,i should be able to call the webservice with the captured stampId's.I tried to put onclick event on checkbox while returning from the renderer function.But how should i access the stampId for that record.if i select multiple checkboxes,multiple stampid's should go to webservice and if i uncheck the checkbox,the corresponding stampid should be removed from stampId's.
Could anyone help on this one..
You can look at CheckColumn user extension http://docs.sencha.com/ext-js/4-1/#!/api/Ext.ux.CheckColumn But in your case you have to override renderer method like this
renderer : function(value, meta, record){
var cssPrefix = Ext.baseCSSPrefix,
cls = [cssPrefix + 'grid-checkheader'],
completedTime = record.get('nccompletedTime'),
completedByLastName = record.get('nccompletedByLastName'),
completedByFirstName = record.get('nccompletedByFirstName');
if (value == 'Completed') {
//render string instead of checkbox
return Ext.String.format('{0} {1},{2}', completedTime, completedByLastName, completedByFirstName);
}
if (value === 'Assigned') {
cls.push(cssPrefix + 'grid-checkheader-checked');
}
//render checkbox stub,
//it is actually a div element with special css classes to simulate checkbox
return '<div class="' + cls.join(' ') + '"> </div>';
}
To collect al checked/unchecked rows you can use this store method http://docs.sencha.com/ext-js/4-1/#!/api/Ext.data.AbstractStore-method-getUpdatedRecords
It returns array of models which were updated by checking/unchecking this column