$locationChangeSuccess triggers four times - angularjs

I am new to angular Js.
My application flow is as below:
1) I have a view controller wherein, each view controller sets the breadcrumb data with the help of Breadcrumbs factory.
2) Breadcrumbs factory takes data from view controller and attaches data to $location.$$state object.(reason for storing in state object is if back button is pressed, view controller doesn't instantiate so I can refer history data for breadcrumbs ) below is code to attach data to state object:
var state = $location.state();
state.breadcrumb = breadcrumbData;
$location.replace().state(state);
3) I have also created breadcrumb directive on global header which will display breadcrumbs on $locationChangeSuccess event. Directive will take data from $location.state(); which was set in factory.
My problem is when location is changed, $locationChangeSuccess event callback function executes four times.
below is my directive code:
angular.module('cw-ui')
.directive('cwBreadcrumbs', function($location, Breadcrumbs, $rootScope) {
return {
restrict: 'E',
replace: true,
templateUrl: 'UI/Directives/breadcrumb',
link: function($scope, element){
//some code for element...
$rootScope.$on('$locationChangeSuccess', function(event, url, oldUrl, state, oldState){
// get data from history of location state
var data = $location.state();
console.log(data);
});
}
};
});
output is as below:
Object {}
Object {key: "Core/Views/dash:1", view: "Core/Views/dash", parameters: Array[0], breadcrumb: Array[2]}
Object {key: "Core/Views/dash:1", view: "Core/Views/dash", parameters: Array[0]}
Object {key: "Core/Views/dash:1", view: "Core/Views/dash", parameters: Array[0]}
breadcrumb: Array[2] disappears 1st, 3rd and 4th times. I really don't know what is causing this callback function execute four times, and I have no clue about an issue and don't know how to debug. Please help guys!

After running into this myself, the problem lies in the fact you are using the root scope to bind the locationChangeSuccess event from within a directive that is either encountered multiple times on a single page, or encountered multiple times as you revisit the page:
$rootScope.$on('$locationChangeSuccess', function(event, url, oldUrl, state, oldState){
Since you are binding to the rootScope, and the rootScope does not go out of scope, the event binding is not cleaned up for you.
Inside your link function, you should add a listener for the element $destroy, as well as capture the return value from the original bind, so you can later unbind it.
First: capture return value:
var unbindChangeSuccess = $rootScope.$on('$locationChangeSuccess' ...
Next, unbind that value in your destroy method:
element.on('$destroy', function() {
unbindChangeSuccess();
});
That should solve the multiple calls to your locationChangeSuccess! :)

Related

How to watch directive attributes changes?

I have a directive:
set-bid.js
app.directive("setBid", ($rootScope, Http, Toast) => {
return {
template: require('./set-bid.html'),
scope: {
newOrder: "<"
},
link: function($scope, element, attrs) {
console.log(`$scope.newOrder:`, $scope.newOrder); // undefined
}
}
});
set-bid.html
{{newOrder}} <!-- nothing -->
parent.html
<set-bid new-order="newOrder"></set-bid>
As you can see, I send newOrder variable to the set-bid directive.
However newOrder will be filled async.
I want the set-bid directive to watch for changes in this attribute.
But I do not want to watch on each param like this:
$scope.$watch("newOrder", newOrder => console.log(newOrder));
This is tedious. I want that the whole directive will listen for changes in every
param that it receives. Automatically. How this can be done?
I know I can do something like this: <set-bid ng-if="newOrder" ...></set-bid> but I need the variable to continously be watched, not just for the first time.
Set the third argument to true:
$scope.$watch("newOrder", newOrder => console.log(newOrder), true);
That creates a "deep" watch.
For more information, see
AngularJS Developer Guide - Scope $Watch Depths
You could just pass one object instead of many params and watch this one (deeply). Then, you also don't have the problem that the watcher fires multiple times, even though you e.g. just wanted to switch all params together.
Another approach would be to use a $watchCollection() and watch multiple params together, but you would need them to be listed one by one.

unable to get $scope form name on load

I'm trying the retrieve the form name inside my angularjs component while the form is being loaded as I wanted to set the form state to dirty based on some data validations that were resolved in to the component. I'm able to access the form name once the form is completely loaded say inside a submit, however i'm unable to do that on the load how can I do that. I'm using ui.router hence the controller name is being set based on the state.
<form class="form-horizontal" name="detail.myForm">
<button ng-click="detail.submit">
</form>
app.component('myDetail', {
bindings: {
alldetails: '<'
},
templateUrl: '/app/detail.html',
controllerAs: 'detail',
controller: function ($state, $transitions, $scope) {
var detail=this;
/*validateData in the alldetails here */
$scope.detail.myForm.$setDirty(); // issue here saying undefined
detail.submit = () =>{
$scope.detail.myForm.$setPristine() //works without any issue
}
}
This happens since the DOM isn't ready on your controller's construction. You have to use the $onInit callback instead. From AngularJS docs:
$onInit() - Called on each controller after all the controllers on an element have been constructed and had their bindings initialized (and before the pre & post linking functions for the directives on this element). This is a good place to put initialization code for your controller.
Also, it'd be better to inject the ngFormController by using the require object instead of assigning it to your model.
Here's a fiddle with a working example. The relevant code is:
.component('myDetail', {
template: '<h1>Details Component</h1>',
controllerAs: 'detail',
// By requiring the form controller, angular will
// create a 'formCtrl' property on your controller with the
// ngFormController instance of the parent form.
require: {
formCtrl: '^form'
},
controller: function() {
// We can't just acces the formController here, couse it will be
// undefined, since the dom isn't ready yet. So we have to use the
// $onInit callback that will be executed by angularjs.
this.$onInit = function() {
/*validateData in the alldetails here */
this.formCtrl.$setDirty();
}
}
});

When does angular destroy a directive instance?

I created a directive, which binds to $stateChangeSuccess events in link function:
module.exports = angular.module("titlePanel", [])
.directive("titlePanel", titlePanel);
function titlePanel($state, $rootScope, $parse, $interpolate) {
return {
restrict: "E",
replace: false,
scope: true,
templateUrl: template,
link: function(scope, element, attrs) {
// some actions
// ...
$rootScope.$on("$stateChangeSuccess", function(){
console.log("State change success happened on titlePanel");
});
}
}
This directive is used only at some pages of my site. What surprises me is that when I transition from state that contains this directive's instance to a state that doesn't contain it anymore, directive still responds to $stateChangeSuccess event and executes console.log().
I believe, I misunderstand the chronological order of events upon transferring to a certain state (I use ui-router). I thought it is as follows:
ui-router starts switching to a new state
it resolves and loads dependencies of new state's controller
it instantiates new controller
it traverses through the code of new controller's template and detects directives within it
it compiles those directives if they were not already compiled and stored in module registry
it creates directive instances by creating directive scopes and calling link linking function
it destroys the previous state, previous controller/scope and instances of directives, built for the previous controller
it emits $stateChangeSuccess
I am probably wrong somewhere. Can you describe the correct order of events?
Directives are destroyed once they are no longer represented in the DOM. (They do persist if they're not visible, hence ng-show=false will hide but not destroy the directive; ng-if=false will destroy it.)
But:
$rootScope.$on("$stateChangeSuccess", function(){
console.log("State change success happened on titlePanel");
});
This attaches the event to rootScope, which persists throughout the lifespan of the application, even after the directive is gone.
You can prevent this by either explicitly detaching the event on the directive's 'destroy' method:
var unbinder = $rootScope.$on("$stateChangeSuccess", function() {
console.log("State change success happened on titlePanel");
});
scope.$on('$destroy', function() {
unbinder();
});
...or, better, by attaching the event to the directive's scope rather than the root scope, so it will be cleaned up automatically. (It's generally good practice to avoid using root scope when not absolutely necessary: instead bind events to the directive they belong to, or to a shared factory or service devoted to that purpose.)

Directive trigger controller before model is updated

I created a custom directive that create two buttons. Each button use ng-click to call a method inside the directive to change a value of the model and then call the controller to update the value in the database. In this way the controller is called before the model is updated because it always use the old value.
If I apply inside the directive $scope.$apply() everything works fine, except an error is fired in the consolle because there are an $apply() executing yet. How properly wait the model update before to call the controller?
The directive is used into an ng-repeat that iterate an array.
Logic I used is:
User Click one of the Buttons of the switch (On or Off i.e.)
Button Trigger directive setModel(value) method
setModel(value) set value to the model and call controller passing index of the object
controller get the object by index and update in database
If i try to send the value edited to the controller, of course, it works, but i would play with models and not with each values. Why the model is updated before controller do it's $http calls?
This is the directive:
app.directive('ngSwitch', function () {
return {
scope: {
model: '=ngSwitch',
index: '=index', //Index in the array
switchValue1: '#switchValue1',
switchDesc1: '#switchDesc1',
switchValue2: '#switchValue2',
switchDesc2: '#switchDesc2',
callback: '=callback'
},
link: function (scope, elm, attr) {
scope.saving = false;
scope.setModel = function (value) {
scope.model = value == 1 ? scope.switchValue1 : scope.switchValue2;
scope.saving = true;
scope.$apply();//IF I DON'T USE $apply, IT DOESN'T WORK
scope.callback(scope.index)
}
},
templateUrl: 'components/direttive/ngSwitch.html'
};
});

directive that listens to document click event gets fired immediately after creation

I am working on an angular.js with angular-ui-router app.
I have a state in my app that imitates a kind of a pop up.
I have created a directive that is responsible to move the app to a different state when someone clicks outside of the popup element.
I have created a js.fiddle with my code:
http://jsfiddle.net/lobengula3rd/1e908559/8/
state 1 - is the popup state.
state 2 - is a different state.
I have a link in state 2 that when clicking it will make a state change to state 1 (the popup state):
<a ui-sref="state1">state1</a>
this is my directive code:
.directive('closeOnOuterClick', ['$document', '$state', function ($document, $state) {
return {
restrict: 'A',
link: function (scope, element, attributes) {
$document.on("click", function (evt) {
$document.off('click');
$state.go(attributes['closeOnOuterClick']);
});
element.on("click", function (evt) {
evt.stopPropagation();
});
}
};
}]
The problem is that for some reason the listener function of 'document.on' click event gets fired immediately after the creation of the popup element, thus throwing me out.
I don't understand why the function gets called immediately.
If i change in my controller (line 20) that on app start i will go to state1 instead of state2, it will work just fine for the first time.
I tried to change the link that changes the state to an ng-click event that will call a function in my controller, but that also didn't work.
what is the problem with my code?
I figured the problem.
I had angular-ui-router version 0.2.0. updating it to version 0.2.11 solved this issue for some unknown reason.
also if i change the link to be an ng-click instead of ui-sref that calls a function that eventually will change the state instead then you should also call the $event (angular object) stop propagation directly like this:
state1
Have you tried change the $document inside of the directive to element ? line 28
element.on("click", function (evt) {
$document.off('click');
$state.go(attributes['closeOnOuterClick']);
});
That works for me in the fiddle: http://jsfiddle.net/1e908559/10/

Resources