Angular UI Router Reload Controller on Back Button Press - angularjs

I have a route that can have numerous optional query parameters:
$stateProvider.state("directory.search", {
url: '/directory/search?name&email',
templateUrl: 'view.html',
controller: 'controller'
When the user fills the form to search the directory a function in the $scope changes the state causing the controller to reload:
$scope.searchDirectory = function () {
$state.go('directory.search', {
name: $scope.Model.Query.name,
email: $scope.Model.Query.email
}, { reload: true });
};
In the controller I have a conditional: if($state.params){return data} dictating whether or not my service will be queried.
This works great except if the user clicks the brower's forward and/or back buttons. In both these cases the state (route) changes the query parameters correctly but does not reload the controller.
From what I've read the controller will be reloaded only if the actual route changes. Is there anyway to make this example work only using query parameters or must I use a changing route?

You should listen to the event for succesful page changes, $locationChangeSuccess. Checkout the docs for it https://docs.angularjs.org/api/ng/service/$location.
There is also a similar question answered on so here How to detect browser back button click event using angular?.
When that event fires you could put whatever logic you run on pageload that you need to run when the controller initializes.
Something like:
$rootScope.$on('$locationChangeSuccess', function() {
$scope.searchDirectory()
});
Or better setup like:
var searchDirectory = function () {
$state.go('directory.search', {
name: $scope.Model.Query.name,
email: $scope.Model.Query.email
}, { reload: true });
$scope.searchDirectory = searchDirectory;
$rootScope.$on('$locationChangeSuccess', function() {
searchDirectory();
});

Using the above, I was able to come up with a solution to my issue:
controller (code snippet):
...var searchDirectory = function (searchParams) {
if (searchParams) {
$scope.Model.Query.name = searchParams.name;
$scope.Model.Query.email = searchParams.email;
}
$state.go('directory.search', {
name: $scope.Model.Query.name,
email: $scope.Model.Query.email,
}, { reload: true });
};...
$rootScope.$on('$locationChangeSuccess', function () {
//used $location.absUrl() to keep track of query string
//could have used $location.path() if just interested in the portion of the route before query string params
$rootScope.actualLocation = $location.absUrl();
});
$rootScope.$watch(function () { return $location.absUrl(); }, function (newLocation, oldLocation) {
//event fires too often?
//before complex conditional was used the state was being changed too many times causing a saturation of my service
if ($rootScope.actualLocation && $rootScope.actualLocation !== oldLocation && oldLocation !== newLocation) {
searchDirectory($location.search());
}
});
$scope.searchDirectory = searchDirectory;
if ($state.params && Object.keys($state.params).length !== 0)
{..call to service getting data...}
This solution feels more like a traditional framework such as .net web forms where the dev has to perform certain actions based on the state of the page. I think it's worth the compromise of having readable query params in the URL.

Related

ui-router 1.x.x change $transition$.params() during resolve

Trying to migrate an angularjs application to use the new version of angular-ui-router 1.0.14 and stumbled upon a problem when trying to change $stateParams in the resolve of a state.
For example, previously (when using angular-ui-router 0.3.2) modifying $stateParams worked like this:
$stateProvider.state('myState', {
parent: 'baseState',
url: '/calendar?firstAvailableDate',
template: 'calendar.html',
controller: 'CalendarController',
controllerAs: 'calendarCtrl',
resolve: {
availableDates: ['CalendarService', '$stateParams', function(CalendarService, $stateParams) {
return CalendarService.getAvailableDates().then(function(response){
$stateParams.firstAvailableDate = response[0];
return response;
});
}]
}
})
The problem is firstAvailableDate is populated after a resolve and I do not know how to update $transition$.params() during a resolve when usign the new version of angular-ui-router 1.0.14.
I have tried, and managed to update the url parameter with
firing a $state.go('myState', {firstAvailableDate : response[0]}) but this reloads the state, so the screen flickers
modified $transition$.treeChanges().to[$transition$.treeChanges().length-1].paramValues.firstAvailableDate = response[0]; to actually override the parameters. I have done this after looking through the implementation on params() for $transition$.
Although both those options work, they seem to be hacks rather than by the book implementations.
What is the correct approach to use when trying to modify parameters inside a resolve?
Approach with dynamic parameter:
Take a look at this document: params.paramdeclaration#dynamic. Maybe thats what you are looking for: ...a transition still occurs....
When dynamic is true, changes to the parameter value will not cause the state to be entered/exited. The resolves will not be re-fetched, nor will views be reloaded.
Normally, if a parameter value changes, the state which declared that the parameter will be reloaded (entered/exited). When a parameter is dynamic, a transition still occurs, but it does not cause the state to exit/enter.
This can be useful to build UI where the component updates itself when the param values change. A common scenario where this is useful is searching/paging/sorting.
Note that you are not be able to put such logic into your resolve inside your $stateProvider.state. I would do this by using dynamic parameters to prevent the state reload. Unfortunally, the dynamic rules doesn't work when you try to update your state (e.g. by using $stage.go()) inside the resolve part. So I moved that logic into the controller to make it work nice - DEMO PLNKR.
Since userId is a dynamic param the view does not get entered/exited again when it was changed.
Define your dynamic param:
$stateProvider.state('userlist.detail', {
url: '/:userId',
controller: 'userDetail',
controllerAs: '$ctrl',
params: {
userId: {
value: '',
dynamic: true
}
},
template: `
<h3>User {{ $ctrl.user.id }}</h3>
<h2>{{ $ctrl.user.name }} {{ !$ctrl.user.active ? "(Deactivated)" : "" }}</h2>
<table>
<tr><td>Address</td><td>{{ $ctrl.user.address }}</td></tr>
<tr><td>Phone</td><td>{{ $ctrl.user.phone }}</td></tr>
<tr><td>Email</td><td>{{ $ctrl.user.email }}</td></tr>
<tr><td>Company</td><td>{{ $ctrl.user.company }}</td></tr>
<tr><td>Age</td><td>{{ $ctrl.user.age }}</td></tr>
</table>
`
});
Your controller:
app.controller('userDetail', function ($transition$, $state, UserService, users) {
let $ctrl = this;
this.uiOnParamsChanged = (newParams) => {
console.log(newParams);
if (newParams.userId !== '') {
$ctrl.user = users.find(user => user.id == newParams.userId);
}
};
this.$onInit = function () {
console.log($transition$.params());
if ($transition$.params().userId === '') {
UserService.list().then(function (result) {
$state.go('userlist.detail', {userId: result[0].id});
});
}
}
});
Handle new params by using $transition.on* hooks on route change start:
An other approach would be to setup the right state param before you change into your state. But you already said, this is something you don't want. If I would face the same problem: I would try to setup the right state param before changing the view.
app.run(function (
$transitions,
$state,
CalendarService
) {
$transitions.onStart({}, function(transition) {
if (transition.to().name === 'mySate' && transition.params().firstAvailableDate === '') {
// please check this, I don't know if a "abort" is necessary
transition.abort();
return CalendarService.getAvailableDates().then(function(response){
// Since firstAvailableDate is dynamic
// it should be handled as descript in the documents.
return $state.target('mySate', {firstAvailableDate : response[0]});
});
}
});
});
Handle new params by using $transition.on* hooks on route change start via redirectTo
Note: redirectTo is processed as an onStart hook, before LAZY resolves.
This does the same thing as provided above near the headline "Handle new params by using $transition.on* hooks on route change start" since redirectTo is also a onStart hook with automated handling.
$stateProvider.state('myState', {
parent: 'baseState',
url: '/calendar?firstAvailableDate',
template: 'calendar.html',
controller: 'CalendarController',
controllerAs: 'calendarCtrl',
redirectTo: (trans) => {
if (trans.params().firstAvailableDate === '') {
var CalendarService = trans.injector().get('CalendarService');
return CalendarService.getAvailableDates().then(function(response){
return { state: 'myState', params: { firstAvailableDate: response[0] }};
});
}
}
});

how to prevent ui-bootstrap pagination control from changing the url route

Within my application I have two views search and masternameserch these views are defined in app.js as:
$stateProvider
.state('search', {
url: '/',
controller: 'searchController',
controllerAs: 'search',
templateUrl: '/app/views/search.html'
})
.state('searchmasterName', {
url: '/searchmastername',
controller: 'searchMasterNameController',
controllerAs: 'masternameSearch',
templateUrl: '/app/views/searchmastername.html'
})
Within my views I have the ui-bootstrap pagination control setup as
<div class="text-center">
<uib-pagination ng-show="masternameSearch.pgTotalItems > 10" total-items="masternameSearch.pgTotalItems" ng-click="masternameSearch.pageChanged(); $event.stopPropagation();" ng-model="masternameSearch.pgCurrentPage" class="pagination-md" max-size="masternameSearch.pgMaxSize" boundary-links="true"></uib-pagination>
</div>
And within the controller I have the pageChanged() function setup as follows:
vm.pageChanged = function () {
var pageRequest = buildMasterNameSearchRequest(); //get cached request
pageRequest.pageFrom = vm.pgCurrentPage; //set page number to request
var currentPath = $state.current.name;
searchService.postSearchRequest(pageRequest).then(renderResults, onError);
$state.go('searchmastername',
{},
{
notify: false,
location: false,
inherit: false,
reload: false
});
$timeout(function () { location.hash = '#top' }, 1000);
}
The question I have is whenever clicking on the pagination control the underlying URL is always the root url. Therefore when I click on a pagination button the search executes but I am navigated back to the default main view which is wrong. As you can see from the code I first tried in the directive itself to change the onchange to an ng-click event and tried to stop propagation to stop the redirect through $event. This did not work. Second thing I tried was to call a state transition / state.go() in the pageChanged() function where I basically reload the view. This however does not work as it throws an error that the state cannot be found. Sadly this actually prevents the page from reloading or navigating to the main page so the error actually makes the page work as the end user might expect, but with errors generated around the missing state I know this isn't right.
Update: The error was generated from a type searchmastername and not searchmasterName.
Making this change fixed the error but still causes the site to redirect to the default view to load.
Can anyone provide an idea or ways to get the pagination control to not cause a navigation event by redirecting me to the default main view when ever clicked?
-cheers
After doing some more studying of this problem I thought the issue could be addressed by listening to the $rootScope for the stateChangeStart event and from there add a preventDefault() function to stop the navigation from occurring in the route which I did.
So the code now looks like this on the pageChanged() function
vm.pageChanged = function () {
var pageRequest = buildMasterNameSearchRequest(); //get cached request
pageRequest.pageFrom = vm.pgCurrentPage; //set page number to request
searchService.postSearchRequest(pageRequest).then(renderResults, onError);
$rootScope.$on('$stateChangeStart',
function(event) {
event.preventDefault();
});
$timeout(function () { location.hash = '#top' }, 1000);
}
While useful, using preventDefault() stops all further interaction with the page. Links would no longer work etc. I thought I had fixed but instead I had just stopped the event so the timeout never got called which was the culprit.
The real issue and I did it to my self was adding the timeout and using the location route #.
$timeout(function () { location.hash = '#top' }, 1000);
Using this function was actually (as designed) changing my route in my URL and navigating me to the main view. I needed to have the ability to scroll to the top so I changed the timeout function to look like this
vm.pageChanged = function () {
vm.showPager = false;
var pageRequest = buildMasterNameSearchRequest(); //get cached request
pageRequest.pageFrom = vm.pgCurrentPage; //set page number to request
searchService.postSearchRequest(pageRequest).then(renderResults, onError);
window.scrollTo(0, 0);
}
Using window.scroll accomplished the same scrolling effect but did not change the route.

calling controller function from within a promise in external JS

Ionic Tabs, root of tabs HTML has "RootTabCtrl", and "Tab1" (with "Tab1_Ctrl") has a form, other tabs are disabled.
User submits form on Tab1
Tab1 Controller function kicks off.
Controller Function calls an external function (not in controller).
External function triggers, which executes a promise
In promise "results", returned data is processed
If X in returned data is true, trigger "RootTabCtrl" function to enable the other disabled tabs.
I can track console messages triggering every step of the way.
This all works except for this following odd behavior. "RootTabCtrl" doesn't enable the disabled tabs until the user clicks the form submit a second time...even though I see console messages saying it is in (at the end) of the RootTabCtrl function. I see all the same console messages from the first click - but on the 2nd time is when the disabled tabs get enabled again.
If I move step 6 outside of the promise in Step 4, and put it after step 3 (and before the promise), then all the tabs get enabled on the 1st click. However, this is no longer taking into account the value of X to determine if other tabs should be re-enabled or not.
What can I look for, or am not aware of, that would be causing this?
app.js:
.state('tab', {
url: "/tab",
abstract: true,
templateUrl: "templates/tabs.html",
controller: 'TabsCtrl'
})
// Each tab has its own nav history stack:
.state('tab.tab1', {
url: '/map',
views: {
'tab-1': {
templateUrl: 'templates/tab-1.html',
controller: 'tab1Ctrl'
}
}
})
controllers:
.controller('TabsCtrl', function($scope,$rootScope,constants) {
$scope.constants = constants ;
$scope.tabControl = {
disableRides : true,
disableBikes : true,
disableTransit : true
}
var refreshFinalizer = $rootScope.$on('updateTabsRefresh', function (event, data) {
console.log("Refresher 1") ;
$scope.tabControl.disableTab2 = false;
$scope.tabControl.disableTab3 = false ;
console.log("Refresher 2") ;
});
$scope.$on('$destroy', function () {
console.log("Destroy") ;
refreshFinalizer ();
});
})
.controller('tab1Ctrl', function($scope,$rootScope) {
$scope.setInfo= function() {
getGoogle(document.getElementById('form_data').value,0);
}
$scope.enableTabs = function(type) {
console.log("Here enableTabs1") ;
$rootScope.$broadcast('updateTabsRefresh');
console.log("Here enableTabs2") ;
}
})
Tab1 has a form, upon click, it executes $scope.setInfo. Then getGoogle() is in an outside JS function, its a call to google maps, and if specific data X is true, then enable all the other tabs using tab1Ctrl $scope.enableTabs() :
function getGoogle(userInfo,clear) {
console.log("setInfo 1") ;
geoCoder.geocode({'address': userInfo}, function(results, status) {
if (status == google.maps.GeocoderStatus.OK) {
if (results[0]) {
// extra code removed
startPointSet = 1;
// tab1Ctrl html has id of "Tab1"
angular.element(document.getElementById('Tab1')).scope().enableTabs();
/* alternative method, worked the same as previous line but with same problem
var $sBody = angular.element(document.body) ;
var $sRootScope = $sBody.injector().get('$rootScope') ;
$sRootScope.$broadcast('updateTabsRefresh') ;
*/
console.log("setInfo 2") ;
}
} else {
// extra code removed
console.log(response) ;
}
});
}
}
All of the above works...except that when the call to enableTabs (within the google response), even though it correctly calls enableTabs and I can see the console.log messages firing from within enableTabs - the other tabs don't "enable" until the form button is clicked a 2nd time (and then I see all the console messages again). I tried 2 different methods, from within getGoogle(), both worked exactly the same - 1st clicked fired all functions correctly, but tabs did not enable. 2nd click fired all the functions and then the tabs got enabled.
Try this previous answer. Are you using $http calls? If so I have never actually had to do that so it seems like something else might be the root cause.
UPDATE
How about creating an Angular Service for this GetGoogle function call. Then you will still be inside of angular and can inject $rootScope and anything else that you need. You will need to inject this service into your tab1Ctrl (the myGoggleService below). I would also probably just pass the form back on the ng-submit:
Html
ng-submit="setInfo(formNameGoesHere)"
Controller
.controller('tab1Ctrl', function($scope, $rootScope, myGoogleService) {
$scope.setInfo = function(form) {
myGoogleService.getGoogle(form);
}
$scope.enableTabs = function(type) {
console.log("Here enableTabs1");
$rootScope.$broadcast('updateTabsRefresh');
console.log("Here enableTabs2");
}
});
Service: If you haven't created any already you will need to register it in your app.js like any directives you have and also put them in your index.html page like a regular controller.
.service('myGoggleService', [ '$rootScope', function ($rootScope) {
this.getGoogle = function(userInfo,clear) {
console.log("setInfo 1") ;
geoCoder.geocode({'address': userInfo}, function(results, status) {
if (status == google.maps.GeocoderStatus.OK) {
$rootScope.$broadcast("updateTabsRefresh");
} else {
}
});
}
}}]);
I did copy your code from above and then just removed some of the extra stuff just to get the point across. Obviously could not run the code so there might be some mistakes or slight changes needed, but hopefully this will get you close.

How to implement filtering with Angular Ui-router

We want to implement pagination (or filtering) using ui-router. We don't want to reload controller each time next/prev parameters changed. We want to listen for an event and load data from API with new state parameters.
Code from our routes.js
$stateProvider.state('app.company.account.charges.index', {
url: '?before&after',
controller: 'ChargesCtrl',
templateUrl: 'app/charges.html',
reloadOnSearch: false,
params: {
before: { value: null, squash: true },
after: { value: null, squash: true },
}
});
our controller:
function ChargesCtrl() {
var loadCharges = function() {
Charge.query({before: $state.params.before, after: $state.params.after}).then(function(charges) {
$scope.charges = charges;
});
}
$scope.goNext = function() {
$state.go('app.company.account.charges.index', {before: $scope.pagination.before, after: null});
};
$scope.goPrevious = function() {
$state.go('app.company.account.charges.index', {after: $scope.pagination.after, before: null});
};
$scope.$on('state params were changed', function() {
loadCharges();
});
}
how can we track state params changes if $stateChangeSuccess is not raised because of reloadOnSearch is false.
P.S. now we just broadcast own event to solve this, but I'm looking for 'natural' solution :)
Ui Bootstrap has a really easy way to implement pagination. And it's also easy to integrate with Ui-router! This link will bring you to the pagination help :)
have fun coding
Thanks to #krish I have found the answer: Set URL query parameters without state change using Angular ui-router
And it's to use a hack with changing reloadOnSearch option to false before updating location params and then revert it back in $timeout.
However I think that using something like this is more explicit:
$scope.goNext = function() {
$state.go('app.company.account.charges.index', {before: $scope.pagination.before, after: null});
$scope.$emit('charges.paginated');
};
$scope.$on('charges.paginated', function(event) {
event.stopPropagation();
loadCharges();
});

Changing states but controller is not being called in angular js

I have a webpage which displays the user profile information from the database which is working fine, and another one in which user edits his/her information, which is edited in the database, which is also working. But after I change the data in the second webpage, I have changed the state to the first page using $state.go, which also seems to be working, but the newly updated data is not showing in this first page, the controller is not being called I think.
This is my controller of webpage which shows the userprofile :
.controller('myProfileCtrl', function (..parameters..) {
$scope.id = window.localStorage.getItem("profileId");
$scope.$root.loading = true;
$http.get('my link...')
.success(function (data) {
$scope.first_name = data[0].first_name;
$scope.middle_name = data[0].middle_name;
})
This is the controller of my second page which updates the user information :
.controller('editPersonalCtrl', function (..parameters..) {
$scope.saveChanges = function (user) {
$http.post('my link..', {
changeFirstName : new_first_name,
changeMiddleName : new_middle_name,
})
.success(function (data) {
$state.go('profile');//Go to the first webpage
$ionicHistory.nextViewOptions({
disableBack: true,
historyRoot: true
});
})
.error(function (data) {
alert("ERROR" + data);
});
}
The state changes but the newly updated data is not shown, I mean the controller of the first webpage is not working, otherwise it would have fetched the new data from the database and displayed it. The old data is showing.
replace
$state.go('profile');
with
$state.go('profile', {}, { reload: true });
It will force the to transit and than also it's not working you can do like
$state.go('profile', {}, {reload: true,notify: true});
This will force and also will broadcast $stateChangeStart and $stateChangeSuccess events. Here is the ref

Resources