Infinite loop in State Change Events in angularjs - angularjs

I want to change the state from "mycarts" state to "carts" state. so I used $stateChangeStart angularjs event in "mycarts" controller and set a condition if the state you are going from "mycarts" state is "carts" state so go to the carts state.
Here is my angular event in my controller :
$scope.$on('$stateChangeStart', function (event, toState, toParams, fromState, fromParams) {
if (toState.name === 'carts' && fromState.name === 'mycarts') {
$state.go('carts', fromParams, true);
event.preventDefault();
return;
}
if (toState.name === 'mycarts' && fromState.name === 'mycarts') {
//Nothing
} else {
$rootScope.selectedItemsMyCarts = undefined;
}
});
My code will be stuck in :
if (toState.name === 'carts' && fromState.name === 'mycarts') {
$state.go('carts', fromParams, true);
event.preventDefault();
return;
}
Here is my state in app.js
.state('mycarts', {
url : '/mycarts/:type/:vendor?vendors&price_to&price_from&manufacturer&editor&theme&page&order&filter_by&no',
params: {
type: {squash: true, value: null},
vendor: {squash: true, value: null}
},templateUrl : 'partials/mycarts.html',
controller : 'MyCartsController'
}).state('carts', {
url : '/carts/:id?vendors&price_to&price_from&manufacturer&editor&theme&page&order&no',
templateUrl : 'partials/carts.html',
controller : 'CartsController'
}
And I get angular.js:12416 RangeError: Maximum call stack size exceeded error.
why this happens? And what is the solution if I want to change a state due to a condition in angular event? And what kind of documents I can read to understand what exactly going on when events like this happends.

If he is already going to the carts state, why are you doing this? :
$state.go('carts', fromParams, true);

Related

TypeError: Attempted to assign to readonly property on '$stateChangeStart' event's params

We have the following function that shows a confirmation dialog to the user when there are unsaved changes and the user tried to navigate away.
$scope.$on('$stateChangeStart', (event, toState, toParams, __fromState, fromParams) => {
if (fromParams.userConfirmedRedirect) {
return;
}
const checklistIsActive = $scope.revision &&
$scope.revision.checklist.status === ChecklistConstants.Status.ACTIVE;
const stateChangeIsWithinSameChecklist = toParams.id === $scope.checklistId;
if (checklistIsActive && !stateChangeIsWithinSameChecklist) {
const queueKey = PromiseQueueKeyGenerator.generateByChecklistId($scope.checklistId);
if (PromiseQueueService.isInProgressByKey(queueKey)) {
event.preventDefault();
MessageBox.confirm({
title: 'Are you sure?',
message: 'Please wait while we save your changes and try again.',
okButton: {
type: 'danger',
text: 'Yes',
action() {
fromParams.userConfirmedRedirect = true;
$state.go(toState.name, toParams, options);
},
},
});
}
}
});
This used to work perfectly fine before, but now throws an error (probably after an upgrade or something).
The error is:
TypeError: Attempted to assign to readonly property.
at action(./app/features/checklist/checklist-ctrl.js:517:33)
at fn(./app/features/message-box/confirm-ctrl.js:13:53)
at expensiveCheckFn(./node_modules/angular/angular.js:16213:1)
at $element(./node_modules/angular/angular.js:26592:1)
at $apply(./node_modules/angular/angular.js:18094:1)
at ? (./node_modules/angular/angular.js:26597:1)
at dispatch(/npm/jquery/dist/jquery.min.js:2:41777)
at s(/login:5:23562)
at _a(./node_modules/#sentry/browser/dist/index.js:3257:1)
The line that it's thrown from is this:
fromParams.userConfirmedRedirect = true;
I understand the reason for the error, however, I am stuck with a solution.
We need to pass an additional parameter to be available on the next $stateChangeStart event in order to make sure that the confirmation only shown once per route change.
I tried using the options argument for that:
$scope.$on('$stateChangeStart', (event, toState, toParams, __fromState, __fromParams, options) => {
if (options.custom.userConfirmedRedirect) {
return;
}
const checklistIsActive = $scope.revision &&
$scope.revision.checklist.status === ChecklistConstants.Status.ACTIVE;
const stateChangeIsWithinSameChecklist = toParams.id === $scope.checklistId;
if (checklistIsActive && !stateChangeIsWithinSameChecklist) {
const queueKey = PromiseQueueKeyGenerator.generateByChecklistId($scope.checklistId);
if (PromiseQueueService.isInProgressByKey(queueKey)) {
event.preventDefault();
MessageBox.confirm({
title: 'Are you sure?',
message: 'Please wait while we save your changes and try again.',
okButton: {
type: 'danger',
text: 'Yes',
action() {
options.custom.userConfirmedRedirect = true;
$state.go(toState.name, toParams, options);
},
},
});
}
}
});
But this approach didn't work.
What are my other options?

user role on UI-STATE

in my app i have many different USER ROLE's.
And on one state i need to set different roles. Bellow code, with only one user role, working ok
.state('distributor-dashboard', {
url: '/distributor-dashboard',
templateUrl: 'assets/html_templates/distributor-dashboard/distributor-dashboard.html',
controller: 'distributorDashboardCtrl',
authentication: {role: admin}
})
But, if i try to add another roles, they don' work here is example
.state('distributor-dashboard', {
url: '/distributor-dashboard',
templateUrl: 'assets/html_templates/distributor-dashboard/distributor-dashboard.html',
controller: 'distributorDashboardCtrl',
authentication: {role: ["admin" || "user"]}
})
Here is where i check USER ROLE
$rootScope.$on('$stateChangeStart', function (event, toState, toParams, fromState, fromParams) {
$rootScope.previousState = fromState.name;
var auth = authService.authentication.isAuth;
if (toState.name !== "login" && toState.name !== "signup" && toState.name !== "resset-password" && toState.name !== "new-password" && toState.name !== "account-activation" && auth === false) {
event.preventDefault();
$state.go('login', {});
}
var role = authService.authentication.role;
if (toState.authentication !== undefined && toState.authentication.role !== role) {
event.preventDefault();
$state.go('login', {});
}
}
Thnx.
I found a solution. It is necessary to set on STATE under authentification any role that we want to use.
authentication: {roles: ["admin", "user"]}
and then make a check if the roll there is in the allowed rolls
var userRole = authService.authentication.role;
if (toState.authentication !== undefined && toState.authentication.roles.indexOf(userRole) < 0) {
event.preventDefault();
$state.go('login', {});
}
I use indexOf for check, is roles in array. If someone have any question, feel free to ask

how to get current url from UI Router - State?

By conditionally, I require to route my application to a new path. so I would like to know the current url of my application.
how can i get my current url from sate like toState.name?
here is my scenario:
.state('serialCreateCase', {
url: '/sn={serialNumber}', //sn=12345;
templateUrl:'app/createCase/create-case.html',
controller: 'createCaseController as ctrl',
data: {
requireLogin: false
}
here is the condition:
if(toState.name==="serialCreateCase"){ // i am getting proper result
console.log('toState.name', toState );
$rootScope.goToView("serialCreateCase"); //i am asking to go there. but not works! //sn=12345;
event.preventDefault();
}
Thanks in advance.
You don't need the url because you are using states
You should use this method
$state.go("serialCreateCase", {sn: 12345});
This will send the user to state serialCreateCase with the data
Key : sn
Value : 12345
EDIT
You code inside the controller should look like this
if (toState.name === "serialCreateCase") { // i am getting proper result
console.log('toState.name', toState);
$state.go("serialCreateCase", { sn: 12345 });
event.preventDefault();
}
Edited Code:
$rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams) {
var curentpath = fromState.url;
var split = curentpath.split(':');
var params = $stateParams.sn;
var mainUrl = split[0] +params;
console.log(mainUrl);
});
&& Route:
.state('serialCreateCase', {
url: '/sn={serialNumber}', //sn=12345;
templateUrl:'app/createCase/create-case.html',
controller: 'createCaseController as ctrl',
data: {
requireLogin: false
}

Resyncing $urlRouterProvider.deferIntercept(); for angular 1 ui router

In Angular 1.5 and angular ui router, I want to change the back button behaviour of my states to recognise the names of the states and not the params as I have a url /restaurant/1?param1=value&param2=value which dynamically changes without refreshing the page. I have the binding and url not changing when a property changes, but when I hit the back button it goes to the previous params state, not the saved state in $rootScope by name. I've debugged this for hours and my current solution only works sometimes, it's not consistent because the url sync isn't always called in time thus the state does not update when the url updates. I'm correctly identifying when the back button is hit but it's not refreshing the state. Right now, I have to use a location.assign and yet it only works some of the time. Is there anyway to resync the deferIntercept in angular ui router?
function bind(propGetters,
getUrl) {
let unwatch = this._$rootScope.$watchGroup(propGetters, () => {
let trimmedUrl = getUrl();
let remove = this._$rootScope.$on('$locationChangeSuccess', () => {
this._$rootScope.disableBackButton = false;
remove();
});
this._$rootScope.disableBackButton = true;
if (!this._$rootScope._hitBackButton) {
this._$rootScope.shouldSync = false;
this._$location.url(trimmedUrl);
}
});
return {
unbind(): void {
unwatch();
}
};
module.run(
($urlRouter, $rootScope) => {
$rootScope.shouldSync = true;
$rootScope.$on('$locationChangeSuccess', e => {
if ($rootScope.shouldSync) {
$urlRouter.sync();
} else if (!$rootScope._hitBackButton) {
e.preventDefault();
}
$rootScope.shouldSync = true;
});
$rootScope.$on("$stateChangeSuccess", (event, toState, toParams, fromState, fromParams) => {
if ($rootScope.previousState) {
if ($rootScope.previousState.name !== fromState.name && !$rootScope.disableBackButton) {
$rootScope.previousState = lodash.merge(fromState, { params: fromParams });
console.log($rootScope.previousState.name, 'previousState');
}
} else {
this._$rootScope.previousState = lodash.merge(fromState, { params: fromParams });
}
});
$rootScope.$on('$locationChangeSuccess', (evt, newUrl, oldUrl) => {
$rootScope.actualPrevious = oldUrl;
if ($rootScope._hitBackButton) {
this._urlProvider.sync();
$rootScope._hitBackButton = false;
}
});
$rootScope.$on('$locationChangeStart', (evt, newUrl, oldUrl) => {
if ($rootScope.actualPrevious === newUrl && $rootScope.previousState && !$rootScope.disableBackButton && !$rootScope._hitBackButton) {
$rootScope.shouldSync = true;
event.preventDefault();
console.log('hit back', $rootScope.previousState.name);
$rootScope._hitBackButton = true;
window.location.assign(this._urlService.getFullPath(this._$state.href($rootScope.previousState.name, $rootScope.previousState.params)))
// this._$state.go($rootScope.previousState.name, $rootScope.previousState.params, { reload: true }); - this doesn't seem to always work because of watch collisions?
}
});
$urlRouter.listen();
});
After playing around for 5 hours,
I found it's best to avoid using deferIntercept all together and instead reloadOnSearch: false like so
$stateProvider.state('app.restaurant', {
url: '/restaurant/{slug}?param1&param2&param3',
resolve: {
param1: function 1,
param2: function 2,
param3: function 3
},
reloadOnSearch: false,
params: {
slug: { value: null },
},
controller: 'restaurantController',
template:
});
Note you will have to use deferIntercept for slashes instead of params. I'd strongly advise using params if your url changes. The back button should work fine with my solution above minus the deferIntercept and plus the reloadOnSearch: false
Full modified implementation:
this._$rootScope.$on('$locationChangeStart', (evt, newUrl, oldUrl) => {
if (this._$rootScope.actualPrevious === newUrl && this._$rootScope.previousState && !this._$rootScope._hitBackButton) {
evt.preventDefault();
this._$rootScope._hitBackButton = true;
this._$state.go(this._$rootScope.previousState.name, this._$rootScope.previousState.params);
}
});
this._$rootScope.$on("$stateChangeSuccess", (event, toState, toParams, fromState, fromParams) => {
if (this._$rootScope.previousState) {
if (this._$rootScope.previousState.name !== fromState.name) {
this._$rootScope.previousState = lodash.merge(fromState, { params: fromParams });
}
} else {
this._$rootScope.previousState = lodash.merge(fromState, { params: fromParams });
}
});
this._$rootScope.$on('$locationChangeSuccess', (evt, newUrl, oldUrl) => {
this._$rootScope.actualPrevious = oldUrl;
if (this._$rootScope._hitBackButton) {
this._$rootScope._hitBackButton = false;
}
});

How to dynamically set ui-sref value to parent state of the current state

1) My html form/view has below anchor tag which takes me to a state after clicking it
<a class="btn btn-default" ui-sref="custom.email.send">
2) But the same form is called from 2 different routes
3) Hence I want to set value of ui-sref dynamically to point to parent state of the state from where it was called!
For ex , if the above form is called from state "custom.email.send" then ui-sref should point to "custom.email"
http://angular-ui.github.io/ui-router/site/#/api/ui.router.state.$state
$rootScope.linkHistoryBack = '/';
$rootScope.$on('$stateChangeSuccess', function(event, to, toParams, from, fromParams) {
$rootScope.linkHistoryBack = $state.href(from, fromParams, {
absolute: true
});
});
<a ng-href="{{$root.linkHistoryBack}}">Go Back</a>
or, alternatively, build a directive:
angular
.module('uiSrefUtils', ['ui.router'])
.directive('uiSrefBack', function uiSrefBackDirectiveFactory($state) {
return function(iScope, iElement, iAttributes) {
iElement.attr('href', '/');
$rootScope.$on('$stateChangeSuccess', function(e, to, toP, from, fromP) {
var link = $state.href(from, fromP, {
absolute: true
});
return iElement.attr('href', link);
});
};
})
;
// use in your app
angular.module('myApp', ['ui.router', 'uiSrefUtils']);
<a ui-sref-back>Return</a>
You may create a state change listener to listen for state change globally
$rootScope.$on('$stateChangeSuccess', function(event, to, toParams, from, fromParams) {
$rootScope.previousState = from;
});
You may then access the previous state name with $rootScope.previousState.name and use this as the binding value

Resources