I have a main directive that has an array on it's scope that contains data for constructing other directives that should be compiled and appended to the main directive.
The problem is that when I iterate through that array I only get the data from the last element in array,
so I can't properly bind respective data for each custom directive.
Plunker
main directive :
angular.module('testApp')
.directive('mainDirective', ["$compile", function ($compile) {
return {
template: ' <div><p>Main Directive </p><div class="insertion-point"></div></div>',
link: function (scope, element, attributes, controller) {
var insertionPoint = element.find('.insertion-point');
angular.forEach(scope.demoObj.panels, function (value, index) {
var directiveName = value.type;
scope.value = value;
var directiveString = "<div " + directiveName + " panel-data=value ></div>";
var compiledElement = $compile(directiveString)(scope);
insertionPoint.append(compiledElement);
});
}
}
}]);
directive to be nested:
angular.module('testApp')
.directive('nestedDirective', [function () {
return {
scope:{
panelData:'='
},
template:' <div><p>Nested Directive </p>{{panelData.data.test}}</div>'
}
}]);
data looks like this:
$scope.demoObj = {
panels:[
{
id:'unique_id_1',
type:'nested-directive',
data:{
test:'test 1'
}
},
{
id:'unique_id_2',
type:'nested-directive',
data:{
test:'test 2'
}
},
{
id:'unique_id_3',
type:'nested-directive',
data:{
test:'test 3'
}
}
]
}
As far as I can understand , the compilation is not happening immediately in the forEach statement, that's why every directive gets the data from the object with the id unique_id_3 (last element in array). Also all directives have isolated scope.
edit: I understand that in forEach I need to add the value to the scope so I can pass it to the nested directive isolated scope, and I understand that when the loop finishes scope.value will be the last value of the loop, but I was under the impression that compile will immediately pass the value to the nested directive and be done with it.
So, when does the compilation happen?
How can I circumvent this limitation?
The problem is the link step of the compiledElement will happen in the next digest cycle, at that time, scope.value is the last value of the data.
The solution is to create different value properties on scope, like this:
var directiveName = value.type;
var valueProp = 'value' + index;
scope[valueProp] = value;
var directiveString = "<div " + directiveName + " panel-data=" + valueProp + "></div>";
plunk
Please find the update code below. Rather than creating duplicate variable in scope Below is the solution. I have created plunker for the same
angular.module('testApp')
.directive('mainDirective', ["$compile", function ($compile) {
return {
template: ' <div><p>Main Directive </p><div class="insertion-point"></div></div>',
link: function (scope, element, attributes, controller) {
var insertionPoint = element.find('.insertion-point');
angular.forEach(scope.demoObj.panels, function (value, index) {
var directiveName = value.type;
var directiveString = "<div " + directiveName + " panel-data=demoObj.panels["+ index+"]></div>";
var compiledElement = $compile(directiveString)(scope);
insertionPoint.append(compiledElement);
});
}
}
}]);
Related
I have a directive that dynamically adds child custom directives to the DOM based on some input. Everything works fine. But when the input changes and I re-render the DOM with a different set of child custom directives, the old scopes of the child custom directives are not deleted and hence, the event handlers attached to them are still in memory.
I am re-rendering the DOM by just setting element[0].innerHTML = ''.
Is there a way to delete/destroy the scopes of the custom directive? I saw in some articles that scope.$destroy can be called but how to get a reference of the scope of the child custom directive?
const linker = function (scope, element) {
scope.$watch('data', function () {
reRenderToolbar();
}, true);
const reRenderToolbar = function () {
element[0].innerHTML = '';
_.forEach(scope.data, function (item, key) {
const directive = item.CustomDirective;
scope.options = item.options || {};
html = '<' + directive + ' options="options"></' + directive + '>';
element.append(html);
}
});
}
$compile(element.contents())(scope);
};
The issue was that I was not destroying the childscope in the parent as my app is multiscoped. This article helped me http://www.mattzeunert.com/2014/11/03/manually-removing-angular-directives.html
Code:
const linker = function (scope, element) {
scope.$watch('data', function () {
reRenderToolbar();
}, true);
let childScope;
const reRenderToolbar = function () {
if(childScope) {
childScope.$destroy();
}
element[0].innerHTML = '';
_.forEach(scope.data, function (item, key) {
const directive = item.CustomDirective;
scope.options = item.options || {};
html = '<' + directive + ' options="options"></' + directive + '>';
element.append(html);
}
});
}
childScope = scope.$new()
$compile(element.contents())(childScope);
};
on your custom directive handle the destroy event
directive("CustomDirective", function(){
return {
restrict: 'C',
template: '<div >Custom Directive</div>',
link: function(scope, element){
scope.$on("$destroy",function() {
element.remove();
});
}
}
});
I'm currently trying to get my directive to listen to changes happening to a specific variable in the parent scope. For that purpose I came up with the following code which runs fine the first time the controller gets loaded but is not picking up the change to the variable gridCells in the second part running in the $timeout.
Anyone able to give me a hint what I'm doing wrong here?
Controller:
$scope.gridCells = [];
$scope.gridCells.push({value: '1'});
$scope.gridCells.push({value: '2'});
$scope.gridCells.push({value: '1'});
$scope.gridCells.push({value: '2'});
// this change is not reflected in the directive
$timeout(function() {
console.log('push');
$scope.gridCells.push({value: '2'});
$scope.gridCells.push({value: '1'});
}, 4000);
HTML:
<my-directive cells=gridCells></my-directive>
Directive:
angular.module('myApp').directive('myDirective', [ function() {
return {
restrict: 'E',
scope: {
cells: '='
},
link: function (scope, element, attrs) {
scope.gridCells = [];
scope.$watch(attrs.cells, function(newValue, oldValue) {
console.log('new: ' + newValue);
console.log('old: ' + oldValue);
for(var i = 0; i < scope.cells.length; i++) {
// populate my scope.gridCells variable with some of the content in the UPDATED cells variable
if (scope.cells[i].value === '1') {
gridCells.push(scope.cells[i]);
}
}
});
},
template: '{{gridCells.length}}'
};
}]);
You need to use $watchCollection instead of $watch or do a deepWatch (scope.$watch(prop, func, true)) in order to be able to track the changes in the array that has been 2-way bound. It is also better to watch scope property cells instead of attribute cells
scope.$watchCollection('cells', function() {
//probably you want to reset gridCells here?
//scope.gridCells = [];
for(var i = 0; i < scope.cells.length; i++) {
if (scope.cells[i].value === '1') {
scope.gridCells.push(scope.cells[i]);
}
}
});
Plnkr
I'm making a directive that modifies it's inner html. Code so far:
.directive('autotranslate', function($interpolate) {
return function(scope, element, attr) {
var html = element.html();
debugger;
html = html.replace(/\[\[(\w+)\]\]/g, function(_, text) {
return '<span translate="' + text + '"></span>';
});
element.html(html);
}
})
It works, except that the inner html is not evaluated by angular. I want to trigger a revaluation of element's subtree. Is there a way to do that?
Thanks :)
You have to $compile your inner html like
.directive('autotranslate', function($interpolate, $compile) {
return function(scope, element, attr) {
var html = element.html();
debugger;
html = html.replace(/\[\[(\w+)\]\]/g, function(_, text) {
return '<span translate="' + text + '"></span>';
});
element.html(html);
$compile(element.contents())(scope); //<---- recompilation
}
})
Here's a more generic method I developed to solve this problem:
angular.module('kcd.directives').directive('kcdRecompile', function($compile, $parse) {
'use strict';
return {
scope: true, // required to be able to clear watchers safely
compile: function(el) {
var template = getElementAsHtml(el);
return function link(scope, $el, attrs) {
var stopWatching = scope.$parent.$watch(attrs.kcdRecompile, function(_new, _old) {
var useBoolean = attrs.hasOwnProperty('useBoolean');
if ((useBoolean && (!_new || _new === 'false')) || (!useBoolean && (!_new || _new === _old))) {
return;
}
// reset kcdRecompile to false if we're using a boolean
if (useBoolean) {
$parse(attrs.kcdRecompile).assign(scope.$parent, false);
}
// recompile
var newEl = $compile(template)(scope.$parent);
$el.replaceWith(newEl);
// Destroy old scope, reassign new scope.
stopWatching();
scope.$destroy();
});
};
}
};
function getElementAsHtml(el) {
return angular.element('<a></a>').append(el.clone()).html();
}
});
You use it like so:
HTML
<div kcd-recompile="recompile.things" use-boolean>
<div ng-repeat="thing in ::things">
<img ng-src="{{::thing.getImage()}}">
<span>{{::thing.name}}</span>
</div>
</div>
JavaScript
$scope.recompile = { things: false };
$scope.$on('things.changed', function() { // or some other notification mechanism that you need to recompile...
$scope.recompile.things = true;
});
Edit
If you're looking at this, I would seriously recommend looking at the website's version as that is likely to be more up to date.
This turned out to work even better than #Reza's solution
.directive('autotranslate', function() {
return {
compile: function(element, attrs) {
var html = element.html();
html = html.replace(/\[\[(\w+)\]\]/g, function(_, text) {
return '<span translate="' + text + '"></span>';
});
element.html(html);
}
};
})
Reza's code work when scope is the scope for all of it child elements. However, if there's an ng-controller or something in one of the childnodes of this directive, the scope variables aren't found. However, with this solution ^, it just works!
LIVE DEMO
Consider the following spinner-click directive:
Directive Use:
<button class="btn btn-mini"
ng-class="{'btn-warning': person.active, disabled: !person.active}"
spinner-click="deleteItem($index)"
spinner-text="Please wait..."
spinner-errors="alerts">
Delete
</button>
Directive:
app.directive('spinnerClick', function() {
return {
restrict: 'A',
scope: true,
link: function(scope, element, attrs) {
var originalHTML = element.html();
var spinnerHTML = "<i class='icon-refresh icon-spin'></i> " + attrs.spinnerText;
element.click(function() {
if (element.is('.disabled')) {
return;
}
element.html(spinnerHTML).addClass('disabled');
scope.$apply(attrs.spinnerClick).then(function() {
element.html(originalHTML).removeClass('disabled');
}, function(errors) {
element.html(originalHTML).removeClass('disabled');
// This is ugly! Is there a better way?
var e = scope[attrs.spinnerErrors];
e.length = 0;
e.push.apply(e, errors);
});
});
}
};
});
Controller:
app.controller('MainCtrl', function($scope, $q, $timeout) {
$scope.alerts = ['First alert'];
$scope.people = [
{ name: 'David', active: true },
{ name: 'Layla', active: false }
];
$scope.deleteItem = function(index) {
var defer = $q.defer();
$timeout(function() {
defer.reject(["Something 'bad' happened.", "Check your logs."]);
}, 2000);
return defer.promise;
};
});
Note: spinner-click can be used with other directives (e.g. ng-class in this example).
As you can see, I set $scope.alerts in the directive using a very nasty way. Can you find a better way to do this?
UPDATE: (DEMO)
I tried to use $parse like this:
var errorsModel = $parse(attrs.spinnerErrors);
errorsModel.assign(scope, errors);
and this doesn't work.
BUT, if I have spinner-errors="wrapper.alerts" rather than spinner-errors="alerts", it does work!
Is there a way to avoid using the wrapper?
I think you can do it more simply using an isolate scope.
Instead of scope: true,, you should put:
scope:{
spinnerClick:"&",
spinnerText : "#",
spinnerErrors: "="
}
And then, in your directive use scope.spinnerClick, scope.spinnerText , scope.spinnerErrors directly.
The & is used to bind function expression defined in your attribute and pass it to your directive's scope, the # will bind the text value of the attribute and the = will set a double binding with the expression passed in the attribute.
You can fine a more precise explanation here http://docs.angularjs.org/guide/directive (look at the long version), and a much clearer explanation here http://www.egghead.io/ (look at the isolate scope videos, it only takes a few minutes and makes it look so simple).
To answer your original question regarding the ugliness of
// This is ugly! Is there a better way?
var e = scope[attrs.spinnerErrors];
e.length = 0;
e.push.apply(e, errors);
You can use angular.copy to achieve the same results
angular.copy(errors, scope[attrs.spinnerErrors]);
The reason this is so ugly in your directive is due to your use of a child scope. If you did not create a child scope, or were willing to create an isolation scope this would not be a problem. You can't use $scope.alerts because
The child scope gets its own property that hides/shadows the parent
property of the same name. Your workarounds are
define objects in the parent for your model, then reference a property of that object in the child: parentObj.someProp
use $parent.parentScopeProperty (not always possible, but easier than 1. where possible)
define a function on the parent scope, and call it from the child (not always possible)
There is a detailed explanation that can be found here.
One option is to create a setter function in the controller that you can call in the directive. The reference to the function in the child scope could then be used set the value in the parent scope. Another option is to create an isolation scope and then pass in a setter function using & binding.
You had the right idea with $parse. The problem is that you're assigning the new array of errors to the child scope, which hides (but doesn't replace) the array on the parent/controller scope.
What you have to do is get a reference to the parent array and then replace the contents (like you were doing in the original version). See here.
I question the need to put the error logic within the directive. You can simply handle the error as part of the controller. Unless you absolutely need to have the html replaced and class removed before manipulating the alerts array, your code could be rewritten as:
app.directive('spinnerClick', function() {
return {
restrict: 'A',
scope: true,
link: function(scope, element, attrs) {
var originalHTML = element.html();
var spinnerHTML = "<i class='icon-refresh icon-spin'></i> " + attrs.spinnerText;
function onClickDone(){
element.html(originalHTML).removeClass('disabled');
}
element.click(function() {
if (element.is('.disabled')) {
return;
}
element.html(spinnerHTML).addClass('disabled');
scope.$apply(attrs.spinnerClick).then(onClickDone, onClickDone);
});
}
};
});
app.controller('MainCtrl', function($scope, $q, $timeout) {
$scope.alerts = ['First alert'];
$scope.people = [
{ name: 'David', active: true },
{ name: 'Layla', active: false }
];
$scope.deleteItem = function(index) {
var defer = $q.defer();
$timeout(function() {
defer.reject(["Something 'bad' happened.", "Check your logs."]);
}, 2000);
return defer.promise.then(function(){
//Success handler
}, function(error){
$scope.alerts.length = 0;
$scope.alerts.push(error);
});
};
});
I am implementing a simple directive that represents a form field with all its extras like label, error field, regex all in a single line.
The directive is as follow:
<div ng-controller="parentController">
{{username}}
<!-- the directive -- >
<form-field label="Username:" regex="someRegex" constrainsViolationMessage="someValidationMessage" model="username" place-holder="some input value">
</form-field>
</div>
Now, I want to test the data binding between the directive scope and the parent scope.
The test is:
it("should bind input field to the scope variable provided by parent scope ! ", function () {
var formInput = ele.find('.form-input');
formInput.val("some input");
expect(ele.find('p').text()).toEqual('some input');
});
This problem is that I don't know why test don't pass, even the directive works correctly. Here is a fiddle of the directive.
And here is the whole test and test set up.
var formsModule = angular.module('forms', []);
formsModule.controller('parentController', function ($scope) {
});
formsModule.directive('formField', function () {
var label;
var constrainsViolationMessage;
var placeHolder;
var model;
return {
restrict:'E',
transclude:true,
replace:false,
scope:{
model:'='
},
link:function (scope, element, attr) {
console.log("link function is executed .... ");
scope.$watch('formInput', function (newValue, oldValue) {
console.log("watch function is executed .... !")
scope.model = newValue;
});
scope.label = attr.label;
},
template:'<div class="control-group ">' +
'<div class="form-label control-label">{{label}}</div> ' +
'<div class="controls controls-row"> ' +
'<input type="text" size="15" class="form-input input-medium" ng-model="formInput" placeholder="{{placeHolder}}">' +
'<label class="error" ng-show={{hasViolationConstrain}}>{{constrainsViolationMessage}}</label>' +
'</div>'
}
});
beforeEach(module('forms'));
var ele;
var linkingFunction;
var elementBody;
var scope;
var text = "";
var placeHolder = "filed place holder";
var label = "someLabel";
var regex = "^[a-z]{5}$";
beforeEach(inject(function ($compile, $rootScope) {
scope = $rootScope;
elementBody = angular.element('<div ng-controller="parentController">' +
'<p>{{username}}</p>' +
'<form-field label="Username:" regex="someRegex" constrainsViolationMessage="someValidationMessage" model="username" place-holder="some input value"> </form-field>');
ele = $compile(elementBody)(scope);
scope.$digest();
}
));
afterEach(function () {
scope.$destroy();
});
iit("should bind input field to the scope variable provided by parent scope ! ", function () {
var formInput = ele.find('.form-input');
formInput.val("some input");
expect(ele.find('p').text()).toEqual('some input');
});
As you can see, I want to assert that form input is reflected in the scope variable set in the 'model' attribute provided by the parent scope.
Am I missing something here ?
Thanks for helping me ... !
You're missing the scope.$apply() call after you set the input value, so the change is never getting digested. In the regular application lifecycle this would happen automatically, but you have to manually trigger this in your tests
Take a look at https://github.com/angular/angular.js/blob/master/test/ng/directive/formSpec.js for a ton of examples.
Use $scope.$digest() after adding condition to execute watch. It will fire watch.