Angularjs 1.2.6: watch directive's parent scope collection - javascript

I'm building custom tree directive:
<ul tree="treeOptions">
<li>{{ item.code + ' - ' + item.name }}</li>
</ul>
In javascript:
$scope.myItems = [];
$scope.treeOptions = {
data: 'myItems',
...
}
In directive:
(function (angular) {
'use strict';
angular.module('tree', []).
directive('tree', ['$compile', '$document', function ($compile,
$document) {
return {
restrict: 'A',
scope: { treeOptions: '=tree' }, //Isolated scope
compile: function (elem, attrs) {
//...
return function (scope, elem, attrs) {
//...
scope.$parent.$watchCollection(scope.treeOptions.data,
function (newItems, oldItems) {
var addedItems = _.difference(newItems, oldItems);
var removedItems = _.difference(oldItems, newItems);
//but newItems and oldItems always the same
//...
}
);
}
};
}
};
} ]);
})(angular);
I'm using lodash ( _ ) to find differences between new and old items.
The problem is newItems and oldItems are always the same, even after new items are pushed to parent scope's myItems array. What am I missing?

So, this is definitely an issue in the angular framework. I'm sure they will get around to fixing it sooner or later, but in the mean time if you need to get your code to work I was able to put together a sample that works quite well. The core is to not use the default old/new elements:
var oldWorkingItems = scope.$parent[attrs.testDirective].slice(0);
scope.$parent.$watchCollection(attrs.testDirective,
function (newItems, oldItems) {
console.log('NEW Items:' + newItems);
console.log('Old Items:' + oldWorkingItems);
For the full example as well as my reproduction of the error, see the following Plunkr: http://plnkr.co/edit/R9hQpRZqrAQoCPdQu3ea?p=preview. By the way, the reason this is called so many times is because it is inside an ng-repeat, but that was my way to force the use of "$parent". Anyways, hope this helps some!
Edit - It really annoyed me how many times the directive was being run in the ng-repeat so I wrote another plunker (http://plnkr.co/edit/R9hQpRZqrAQoCPdQu3ea?p=preview) that uses a single ng-repeat:
<div ng-repeat="element in [1]">
<div test-directive="testCollection"></div>
</div>
This only calls the directive twice (why twice, I'm still not sure).

As Gruff Bunny pointed out in the comments, this is an open issue with AngularJS up to the current version (1.2.13). The workaround for now is to use $watch( , true) or do as drew_w suggested.

Related

How to dynamically bound value to a link using AngularJS

The problem that I need to generate link on the fly since the link is set in ng-repeat. I think I need to execute custom function inside ng-repeat loop which gets data from $http and pushes link to $scope.array. Then bound href to $scope.array[someIndex]....The problem I don't know if:
it's the only way
a good design
how to implement it
Example:
HTML
<div ng-repeat-start="item in items">
the link
// here execute $scope.getUrl(item ) somehow
<div class="extra-div">
<div ng-repeat-end=""></div>
Controller:
$scope.arrayOfUrls= [];
$scope.getUrl = function(url){
$http.get(url).then(
function(data){
arrayOfUrls.push(data.link);
}
)
}
How to execute getUrl during ng-repeat cycle?
PS. I cannot bound href directly to getUrl function since there is $http which eventually result in infinite digest loop.
Also promises can be returned not in order so expecting that first call to getUrl will push link to $scope.arrayOfUrls[0] is false assumption.
UPDATE:
As #Claies suggested I trie to prefetch links like this:
Contoller executes $scope.loadFeed();
$scope.loadFeed = function() {
http.jsonp('feed url').then(function(res) {
$scope.feeds = res.data.responseData.feed.entries;
$scope.feeds.forEach(function(e) {
// prefetch content and links for each feed
//hook new entryStateUrl property to feed objects
e['entryStateUrl'] = $scope.getEntryStateUrl(e.link); // e['entryStateUrl'] is undefined
})
})
}
}
$scope.getEntryStateUrl = function(inputUrl) {
$http.get(inputUrl).then(function(data) {
// do stuff
return data.link;
});
}
}
Now seems like I am trying pre-fetch urls but getting undefined for e['entryStateUrl']...
The problem maybe about assigning scope variable when $http is not done getting results... Also it seems like there are nested promises: $http.jsonp and inside it $http.get.
How to fix it?
As this requires UI enhancement, a directive would be a good approach. How about a directive like this ( JSFiddle here ). Please note that I am calling $window.open here - you can replace this with whatever the application requires. :-
todoApp.directive('todoLinks', ['$window',function ($window) {
var directive = {};
directive.restrict = 'A';
directive.transclude = 'true';
directive.scope = { ngModel: '=ngModel', jsOnClick:'&' };
directive.template = '<li ng-repeat="item in ngModel">{{item.name}}</li>';
directive.link = function ($scope, element, attributes) {
$scope.openLink = function (idx) {
$window.open($scope.ngModel[idx].link); //Replace this with what your app. requires
if (attributes.jsOnClick) {
//console.log('trigger post jsOnClick');
$scope.jsOnClick({ 'idx': idx });
}
};
};
return directive;
}]);
When the controller fills the todo items like this:-
todoApp.controller("ToDoCtrl", ['$scope','$timeout','dbService',function($scope, $timeout, dbService)
{
$scope.todo=[{"name":"google","link":"http://www.google.com"},{"name":"bing","link":"http://www.bing.com"},{"name":"altavista","link":"http://www.altavista.com"}];
}]);
Usage of this directive is simple:-
<div todo-links ng-model="todo"></div>

AngularJS : databinding in isolated scope of directive, using an object?

I have a quite hard time to build a (maybe non-trivial) directive for an SPA. Basically I need to be able to pour data into the directive from any controller and the directive is supposed to show a live graph.
I've read this post and I would like to use a shared object on the isolated scope of this directive anyway.
So I tried to do smth like this:
Wrapping template:
<div ng-controller="WrappingCtrl">
<timeline-chart d3API="d3API"><timeline-chart>
</div>
In the 'wrapping' controller:
$scope.d3API = {};
$scope.d3API.options = {}; //for d3Config
$scope.d3API.currentValue = 3; //asynchronous!!!
Finally to use the shared object d3API in the directive's link method I tried e.g. this:
//in the directive:
scope: { //nice, but does it help??
d3API: '='
}
and:
var data = [1, 2];
var updateTimeAxis = function() {
var newValue;
if (data.length) {
newValue = (data[data.length - 1] !== scope.d3API.currentValue) ? scope.d3API.currentValue : data[data.length - 1];
data.push(newValue);
} else {
console.warn('problem in updateTimeAxis: no data length');
}
};
To gain some simplicity for this question I've created a fiddle, note, that none of both are working:
http://jsfiddle.net/MalteFab/rp55vjc8/3/
http://jsfiddle.net/MalteFab/rp55vjc8/5/
The value in the directive's template is not updated - what am I doing wrong? Any help is appreciated.
Your fiddle mostly works, you just need to update your controller to use $timeout:
app.controller('anyCtrl', function($scope, $timeout) {
show('anyCtrl');
$scope.bound = {};
$timeout(function() {
$scope.bound.says = 'hello';
}, 200);
});
Forked fiddle: http://jsfiddle.net/wvt1f1zt/
Otherwise no digest occurs so angular doesn't know something changed. Based on what you're actual problem is, I'm assuming you're not using timeout vs $timeout, but if your coding style is to intermix angular with "normal" javascript, you may be running into the same kind of scenario.
A good article for reference for telling angular about what your doing is here: http://jimhoskins.com/2012/12/17/angularjs-and-apply.html

Bidirectional Binding not updating within ng-repeat

I'm having trouble with a bi-directional binding in an ng-repeat. I would expect for the below $watch to be triggered when you select a color from the list.
$scope.$watch('favoriteColors', function (newValue) {
console.log('example-favoriteColors', newValue);
});
I would expect for Orange to appear in $scope.favoriteColors when checked.
Example: http://plnkr.co/edit/k5SEQw4XFnxriD2I8ZG7?p=preview
directive('checkBox', function () {
return {
replace: true,
restrict: 'E',
//require: '^ngModel',
scope: {
'externalValue': '=ngModel',
'value': '&'
},
template: function (el, attrs) {
var html =
'<div class="ngCheckBox">'+
'<span ng-class="{checked: isChecked}">' +
'<input type="checkbox" ng-model="isChecked"/>'+
'</span>'+
'</div>';
return html;
},
controller: ['$scope', '$timeout', function ($scope, $timeout) {
var initialized = false;
console.log($scope.value());
if (angular.isArray($scope.externalValue)) {
$scope.isChecked = $scope.externalValue.indexOf($scope.value()) > 0;
} else {
$scope.isChecked = !!$scope.externalValue;
}
$scope.$watch('isChecked', function (newValue) {
if (angular.isDefined(newValue)) {
//add or remove items if this is an array
if (angular.isArray($scope.externalValue)) {
var index = $scope.externalValue.indexOf($scope.value());
if(index > -1) {
$scope.externalValue.splice(index, 1);
} else if (initialized) {
$scope.externalValue.push($scope.value());
}
} else {
//simple boolean value
$scope.externalValue = newValue;
}
if (initialized)
console.log($scope.externalValue);
}
});
$timeout(function () {
initialized = true;
});
}],
link: function (scope, el, attrs) {
}
};
});
Please check out this plunk: http://plnkr.co/edit/pbHz4ohBPi7iYq6uJI8X?p=preview
There were lots of changes. Some of them are:
The template needs not be a function, since it is static.
The initialized (and consequently the $timeout) is not needed.
I implemented my own indexOf function; there is a chance the objects are not the same in == sense, but equals in the x.name === y.name sense; (I have some doubts about this though)
The add or remove items if this is an array part was wrong; you need to update the array based on the value of isChecked, not based on whether the item already exists in the array (indexOf).
Initialize favoriteColors as an array, not as a single object, to be consistent, i.e. $scope.favoriteColors = [$scope.colors[1]];
(minor) Added a little more descriptive log when favoriteColors change.
Use $watch("favoriteColors", function() {...}, true) to watch for changes inside the array (not the true last argument).
I think it's because you need to be referencing a property on an object instead of the flat array. When you pass a primitive data structure like an array, it gets passed by reference and thus the updates aren't passed along properly. (Post by Mark Rajcok.)
I went ahead and showed this by hacking your plunkr a little bit. I changed $scope.favoriteColors = $scope.colors[1]; to $scope.favoriteColors = {value:$scope.colors[1]}; and changed <check-box ng-model="favoriteColors" value="color"> to <check-box ng-model="favoriteColors.value" value="color">.
Plunkr
You can see in the plunkr that when you hit the checkboxes the console.log statements now go off under the $watch function.
I see that you're using angular-form-ui's checkbox directive.
Use $watchCollection (link to documentation) instead of $watch for watching arrays for changes
Initialize $scope.favoriteColors as an array containing the values that should be checked
I've reverted your changes to angular-form-ui.js as those changes broke the directive. They are now exactly as the code appears in the latest commit on Github (checkbox.js). Only one thing has changed, the initialization of the angular-form-ui module by adding [] as the second argument to that first line.
Here is the updated plunker: http://plnkr.co/edit/mlUt46?p=preview

$observe multiple attributes at the same time and fire callback only once

I wonder is it possible to execute some callback only once after evaluation all (or only some) attributes of directive (without isolated scope). Attributes are really great to pass configuration to the directive. The thing is that you can observe each attribute separately and fire callback several times.
In the example we have a directive without isolated scope which observs two attributes: name and surname. After any change action callback is fired:
html
<button ng-click="name='John';surname='Brown'">Change all params</button>
<div person name="{{name}}" surname="{{surname}}"></div>
js
angular.module('app', []).
directive('person', function() {
return {
restrict: 'A',
link: function($scope, $elem, $attrs) {
var action = function() {
$elem.append('name: ' + $attrs.name + '<br/> surname: ' + $attrs.surname+'<br/><br/>');
}
$attrs.$observe('name', action);
$attrs.$observe('surname', action);
}
}
});
Plunker here.
So the effect is that after changing name and surname during one click, action callback is fired twice:
name:
surname: Brown
name: John
surname: Brown
So the question is: can action be fired only once with both name and surname values changed?
You can use $watch to evaluate a custom function rather than a specific model.
i.e.
$scope.$watch(function () {
return [$attrs.name, $attrs.surname];
}, action, true);
That will be run on all $digest cycles, and if $watch detects the return array (or however you want to structure your function's return value) doesn't match the old value, the callback argument to $watch will fire. If you do use an object as the return value though, make sure to leave the true value in for the last argument to $watch so that $watch will do a deep compare.
Underscore (or lo-dash) has a once function. If you wrap your function inside once you can ensure your function will be called only once.
angular.module('app', []).
directive('person', function() {
return {
restrict: 'A',
link: function($scope, $elem, $attrs) {
var action = function() {
$elem.append('name: ' + $attrs.name + '<br/> surname: ' + $attrs.surname+'<br/><br/>');
}
var once = _.once(action);
$attrs.$observe('name', once);
$attrs.$observe('surname', once);
}
}
});
So, I've ended up with my own implementation of observeAll method, which can wait for several changes of attributes during one call stack. It works however I'm not sure about performance.
Solution of #cmw seems to be simpler but performance can suffer for large number of parameters and multiple $digest phase runs, when object equality is evaluated many many times. However I decided to accept his answer.
Below you can find my approach:
angular.module('utils.observeAll', []).
factory('observeAll', ['$rootScope', function($rootScope) {
return function($attrs, callback) {
var o = {},
callQueued = false,
args = arguments,
observe = function(attr) {
$attrs.$observe(attr, function(value) {
o[attr] = value;
if (!callQueued) {
callQueued = true;
$rootScope.$evalAsync(function() {
var argArr = [];
for(var i = 2, max = args.length; i < max; i++) {
var attr = args[i];
argArr.push(o[attr]);
}
callback.apply(null, argArr);
callQueued = false;
});
}
});
};
for(var i = 2, max = args.length; i < max; i++) {
var attr = args[i];
if ($attrs.$attr[attr])
observe(attr);
}
};
}]);
And you can use it in your directive:
angular.module('app', ['utils.observeAll']).
directive('person', ['observeAll', function(observeAll) {
return {
restrict: 'A',
link: function($scope, $elem, $attrs) {
var action = function() {
$elem.append('name: ' + $attrs.name + '<br/> surname: ' + $attrs.surname+'<br/><br/>');
}
observeAll($attrs, action, 'name', 'surname');
}
}
}]);
Plunker here
I resolved the exact same problem that I had using another approach, though I was looking for different ideas. While cmw's suggestions is working, I compared its performance against mine, and saw that the $watch method is called far too many times, so I decided to keep things the way I had implemented.
I added $observe calls for both variables I wanted to track and bound them to a debounce call. Since they both are modified with very little time difference, both $observe methods trigger the same function call, which gets executed after a short delay:
var debounceUpdate = _.debounce(function () {
setMinAndMaxValue(attrs['minFieldName'], attrs['maxFieldName']);
}, 100);
attrs.$observe('minFieldName', function () {
debounceUpdate();
});
attrs.$observe('maxFieldName', function () {
debounceUpdate();
});
There are several ways presented to solve this problem. I liked the debounce solution a lot. However, here is my solution to this problem. This combines all the attributes in one single attribute and creates a JSON representation of the attributes that you are interested in. Now, you just need to $observe one attribute and have good perf too!
Here is a fork of the original plunkr with the implementation:
linkhttp://plnkr.co/edit/un3iPL2dfmSn1QJ4zWjQ

How to Create recursive Angular.js Templates without isolating scope?

I have a recursive data structure I am trying to represent in Angular.js. a simplified demo is available here:
http://plnkr.co/edit/vsUHLYMfI4okbiVlCK7O?p=preview
In the Preview, I have the following HTML for a recursive object:
<ul>
<li ng-repeat="person in people">
<span ng-click="updateClicks(person)">{{person.name}}</span>
<ul>
<li ng-repeat="kid in person.kids">
<span ng-click="updateClicks(kid)">{{kid.name}}</span>
</li>
</ul>
</li>
</ul>
In my application, the view is much more complex. I would like to have a way to generate the template html for each person in a recursive fashion. I tried doing this with a directive, however I ran into issues with infinite loops when I did not isolate the scope. And when I did isolate the scope, I was no longer able to call functions that are tied to the controller (in this example, the updateClicks function, however in my application there are several).
How can I generate html for these objects recursively, and still be able to call functions belonging to a controller?
I think the best way to do this is with an $emit.
Let's say your recursive directive looks like this:
directive('person', function($compile){
return{
restrict: 'A',
link: function(scope, element, attributes){
//recursive bit, if we've got kids, compile & append them on
if(scope.person.kids && angular.isArray(scope.person.kids)) {
$compile('<ul><li ng-repeat="kid in person.kids" person="kid"></li></ul>')(scope, function(cloned, scope){
element.find('li').append(cloned);
});
}
},
scope:{
person:'='
},
template: '<li><span ng-click="$emit(\'clicked\', person)">{{person.name}}</span></li>'
}
});
notice the ng-click="$emit(clicked, person)" code, don't be distracted the \, that's just there to escape. $scope.$emit will send an event all the way up your scope chain, so that in your controller, your clicked function stays mostly unchanged, but now instead of being triggered by ng-click, you're listening for the event.
$scope.$on('clicked', function(event, person){
person.clicks++;
alert(person.name + ' has ' + person.clicks + ' clicks!');
});
cool thing is that the event object even has the isolated scopes from your recursed directives.
Here's the fully working plnkr: http://plnkr.co/edit/3z8OXOeB5FhWp9XAW58G?p=preview
even went down to tertiary level to make sure recursion was working.
Recursive tree with angular directive without scope isolation, forces you to simulate isolation by using different scope properties per depth level.
I didn't find any so I wrote my own.
Let's say your HTML is :
<body ng-app="App" ng-controller="AppCtrl">
<div test="tree.children" test-label="tree.label">{{b}}</div>
</body>
Then you have a main module and a controller adding a tree to the scope :
var App = angular.module('App', []);
App.controller('AppCtrl', function($scope, $timeout) {
// prodive a simple tree
$scope.tree = {
label: 'A',
children: [
{
label: 'a',
children: [
{ label: '1' },
{ label: '2' }
]
},
{
label: 'b',
children: [
{ label: '1' },
{ label: '2' }
]
}
]
};
// test that pushing a child in the tree is ok
$timeout(function() {
$scope.tree.children[1].children.push({label: 'c'});
},2000);
$timeout(function() {
// test that changing a label is ok
$scope.tree.children[1].label = 'newLabel';
},4000);
});
Finally consider the following implementation of the directive test :
App.directive('test', function($compile) {
// use an int to suffix scope properties
// so that inheritance does not cause infinite loops anymore
var inc = 0;
return {
restrict: 'A',
compile: function(element, attr) {
// prepare property names
var prop = 'test'+(++inc),
childrenProp = 'children_'+prop,
labelProp = 'label'+prop,
childProp = 'child_'+prop;
return function(scope, element, attr) {
// create a child scope
var childScope = scope.$new();
function observeParams() {
// eval attributes in current scope
// and generate html depending on the type
var iTest = scope.$eval(attr.test),
iLabel = scope.$eval(attr.testLabel),
html = typeof iTest === 'object' ?
'<div>{{'+labelProp+'}}<ul><li ng-repeat="'+childProp+' in '+childrenProp+'"><div test="'+childProp+'.children" test-label="'+childProp+'.label">{{'+childProp+'}}</div></li></ul></div>'
: '<div>{{'+labelProp+'}}</div>';
// set scope values and references
childScope[childrenProp]= iTest;
childScope[labelProp]= iLabel;
// fill html
element.html(html);
// compile the new content againts child scope
$compile(element.contents())(childScope);
}
// set watchers
scope.$watch(attr.test, observeParams);
scope.$watch(attr.testLabel, observeParams);
};
}
};
});
All the explanations are in the comments.
You may have a look at the JSBin.
My implementation can of course be improved.

Categories

Resources