ngRepeat track by: How to add event hook on model change? - javascript

I have a simple ngRepeat like the following:
<some-element ng-repeat="singleRecord in arrayOfRecords track by singleRecord.id">
<!-- stuff -->
</some-element>
arrayOfRecords is updated from a server and may contain new data.
ngRepeat's track by feature can figure out when a new element is added to the array and automatically updates the DOM without changing the existing elements. I would like to hook into that code and execute a callback function when there's new data coming in or old data is removed. Is it possible to easily do this via Angular?
From what I understand, there's a $$watchers which triggers callbacks whenever there's changes to certain variables, but I don't know how to go about hacking that. Is this the right direction?
NOTE: I know I can manually save the arrayOfRecords and compare it with the new values when I fetch them to see what changed. However, since Angular already offers a track by feature which has this logic, it would be nice if I can have Angular automatically trigger an event callback when an element is added or removed from the array. It doesn't make sense to duplicate this logic which already exists in Angular.

Probably you could create a directive and add it along with ng-repeat, so the directive when created(when item is added by ng-repeat) will emit an event and similarly when the item is destroyed it will emit another event.
A simple implementation here:
.directive('tracker', function(){
return{
restrict:'A',
link:function(scope, el, attr){
scope.$emit('ITEM_ADDED', scope.$eval(attr.tracker))
scope.$on('$destroy', function(){
scope.$emit('ITEM_REMOVED', scope.$eval(attr.tracker))
});
}
}
});
and use it as:
<some-element
ng-repeat="singleRecord in arrayOfRecords track by singleRecord.id"
tracker="item">
and listen for these events at the parent controller for example.
Demo
Or using function binding but in a different way, without using isolate scope for that.
.directive('tracker', function() {
return {
restrict: 'A',
link: function(scope, el, attr) {
var setter = scope.$eval(attr.tracker);
if(!angular.isFunction(setter)) return;
setter({status:'ADDED', item:scope.$eval(attr.trackerItem)});
scope.$on('$destroy', function() {
setter({status:'REMOVED', item:scope.$eval(attr.trackerItem)});
})
}
}
});
Demo
The one above was specific to your question since there is no other built in way, Note that if you were to really find out the items added/removed, you could as well do it in your controller by diffing the 2 lists. You could try use lodash api like _.unique or even simple loop comparisons to find the results.
function findDif(oldList,newList){
return {added:_.uniq(newList, oldList), removed:_.uniq(oldList, newList)};
}
Demo

You can change it to:
<div ng-model="arrayOfRecords">
<some-element ng-repeat="singleRecord in arrayOfRecords track by singleRecord.id">
<!-- stuff -->
</some-element>
</div>
The model will change as soon as arrayOfRecords will change.

Related

Angular directives that call methods on child directives

I am looking for advice on how to implement a hierarchical structure in Angular, where a directive (<partition>) can call a method on a child directive's controller (<property-value>).
I have put together a detailed example here:
https://jsfiddle.net/95kjjxkh/1/
As you can see, my code contains an outer directive, <partition>, which displays one or more <property-value> directives within.
The <property-value> directive offers an editing method, editItem(), which allows the user to change the value of a single entry. (To keep my example short, I simply assign a random number here, but in my production app, a modal will appear, to query the user for a new value.)
This works fine. However, in the outer directive, <partition>, I would like to add the ability to create a new, blank <property-value> directive and then immediately call its editing method so that the user can enter an initial value. If no initial value is entered, the new item would be discarded.
I have seen examples of inner directives calling methods on enclosing directives, but not the other way around.
Is there a way to do this? Alternatively, is there a better way for me to build this kind of view?
You can always use $broadcast to talk both ways. To your parent as well as to your childrens.
In your Child controller you can do the following
app.directive('propertyValue', function() {
return {
require : '^partition'
restrict: 'E',
scope: {
item: '='
},
with this you will get the parent controller in child directive's link function like this
link:function(scope,element,attrs,partitionCtrl){
partitionCtrl.getChildCtrl(element)
}
in partition controller create getChildCtrl function and with that call "propertyvalue" controller function
controller: function ($scope, ItemFactory) {
// your code
var propValueCtrl =undefined;
this.getChildCtrl =function(elem)
{
propValueCtrl = elem.controller();
}
this.callChildFunction = function()
{
propValueCtrl.Edit();// whatever is the name of function
}
call this function when needed in property link function.
Hope this helps.

AngularJs Directive: How to dynamically set attributes on ng-repeat element

I am relatively new to AngularJS. While venturing into directive creation, I can across this problem: How to dynamically add / remove attributes on the children of the directive's element when these children are dynamically added with 'ng-repeat'?
First, I thought of this solution:
template
...
a.list-group-item(ng-repeat='playlist in playlists', ng-click='addToPlaylist(playlist, track)', ng-href='playlist/{{ playlist._id }})
...
*directive
link: function(scope, elm, attrs) {
var listItems = angular.element(element[0].getElementsByClassName('list-group-item')
angular.forEach(listItems, function(item, index) {
'add' in attrs ? item.removeAttr('href') : item.removeAttr('ng-click');
listItems[index] = item;
}
...
Result
It turns out, my code never enters this angular.forEach loop because listItems is empty. I suppose it's because the ng-repeat is waiting for the scope.playlists to populate with the data from a async call to a server via $resource.
temporary fix
in the directive definition, I added a boolean variable that checks for the presence of 'add' in the element's attributes: var adding = 'add' in attrs ? true : false;
And then in the template,
a.list-group-item(ng-if='adding', ng-repeat='playlist in playlists', ng-click='addToPlaylist(playlist, track)')
a.list-group-item(ng-if='!adding', ng-repeat='playlist in playlists', ng-href='playlist/{{playlist._id }}')
While it works fine, it is obviously not DRY at all. HELP!
Instead of removing attributes, change your click handler.
Add $event to the list of arguments and conditionally use preventDefault().
<a ng-click='addToPlaylist($event,playlist)' ng-href='playlist'>CLICK ME</a>
In your controller:
$scope.addToPlaylist = function(event,playlist) {
if (!$scope.adding) return;
//otherwise
event.preventDefault();
//do add operation
};
When not adding, the function returns and the href is fetched. Otherwise the default is prevented and the click handler does the add operation.
From the Docs:
$event
Directives like ngClick and ngFocus expose a $event object within the scope of that expression. The object is an instance of a jQuery Event Object when jQuery is present or a similar jqLite object.
-- AngularJS Developer Guide -- $event
The way that you are trying to do things may not be the most Angularish (Angularist? Angularyist?) way. When using angular.element() to select child elements as you are trying to do here, you can make sure the child elements are ready as follows:
link: function(scope, elm, attrs) {
elm.ready(function() {
var listItems = angular.element(element[0].getElementsByClassName('list-group-item')
angular.forEach(listItems, function(item, index) {
'add' in attrs ? item.removeAttr('href') : item.removeAttr('ng-click');
listItems[index] = item;
}
});
}
However, this is unlikely to work in your situation, as #charlietfl points out below. If you want to avoid the solution you already have (which I think is better than your first attempt), you will have to reimplement your code altogether.
I would suggest defining an additional directive that communicates with its parent directive using the require property of the directive definition object. The new directive would have access to an add property of the parent (this.add in the parent directive's controller) and could be programmed to behave accordingly. The implementation of that solution is beyond the scope of this answer.
Update:
I decided to give the implementation something of a shot. The example is highly simplified, but it does what you are trying to do: alter the template of a directive based on the attributed passed to it. See the example here.
The example uses a new feature in Angular 1: components. You can read more about injectable templates and components here. Essentially, components allow you to define templates using a function with access to your element and its attributes, like so:
app.component('playlistComponent', {
// We can define out template as a function that returns a string:
template: function($element, $attrs) {
var action = 'add' in $attrs
? 'ng-click="$ctrl.addToPlaylist(playlist, track)"'
: 'ng-href="playlist/{{playlist._id}}"';
return '<a class="list-group-item" ng-repeat="playlist in playlists" ' +
action + '></a>';
},
// Components always use controllers rather than scopes
controller: ['playlistService', function(playlists) {
this.playlists = playlists;
this.addToPlaylist = function(playlist, track) {
// Some logic
};
}]
});

Add AngularJS directive at runtime

I am creating a game where the first thing that needs to happen is some state is loaded in from an external JSON file - the contents of one of my directives are dependent on this data being available - because of this, I would like to delay applying the directive until after the data has loaded. I have written the following:
window.addEventListener('mythdataLoaded', function (e) {
// Don't try to create characters until mythdata has loaded
quest.directive('character', function() {
return {
restrict: 'A',
scope: {
character: '#'
},
controller: 'CharacterCtrl',
templateUrl: 'partials/character.html',
replace: true,
link: function(scope, element) {
$(document).on('click', '#'+scope.character, function () {
$('#'+scope.character+'-popup').fadeToggle();
});
}
};
});
});
// Load in myth data
var myth_data;
$.getJSON("js/mythdata_playtest.json", function(json) {
myth_data = json;
window.dispatchEvent(new Event('mythdataLoaded'));
});
However, it appears that my directive's link function never runs - I'm thinking this is because angular has already executed the part of it's cycle where directives are compiled/linked by the time this directive gets added. Is there some way to force angular to compile this directive after it is created? I googled around a bit, and some people suggested adding $compile to the link function for similar issues - but the link function is never run, so that doesn't work for this case. Thanks!
It seems to me it would be better to always configure the directive, to do the JSON call in the directive, and attach logic to the element in the JSON call's success handler. This would, if I understand you correctly, do what you want.
AngularJS is meant as a framework, not a library, so using it in the way you mentioned is not recommended. Exactly as you mentioned, AngularJS does a lot of things for you when it runs. AngularJS, by default, runs on document loaded, and your $.getJSON callback arrives after that. When AngularJS runs it does all its magic with compiling the content and all that.
As a sidenote, it's also more the Angular way to use $http over $.getJSON.
I think you're thinking about this the wrong way. A major ideology in angular is that you set up declarative elements and let it react to the state of the scope.
What I think you might want to do is pass in what you need through the directive scope, and use other angular built in directives to hide or show your default ("non directive") state until the scope gets set from the controller for example.
Example:
You want a box to be hidden until an api call comes back. Your directive sets special styles on your element (not hidden). Instead of delaying to dynamically set your directive, you can pass in a scope var with a default value and use something like ng-show="data.ready" in your directive template to handle the actual dom stuff.

Angular JS: What is the need of the directive’s link function when we already had directive’s controller with scope?

I need to perform some operations on scope and the template. It seems that I can do that in either the link function or the controller function (since both have access to the scope).
When is it the case when I have to use link function and not the controller?
angular.module('myApp').directive('abc', function($timeout) {
return {
restrict: 'EA',
replace: true,
transclude: true,
scope: true,
link: function(scope, elem, attr) { /* link function */ },
controller: function($scope, $element) { /* controller function */ }
};
}
Also, I understand that link is the non-angular world. So, I can use $watch, $digest and $apply.
What is the significance of the link function, when we already had controller?
After my initial struggle with the link and controller functions and reading quite a lot about them, I think now I have the answer.
First lets understand,
How do angular directives work in a nutshell:
We begin with a template (as a string or loaded to a string)
var templateString = '<div my-directive>{{5 + 10}}</div>';
Now, this templateString is wrapped as an angular element
var el = angular.element(templateString);
With el, now we compile it with $compile to get back the link function.
var l = $compile(el)
Here is what happens,
$compile walks through the whole template and collects all the directives that it recognizes.
All the directives that are discovered are compiled recursively and their link functions are collected.
Then, all the link functions are wrapped in a new link function and returned as l.
Finally, we provide scope function to this l (link) function which further executes the wrapped link functions with this scope and their corresponding elements.
l(scope)
This adds the template as a new node to the DOM and invokes controller which adds its watches to the scope which is shared with the template in DOM.
Comparing compile vs link vs controller :
Every directive is compiled only once and link function is retained for re-use. Therefore, if there's something applicable to all instances of a directive should be performed inside directive's compile function.
Now, after compilation we have link function which is executed while attaching the template to the DOM. So, therefore we perform everything that is specific to every instance of the directive. For eg: attaching events, mutating the template based on scope, etc.
Finally, the controller is meant to be available to be live and reactive while the directive works on the DOM (after getting attached). Therefore:
(1) After setting up the view[V] (i.e. template) with link. $scope is our [M] and $controller is our [C] in M V C
(2) Take advantage the 2-way binding with $scope by setting up watches.
(3) $scope watches are expected to be added in the controller since this is what is watching the template during run-time.
(4) Finally, controller is also used to be able to communicate among related directives. (Like myTabs example in https://docs.angularjs.org/guide/directive)
(5) It's true that we could've done all this in the link function as well but its about separation of concerns.
Therefore, finally we have the following which fits all the pieces perfectly :
Why controllers are needed
The difference between link and controller comes into play when you want to nest directives in your DOM and expose API functions from the parent directive to the nested ones.
From the docs:
Best Practice: use controller when you want to expose an API to other directives. Otherwise use link.
Say you want to have two directives my-form and my-text-input and you want my-text-input directive to appear only inside my-form and nowhere else.
In that case, you will say while defining the directive my-text-input that it requires a controller from the parent DOM element using the require argument, like this: require: '^myForm'. Now the controller from the parent element will be injected into the link function as the fourth argument, following $scope, element, attributes. You can call functions on that controller and communicate with the parent directive.
Moreover, if such a controller is not found, an error will be raised.
Why use link at all
There is no real need to use the link function if one is defining the controller since the $scope is available on the controller. Moreover, while defining both link and controller, one does need to be careful about the order of invocation of the two (controller is executed before).
However, in keeping with the Angular way, most DOM manipulation and 2-way binding using $watchers is usually done in the link function while the API for children and $scope manipulation is done in the controller. This is not a hard and fast rule, but doing so will make the code more modular and help in separation of concerns (controller will maintain the directive state and link function will maintain the DOM + outside bindings).
The controller function/object represents an abstraction model-view-controller (MVC). While there is nothing new to write about MVC, it is still the most significant advanatage of angular: split the concerns into smaller pieces. And that's it, nothing more, so if you need to react on Model changes coming from View the Controller is the right person to do that job.
The story about link function is different, it is coming from different perspective then MVC. And is really essential, once we want to cross the boundaries of a controller/model/view (template).
Let' start with the parameters which are passed into the link function:
function link(scope, element, attrs) {
scope is an Angular scope object.
element is the jqLite-wrapped element that this directive matches.
attrs is an object with the normalized attribute names and their corresponding values.
To put the link into the context, we should mention that all directives are going through this initialization process steps: Compile, Link. An Extract from Brad Green and Shyam Seshadri book Angular JS:
Compile phase (a sister of link, let's mention it here to get a clear picture):
In this phase, Angular walks the DOM to identify all the registered
directives in the template. For each directive, it then transforms the
DOM based on the directive’s rules (template, replace, transclude, and
so on), and calls the compile function if it exists. The result is a
compiled template function,
Link phase:
To make the view dynamic, Angular then runs a link function for each
directive. The link functions typically creates listeners on the DOM
or the model. These listeners keep the view and the model in sync at
all times.
A nice example how to use the link could be found here: Creating Custom Directives. See the example: Creating a Directive that Manipulates the DOM, which inserts a "date-time" into page, refreshed every second.
Just a very short snippet from that rich source above, showing the real manipulation with DOM. There is hooked function to $timeout service, and also it is cleared in its destructor call to avoid memory leaks
.directive('myCurrentTime', function($timeout, dateFilter) {
function link(scope, element, attrs) {
...
// the not MVC job must be done
function updateTime() {
element.text(dateFilter(new Date(), format)); // here we are manipulating the DOM
}
function scheduleUpdate() {
// save the timeoutId for canceling
timeoutId = $timeout(function() {
updateTime(); // update DOM
scheduleUpdate(); // schedule the next update
}, 1000);
}
element.on('$destroy', function() {
$timeout.cancel(timeoutId);
});
...

How to have an angularJS directive on a page called via ajax?

I have the following html (which can be accessed directly or called via ajax):
<section id="content" ng-controller="setTreeDataCtrl" get-subthemes>
<dl ng-repeat="subtheme in allSubthemes">
<dt>{{subtheme.Title}}</dt>
</dl>
Then I'm using the following directive:
myApp.directive('getSubthemes', function() {
return function($scope, element, attrs) {
$scope.allSubthemes = [];
angular.forEach($scope.data.Themes, function(value, key) {
angular.forEach(value.SubThemes, function(value2, key2) {
$scope.allSubthemes.push({
'ThemeTitle': value.Title,
'ThemeUrlSlug': value.UrlSlug,
'Title': value2.Title,
'UrlSlug': value2.UrlSlug
});
});
});
}
});
$scope.allSubthemes seems ok, but the dl's don't get rendered.
I can see for a second everything rendered properly and then it get's back to {{subtheme.Title}}, almost like it's being "unrendered"... any ideas of what I'm doing wrong?
Demo jsFiddle: http://jsfiddle.net/HMp3a/
rGil fixed the jsFiddle. It was missing a ng-app="pddc" declaration on an element so Angular did not know where to begin its magic.
I'd like to mention another way to render to the data in question. I suggest using an ng-repeat within an ng-repeat. See my forked & updated fiddle here. You can actually refer to the parent theme within the ng-repeat of the subtheme, so you don't have to copy values from the parent theme into each subtheme (which effectively eliminates the need for the directive in this example).
Another reason to use a nested ng-repeat is because of async issues that could come up when pulling data from a web service asynchronously. What could happen is when the directive executes, it may not have any data to loop through and populate because the data hasn't arrived yet.
If you use two ng-repeats, Angular will watch the $scope.data and re-run the ng-repeats when the data arrives. I've added a 500 ms delay to setting the data in my example to simulate web service latency and you'll see that even with the "latency", the data eventually renders.
There are two other ways around the async issue:
Use scope.$watch() in your directive, to watch for the data manually, or
Use the "resolve" functionality from Angular's routing feature to make sure the data is retrieved prior to controller execution.
While these alternative methods work, I think both are more complicated then just using two ng-repeats.

Categories

Resources