How to persist optional state parameter on browser back in ui-router? - javascript

I'm having one parent state that has two children's state inside that I'm going to show one state based on the URL.
Out of those two states one is having to parameters like param1 and param2, I have use params option of ui-router inside state definition.
State
$stateProvider.state('tabs.account', {
url: '/account',
views: {
'content#tabs': {
templateUrl: 'account.html',
controller: function($scope, $stateParams) {
//This params are internally used to make ajax and show some data.
$scope.param1 = $stateParams.param1;
$scope.param2 = $stateParams.param2;
},
}
},
params: {
param1: { value: null }, //this are optional param
param2: { value: null } //because they are not used in url
}
});
If you look at my route the params option is not really introduced inside the URL, that's why I'm considering then as optional.
Problem
Look at plunkr, I've shown two tabs Account & Survey,
Click on Survey tab, then add some data in the textarea which are shown.
Click on Go to Account that will pass those textarea values to
the other Account tab by doing ui-sref="tabs.account({param1: thing1, param2: thing2})" on the anchor
Now you will see the param1 & param2 values on html which has been assigned to scope from $stateParams
Now again Click on Survey tab, you will land on the survey page.
Just click browser back, you will notice that param value is not getting null.
Problem Plunkr
I believe you got what I wanted to ask, why the optional parameter value has not been store? as they have been a part of state.
I know I can solve this issue by below two solutions.
By creating one service that will share data between two views.
By adding parameter inside the state URL. like url: '/account/:param1/:param2', (But i wouldn't prefer this)
I already tried angular-ui-routers sticky states but that doesn't seems to work for me. What is the better way to this?
Is there any way by which I can make my use case working, Any ideas would appreciate.
Github Issue Link Here

I would move the params definition to the parent state, so as to share the optional state params between your two child states.
The child states will inherit the $stateParams from your parent, as such there is no real 'workaround' needed.
Simply inject $stateParams as per usual in your child controllers and you will have full access to the params being passed around. If you don't want to utilise the params in a specific child state, simply avoid injecting them.
This works with;
Back button
Forward button
ui-sref (without params (will keep as-is))
ui-sref (with params (will overwrite))
$stateProvider
.state('parent', {
params: { p1: null, p2: null }
})
.state('parent.childOne', {
url: '/one',
controller: function ($stateParams) {
console.log($stateParams); // { p1: null, p2: null }
}
})
.state('parent.childTwo', {
url: '/two',
controller: function ($stateParams) {
console.log($stateParams); // { p1: null, p2: null }
}
})
If you at any point want to clear the params while travelling within the state tree of parent, you would have to do so manually.
That would be the only real caveat I can see by using this solution.
I realise manual clearing may not be desirable in the case you present, but you haven't taken an active stand against it, as such I feel the suggestion has merit.
updated plunker

One workaround solution is to cache the state params and conditionally load them when entering the tabs.account state. UI Router state config actually lets you provide an onEnter callback for these types of "do something on entering the state" situations.
Here's the basic logic using localStorage as the cache, with working Plunker here:
When you enter the tabs.account state, check for your state params
If you have them, cache them to local storage
If you don't, load them from local storage into $stateParams
Here's an example code snippet for reference (taken from the Plunker):
$stateProvider.state('tabs.account', {
...
onEnter: ['$stateParams', '$window', function($stateParams, $window) {
if($stateParams.param1) {
$window.localStorage.setItem('tabs.account.param1', $stateParams.param1);
} else {
$stateParams.param1 = $window.localStorage.getItem('tabs.account.param1');
}
if($stateParams.param2) {
$window.localStorage.setItem('tabs.account.param2', $stateParams.param2);
} else {
$stateParams.param2 = $window.localStorage.getItem('tabs.account.param2');
}
}],
...
}
One caveat is that your params will persist indefinitely (e.g. across refreshes and sessions). To get around this, you could clear out the cache on application load like in app.run.
One last note is that in the Plunker, I'm accessing local storage directly (through the Angular $window service). You might want to use some AngularJS module - I've used angular-local-storage in production.

I believe that what you want to achieve is not possible without using one of the two solution you provided.
The browser back-button is just keeping the URL history. He have no clue about the ui-router internal states and will just force the URL to change.
Forcing the URL to change will trigger internal ui-router machine but unfortunately ui-router will see the URL change the same as if someone would have change the url by hand.
Ui-router will fire a new route change to the route pointed by the URL. That mean he doesn't know you wanted to go "back" and will just change state to the new one without any parameters.
Summary
Clicking on back button will fire a state change to a new state according to the URL instead of going back to the previous state.
This is why adding the params to the URL solve the issue. Since the URL is discriminatory you'll finally land on the state you wanted.
Hope it helped.

To me this sounds as X Y problem. There are suggested ways to make state params persistent whereas the problem is located out of this surface.
By definition of the question there is data that should be kept independent of states. So it is kind of global relative to states. Thus there should be a service that keeps it and controllers of both states maintain it as required. Just don't pass data that is out of one state's scope in state params.
Sticky states would fit this approach easily since it allows to keep DOM and $scope while another state is active. But it has nothing to do with state params when the state is reactivated.

Related

How do I specify a state parameter on a state in the middle of the route (ancestor of target state)?

Spent hours googling and combing through the docs angular ui-router docs to no avail...seems like this should be really basic.
The quick version
Looking for something that I imagine might look something like this
<a ui-sref="parent({param: value}).child">go</a>
or this
$state.go("parent.child", {parent:{param: value}})
- the idea being that param gets applied to parent, not child, resulting in a url such as /parent/value/child
The long version
THE SETUP:
Let's say I have a state that takes a parameter, projectId, like so:
.state('manage.project', {
url: '/project/:projectId',
templateUrl: 'fragments/projectTemplate.html',
controller: 'projectController'
}
...then way down the state tree, I have one of many child states that depend on on that parameter:
.state('descendantOfProjectState.grandchildState', {
url: '/projectInfo',
templateUrl: 'fragments/projectInfo.html',
controller: function($state){
var projectId = $state.params.projectId;
}
}
Normally, one would first visit the parent:
$state.go('manage.project', {projectId: 123})
At this point the URL would look something like
/manage/project/123/foo/bar/projectInfo
From there, navigating to a child state is as simple as
$state.go("manage.project.foo.bar.descendantOfProjectState.grandchildState");
and the projectId is accessible via $state.params by virtue of being in the URL - no need to configure every single child state to explicitly expect the projectId (wondering if this is my fatal flaw).
THE PROBLEM:
Now the need has arisen to jump to that grandchild state, without first visiting the parent state. That is, I want to go directly to
/manage/project/123/foo/bar/projectInfo
from within the app (so, I want to go to the state, not the url).
How can I specify the projectId parameter for the project state, when it's in the middle of the route I'm navigating to?
$state.go('manage.project.foo.bar.descendant.grandchild', {projectId: 123})
doesn't inform the the ancestor state that needs the id.
How can I, in one fell swoop, tell ui-router to go directly to that grandchild state while passing the projectId to manage.project state which is in the middle of the route?

Angular, setting up a callback function for updating between factory and controller

I'm not sure if i have completely wrapped my head around this idea - but I'll try my best to clearly describe what I am trying to do here.
I have a factory that changes and parses a URL for me, so I can pass params into a controller for use (that were stored in the url). This is sort of so I can save a state for the user and they can share it via copy'ing of a URL (send it to their friends or bookmark it or w/e).
I am trying to set up a factory (or service) that listens for locationChangeSuccess - so that if the user mofies the url and presses enter, it will refresh the scopes in the controllers. So here is what I have:
.factory("urlFactory", function($location, freshUrl, StateString){
//request to change new url
requestObj.requestState = function(moduleName, stateName, startVar){
}
//request item from url, via your module and state name
requestObj.parseState = function(moduleName, stateName){
}
I dropped the center out (if it is needed im happy to link), but those just get and set the url for me.
So in the controllers I do something like
$scope.mod2m3 = urlFactory.parseState("module2", "mod3");
$scope.mod2m4 = urlFactory.parseState("module2", "mod4");
So when they land on the page, they pull their state. This works great. However, now i'm trying to solve some edge case scenarios where maybe the user modifies the url.
So I can latch onto that even pretty easily with
.factory("urlWatcher", function($location, $scope){
var urlWatcher = {};
$scope.$on('$locationChangeSuccess', function(event) {
console.log("Asdsa");
});
return urlWatcher
});
However, where I am struggling is trying to determine a way where when this fires, it would connect the new value to the scope in the controller. It was suggested to me that a callback of some sort in the parse (set) function, but I am struggling with how to approach that. It would be super cool if I could set a way for this factory/service to re send the new value when it changes to the right place. Callback sounds good, however I don't know how to config this correct.
The easiest route would be to just do an
$scope.$on('$locationChangeSuccess', function(event) {
console.log("Asdsa");
});
In each controller and manually bind to each scope, but I am trying to make this as modular as possible (and thats also a ton of watchers on the locationchangesuccess). would be fantastic if I could figuire out a clean way to set the service/factory to listen once, and on change find the right module/controller and change the value.
I can't seem to think a clear route, so I would be very greatful for any insight to this issue. Thank you very much for reading!
If what you want is a publish/subscribe architecture, where publications are global and subscriptions have the same lifecycles as Angular scopes... then Angular events are what you're looking for. There's no point setting up an ad hoc communication system with callbacks and whatnut, that would just be partially reinventing events.
However, if you want to make the semantics more obvious / add flexibility, you can listen once to $locationChangeSuccess in a service and broadcast a custom event.
$rootScope.$on("$locationChangeSuccess", function (event) {
$rootScope.$broadcast('myCustomeEvent', {message: "Guys, time to refresh!"});
});
Then listen to this event in each of the scopes where it is relevant.
$scope.$on('myCustomeEvent', function (event) {
console.log("Asdsa");
});
If setting up the listening gets repetitive, by all means, factor it out in a function, which you can for example put in a service:
myApp.factory('someFactory', [function () {
return {
listenToLogAsdsa: function (scope) {
scope.$on('myCustomeEvent', function (event) {
console.log("Asdsa");
});
}
};
}]);
Then all you have to write in your controller is:
someFactory.listenToLogAsdsa($scope);
You can assign a variable in the scope to an object in the factory, that way it's bound to a reference instead of a value. Then, in your HTML you bind the reference to the DOM. urlFactory.parseState() should then save the result to said object, and return the key where it was saved.
For example:
In urlFactory:
requestObj.parseState = function(moduleName, stateName){
var key = moduleName+stateName;
this.urlContainer[key] = "www.example.com";
return key;
}
In the controller:
$scope.urls = urlFactory.urlContainer;
$scope.mod2m3 = urlFactory.parseState("module2", "mod3");
In your HTML:
{{urls[mod2m3]}}
This way, "urls" is bound to a reference, which angular watches for changes, and whenever you change urls[mod2m3], it will affect the DOM.
You can also just react to changes in the scope variables by watching them:
$scope.$watch('urls', function() {
//do something
});
NOTE: Since this is an object, you might need to use $watchCollection instead of $watch.

AngularJS - Router UI - Defining a wildcard parented state

Fellas, Fellaaas.
I'm writing from my phone so please excuse me for anything misleading or unclear.
In my app there's a state that can be inherited from any other parent state, called "deals/:dealId:".
As of now, I have to define the same state over and over again for any other parent state available, so it could be accessible from any URL on the app.
For example: the parent state "dashboard" has a child state "parent.dashboard.deals", so is another state - "parent.lookup.deals", and so on.
While googling for a solution I found an example on plunkr using a parent state variable definition to create a wildcard'ed state environment:
.state(currentState+".deals").
"Boy oh boy", I thought to myself. That's exactly what I need. Well, not.
I was testing it and it on a static "currentState" variable and it worked just fine, thought all I had left to do was to dynamically change the currentState variable between parent states switchings.
It appears that when angular is generating the routes for the first time on loading, it takes the default "currentState" var as a string and defines that state static. So, even though I'm changing the default "currentState" var between route changes, the state is only available to the first generated state definition.
Thank you.
I've posted a feature request on GitHub, just in case I'm the first one in the need for this scenario - or there isn't a proper solution meanwhile for this problem:
github.com/angular-ui/ui-router/issues/1014
Anyhow,
I managed this problem by looping through the parent states and attaching the static child state to them, instead of defining them manually:
angular.forEach(states, function(stateOptions,stateName) {
$stateProvider.state(stateName, stateOptions.options);
if (stateOptions.defaultState){
var dealStateName = stateName+'.deals';
console.log(dealStateName);
$stateProvider.state(dealStateName,{
url: (stateName=='parent.dashboard') ? "deals/:dealId" : "/deals/:dealId",
views:{
'fsItem#parent':{
templateUrl: "/static/html/fsItems/dealNewD.html",
controller: 'dealCtrl',
}
}
});
}
});
The question isn't quite clear, but it sounds like you want a state variable(s) that persists between route changes. I'd suggest using a factory or service. Take a look at Sharing a Variable Between Controllers in AngularJS.

Getting state from URL string with Angular UI Router

I'm using state-based routing (Angular UI Router v0.2.7) in a project and looking for a way to get the current state (name) from a given URL string.
Something like:
$state.get([urlString]) returns stateName:String or state:Object
I need this method to check if a state exists to a given URL because not all URLs are mapped to a state in my project. Using Play Framework as backend, some URLs (e.g., login form) are not mapped to a state because they using different templates then the Angular (main) part of my application.
For those "none-Angular" pages (i.e., not covered by a state) I would do a reload. To identify URLs not covered by a state I need the method mentioned above. Planned to do it like this:
$rootScope.$watch(function() { return $location.path(); }, function(newUrl, oldUrl) {
if(newUrl !== oldUrl) {
if (!$state.get(newUrl)) {
$window.location.assign(newValue);
}
}
}
Already checked the docu but there is no such method.
Any ideas?
It's all or nothing. If you plan to use ui-router make sure all your URLs resolve to a state. If the state doesn't exist it will go to the otherwise state. It's possible to have optional parameters.
An alternative is to use .htaccess redirects to catch the URL and redirect you before it hits the ui-router.
Provide more details and we can see what the best option is.
Try using this will get the current sate name.
var stateName = $state.current.name;

Angular simple routing without templates

I am looking for simple solution with Angular how to handle routes.
I have page generated by server with simple google map and little logic which is in 3 separated controllers.
Now i want to hook routing inside of this. Just simple routing. When I move with map and get new coordinas i want to push them into current url as param "?position=10.11,50.23". I want to use history push state with hashbang backup.
In other part of application i want to listen on change of this parameter (some $watch maybe?). And i want to use same code for detecting change to be used when page is loaded first.
I used CanJS in previous project, where this was absolutely simple, but in Angular i cant event make baby step to correct result :)
PS: this is just cut of minimal usecase.
Thanks
You do not really need angular routing to do that. Routing in angular is usually used to replace ng-view with different templates though it can be used for other things as well.
Your requirements can be met by using the $location service and setting up a $watch:
$scope.$watch(function () { return $location.search().position; }, function (newVal) {
$scope.params = newVal || 'No params';
});
$scope.setParams = function (val) {
$location.search('position', val);
};
Working demo
Launch the live-preview in a separate window to see the parameters change in the window address bar.
Update: The Demo doesn't work on plnkr.co anymore. Possibly because they have changed how they use the embedded mode.
Do not know CanJS, but in angular it is pretty easy as well.
First use: $location.search({parameter1:'someValue', parameter2: 'someOtherValue'}) to set you url -- the history will be updated automatically.
Then the most elegant way to detect any changes in url is to use (one of) two built-in events:
$locationChangeStart and $locationChangeSuccess, like that:
$scope.$on("$locationChangeStart", function (event, next, current) {
// your staff goes here
});
$scope.$on("$locationChangeSuccess", function (event, next, current) {
// or here (or both if needed)
});
To get the search parameters just use $location.search() in one of the above methods.

Categories