Angular JS callback after ng-repeat is completed dynamically - javascript

There are tons of posts showing how to have callback functions based on a directive so that when an ng-repeat function is finished you can wait and then call a function. For example here is my example.
<div ng-repeat="Object in Objects" class="objectClass" on-finish-render>{{Object.Overlay}</div>
Then when it is completed the following calls my function
.directive('onFinishRender', function ($timeout) {
return {
restrict: 'A',
link: function (scope, element, attr) {
if (scope.$last === true) {
$timeout(function () {
scope.$emit('ngRepeatFinished');
}, 0);
}
}
}
});
This successfully calls my function below when it is completed
$scope.$on('ngRepeatFinished', function(ngRepeatFinishedEvent) {
//my code goes here
}
Now all of that works perfectly and as intended the first time I set my $scope.Objects variable. My code will wait until all objects are fully rendered and then runs, literally perfect. However if after the initial everything I change $scope.Objects again, my function will still run but it will NOT wait for the completion. Its actually visible with console logs, the first go round it will pause for about half a second after I go into the directive but before the actual emit, but on subsequent changes to my ng-repeat it does not pause and simply calls my function before the dom is finished rendering.
This is super annoying and any help would be great!

One thing you should understand about ng-repeat is that it reuses DOM elements whenever possible, so if you have two objects in your repeater and you add a third, the first two elements will not be re-rendered. Only the third will be rendered, thus your directive's link function will only be called once for the newly-added object.
Similarly, if you remove an item, your directive's link function will not be run again.
Observe the behavior in this JSFiddle.

Angular’s $emit, $broadcast and $on fall under the common “publish/subscribe” design pattern, or can do, in which you’d publish an event and subscribe/unsubscribe to it somewhere else. The Angular event system is brilliant, it makes things flawless and easy to do (as you’d expect!) but the concept behind it isn’t so simple to master and you can often be left wondering why things don’t work as you thought they might.
Using $scope.$emit will fire an event up the $scope. Using
$scope.$broadcast will fire an event down the $scope. Using $scope.$on
is how we listen for these events.
(reference)
I have provided two solution according to your problem.
Solution One
<div ng-controller="MyCtrl">
<div ng-repeat="Object in Objects" class="objectClass" on-finish-render isolated-expression-foo="updateItem(item,temp)">{{Object|json}</div>
</div>
var app = angular.module('app', []);
app.directive('onFinishRender', function () {
return {
restrict: 'A',
scope: {
isolatedExpressionFoo: '&'
},
link: function (scope, element, attr) {
if (scope.$parent.$last) {
scope.isolatedExpressionFoo({ temp: "some value" });
}
}
};
});
app.controller('MyCtrl', ['$scope', function ($scope) {
$scope.Objects = [{ id: 1, value: "test" }, { id: 2, value: "TEst2" }];
$scope.updateItem = function (item, temp) {
console.log("Item param " + item.id);
console.log("temp param " + temp);
}
}]);
Solution Two
<div ng-controller="MyCtrl">
<div ng-repeat="Object in Objects" class="objectClass" on-finish-render>{{Object|json}</div>
</div>
var app = angular.module('app', []);
app.directive('onFinishRender', function ($rootScope) {
return {
restrict: 'A',
link: function (scope, element, attr) {
if (scope.$last) {
$rootScope.$broadcast("ngRepeatFinished", { temp: "some value" });
}
}
};
});
app.controller('MyCtrl', ['$scope', function ($scope) {
$scope.Objects = [{ id: 1, value: "test" }, { id: 2, value: "TEst2" }];
$scope.$on('ngRepeatFinished', function (temp) {
console.log("Item param " + temp);
});
}]);

Related

Update UI based on change of a directive attribute in AngularJs

I am struggling with data binding in AngularJs.
I have the following piece of markup in .html file that includes the custom directive:
<my-directive ng-repeat="i in object" attr-1="{{i.some_variable}}"></my-directive>
Note: 'some-variable' is being updated every 10 seconds(based on the associate collection and passed to template through controller).
The directive's code includes:
myApp.directive('myDirective', function () {
scope: {
'attr-1': '=attr1'
which throws this exception because of the brackets in attr-1(see html code above).
It works though if I use read-only access(note at sign below):
myApp.directive('myDirective', function () {
scope: {
'attr-1': '#attr1'
I use scope.attr-1 in directive's HTML to show its value.
The problem is that with read-only access UI is not reflecting the change in attribute change.
I've found solution with $parse or $eval(couldn't make them work tho). Is there a better one there?
You'll need only two-way binding and I think $parse or $eval is not needed.
Please have a look at the demo below or in this fiddle.
It uses $interval to simulate your updating but the update can also come from other sources e.g. web socket or ajax request.
I'm using controllerAs and bindToController syntax (AngularJs version 1.4 or newer required) but the same is also possible with just an isolated scope. See guide in angular docs.
The $watch in the controller of the directive is only to show how the directive can detect that the data have changed.
angular.module('demoApp', [])
.controller('MainController', MainController)
.directive('myDirective', myDirective);
function MainController($interval) {
var self = this,
refreshTime = 1000; //interval time in ms
activate();
function activate() {
this.data = 0;
$interval(updateView, refreshTime);
}
function updateView() {
self.data = Math.round(Math.random()*100, 0);
}
}
function myDirective() {
return {
restrict: 'E',
scope: {
},
bindToController: {
data: '='
},
template: '<div><p>directive data: {{directiveCtrl.data}}</p></div>',
controller: function($scope) {
$scope.$watch('directiveCtrl.data', function(newValue) {
console.log('data changed', newValue);
});
},
controllerAs: 'directiveCtrl'
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.4.7/angular.js"></script>
<div ng-app="demoApp" ng-controller="MainController as ctrl">
model value in ctrl. {{ctrl.data}}
<my-directive data="ctrl.data"></my-directive>
</div>
I've come to the following solution(in case somebody runs into the the same problem):
// Directive's code
myApp.directive('myDir', function () { return {
restrict: 'E',
templateUrl: function () {
return 'my-dir.html';
},
scope: {
'id': '#arId',
'x': '#arX',
'y': '#arY',
//....
},
link: function ($scope, element, attrs) {
// *** SOLUTION ***
attrs.$observe('arId', function (id) {
$scope.id = id;
});
//...
}
Update: somebody sent me this answer, they have the same problem and came up with a very similar if not exact same solution:
Using a directive inside an ng-repeat, and a mysterious power of scope '#'
It is useful to read because they explain what's the idea behind it.

Bind model to function call return value

I am working on an angular project and I use a directive to create an isolated scope. The directive looks like this:
var directive = module.directive('question', function () {
return {
restrict: 'E',
templateUrl: 'question.html',
transclude: true,
scope: {
quiz: '=quiz'
},
link: function (scope, attr, element) {
scope.$watch(function () {
return scope.quiz;
},
function (oldVal, newVal) {
scope.currentQuestion = scope.quiz;
});
}
};
});
For I do not want to bind to a property (or field) in my Controller, I created a function and call the directive this way:
<question quiz="quiz.getCurrentQuestion()">... (transcluding stuff)</question>
Please note that quiz is my Controller using the as-Syntax.
The way I process the directive is working, but I don't like to create a two-way-binding ( to an R-value?).
Now I tried to just pass the function using &-binding but this just turns out odd results in the link-function and breaks everything.
Can I use the function-binding using & and somehow call the function (in my template or in the link-function) to get the result I need to make it work like two-way-binding?
Thank you for your help.
EDIT
The return value of the getCurrentQuestion-function is an object which looks like
{
questionNumber: 1,
answers: [],
getQuestionText() : function(...),
...
}
So nothing to special, I hope...
EDIT 2
When I use
...
scope: {
quiz: '&quiz'
}
then in the $watch-function I get
function(locals) { return parentGet(scope, locals); } for scope.quiz
And if I call the function like scope.quiz() I get undefined as result.
Couldn't find any way to watch a function in scope binding. However, there are other solutions. If you want single way binding you can use '#', but that means that you would have to parse the JSON in the watch ( working example):
var directive = module.directive('question', function () {
return {
restrict: 'E',
templateUrl: 'question.html',
transclude: true,
scope: {
quiz: '#'
},
link: function (scope, attr, element) {
scope.$watch('quiz', function (newVal, oldVal) {
scope.currentQuestion = angular.fromJson(newVal);
});
}
};
});
It works, but if you have a high rate of updates, the overhead can be annoying. What I would do, is use a service that holds all the questions, and both controller and directive can talk to. When the current question is changed, the controller should pass to the directive only the id of the new question (using simple # bind), and the directive would query the service for the question.

AngularJS directive scope not updating when value changed outside of AngularJS

I am getting started with AngularJS and have a noob problem that I am not sure how to resolve. I am modifying a value outside of angular (I have put it in the .run section only for demonstration purposes), and then attempting to run $apply so that Angular will notice that the scope needs to be updated.
However, in the following code, the {{currentState}} value gets set to "Initial value" and does not ever update to "Second value".
What is the correct approach to get the value to update?
angular.module("exampleApp", [])
.run(function(userNotificationService) {
userNotificationService.setStatus("Initial value");
setTimeout(function() {
userNotificationService.setStatus("Second value");
}, 1000);
})
.factory('userNotificationService', function($rootScope) {
var currentState = 'Unknown state'; // this should never be displayed
return {
setStatus: function(state) {
$rootScope.$apply(function() {
currentState = state;
});
},
getStatus: function() {
return currentState;
}
};
}).directive('currentState', function(userNotificationService) {
return {
restrict: 'AE',
scope: false, // set to false so that directive scope is used for transcluded expressions
link: function(scope) {
scope.currentState = userNotificationService.getStatus();
}
};
}).controller("defaultCtrl", function ($scope) {
// does nothing
});
And the html is the following:
<body ng-controller="defaultCtrl">
<div current-state>
current state: {{ currentState }}
</div>
</body>
If your use-case involves a timer, then Angular provides its own timer service called $interval which wraps the call in a scope.$apply for you. You should use that instead of setTimeout.
Now in this case, since you need a one way binding between a service and a value in your scope, you can set up a $watch in your directive:
.directive('currentState', function(userNotificationService) {
return {
restrict: 'AE',
scope: false, // set to false so that directive scope is used for transcluded expressions
link: function(scope) {
scope.$watch(function () { return userNotificationService.getStatus(); }, function (newVal) {
scope.currentState = userNotificationService.getStatus();
});
}
};
Ideally how you would do it is by creating this one way (or two way) binding in your controller (which you have left empty). The $scope you define on the controller will be available to the directive (if you set $scope: false or $scope: true), and then you can leave the link function empty.

Pass object context back to controller callback from AngularJS Directive

I'm essentially trying to recreate ng-change but add some delay in it (auto-save on change frequency timeout).
So far, I have the following directive:
myApp.directive('changeDelay', ['$timeout', function ($timeout) {
return {
restrict: 'A',
require: 'ngModel',
scope: {
callBack: '=changeDelay'
},
link: function (scope, elem, attrs, ngModel) {
var firstRun = true;
scope.timeoutHandle = null;
scope.$watch(function () {
return ngModel.$modelValue;
}, function (nv, ov) {
console.log(firstRun);
if (!firstRun) {
console.log(nv);
if (scope.timeoutHandle) {
$timeout.cancel($scope.timeoutHandle);
}
scope.timeoutHandle = $timeout(function () {
//How can I pass person??
scope.callBack();
}, 500);
}
firstRun = false;
});
}
};
}]);
With the following controller:
myApp.controller('MyCtrl', ['$scope', function ($scope) {
$scope.people = [{
name: "Matthew",
age: 20
}, {
name: "Mark",
age: 15
}, {
name: "Luke",
age: 30
}, {
name: "John",
age: 42
}];
$scope.updatePerson = function (person) {
//console.log("Fire off request to update:");
//How can I get person here??
//console.log(person);
};
}]);
And this markup should be able to define which controller scope method to call as well as the object that is passed to it:
<div ng-app='myApp'>
<div ng-controller="MyCtrl">
<div ng-repeat="person in people">
<input type="text" ng-model="person.name" change-delay="updatePerson(person)" />
</div>
</div>
</div>
Here's an failing fiddle: http://jsfiddle.net/Troop4Christ/fA4XJ/
As you can see, I can't figure out how to call the directive attribute parameter w/ the "person" parameter passed to it.
So like I said, at the begining.. just trying to recreate ng-change w/ some "tweaking". How is this done in ng-change? i.e.
Solution
Isolate scope binding should be declared with "&" instead of "=", thus resulting in scope.callBack() executing the updatePerson(person) given function.
Explanations
When isolating a scope, you work with "#", "=" and "&":
"#" tells angular to watch the result of attribute evaluation against the element scope
"=" tells angular to build the getter/setter with $parse
"&" tells angular to bind a function that will evaluate the attribute (and, as an option, provide an extension to the attribute definition scope as an argument to this function call).
So, when you choose this last option "&", it means that calling callBack() on the isolate directive scope will actually call updatePerson(person) againts the outside scope (not extended with any object coming from isolate scope).
Taking the scope extension capability into account, you could have replaced the person argument of the updatePerson(person) by calling scope.callBack({person: {a:1}}). Then person would have been {a:1} in the updatePerson call scope (function scope, not angular scope).

Angular Directive refresh on parameter change

I have an angular directive which is initialized like so:
<conversation style="height:300px" type="convo" type-id="{{some_prop}}"></conversation>
I'd like it to be smart enough to refresh the directive when $scope.some_prop changes, as that implies it should show completely different content.
I have tested it as it is and nothing happens, the linking function doesn't even get called when $scope.some_prop changes. Is there a way to make this happen ?
Link function only gets called once, so it would not directly do what you are expecting. You need to use angular $watch to watch a model variable.
This watch needs to be setup in the link function.
If you use isolated scope for directive then the scope would be
scope :{typeId:'#' }
In your link function then you add a watch like
link: function(scope, element, attrs) {
scope.$watch("typeId",function(newValue,oldValue) {
//This gets called when data changes.
});
}
If you are not using isolated scope use watch on some_prop
What you're trying to do is to monitor the property of attribute in directive. You can watch the property of attribute changes using $observe() as follows:
angular.module('myApp').directive('conversation', function() {
return {
restrict: 'E',
replace: true,
compile: function(tElement, attr) {
attr.$observe('typeId', function(data) {
console.log("Updated data ", data);
}, true);
}
};
});
Keep in mind that I used the 'compile' function in the directive here because you haven't mentioned if you have any models and whether this is performance sensitive.
If you have models, you need to change the 'compile' function to 'link' or use 'controller' and to monitor the property of a model changes, you should use $watch(), and take of the angular {{}} brackets from the property, example:
<conversation style="height:300px" type="convo" type-id="some_prop"></conversation>
And in the directive:
angular.module('myApp').directive('conversation', function() {
return {
scope: {
typeId: '=',
},
link: function(scope, elm, attr) {
scope.$watch('typeId', function(newValue, oldValue) {
if (newValue !== oldValue) {
// You actions here
console.log("I got the new value! ", newValue);
}
}, true);
}
};
});
I hope this will help reloading/refreshing directive on value from parent scope
<html>
<head>
<!-- version 1.4.5 -->
<script src="angular.js"></script>
</head>
<body ng-app="app" ng-controller="Ctrl">
<my-test reload-on="update"></my-test><br>
<button ng-click="update = update+1;">update {{update}}</button>
</body>
<script>
var app = angular.module('app', [])
app.controller('Ctrl', function($scope) {
$scope.update = 0;
});
app.directive('myTest', function() {
return {
restrict: 'AE',
scope: {
reloadOn: '='
},
controller: function($scope) {
$scope.$watch('reloadOn', function(newVal, oldVal) {
// all directive code here
console.log("Reloaded successfully......" + $scope.reloadOn);
});
},
template: '<span> {{reloadOn}} </span>'
}
});
</script>
</html>
angular.module('app').directive('conversation', function() {
return {
restrict: 'E',
link: function ($scope, $elm, $attr) {
$scope.$watch("some_prop", function (newValue, oldValue) {
var typeId = $attr.type-id;
// Your logic.
});
}
};
}
If You're under AngularJS 1.5.3 or newer, You should consider to move to components instead of directives.
Those works very similar to directives but with some very useful additional feautures, such as $onChanges(changesObj), one of the lifecycle hook, that will be called whenever one-way bindings are updated.
app.component('conversation ', {
bindings: {
type: '#',
typeId: '='
},
controller: function() {
this.$onChanges = function(changes) {
// check if your specific property has changed
// that because $onChanges is fired whenever each property is changed from you parent ctrl
if(!!changes.typeId){
refreshYourComponent();
}
};
},
templateUrl: 'conversation .html'
});
Here's the docs for deepen into components.

Categories

Resources