AngularJS - Router UI - Defining a wildcard parented state - javascript

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.

Related

Angular - recalculate a variable on every change

I have a variable that stores the available cars at any moment. Is there a way to automatically re-evaluate this function on every change?
Just using this.carFactory.available in this case is not a solution, because this example I'm showing is simplified - the real calculation in my project is alot more complex.
calculateAvailableCars(){
this.carFactory.available.forEach(function(item){
this.availableCars.push(car.id);
}.bind(this));
}
How could I do this in Angular 2? In Angular JS there was the possibility to $watch a function.
I could of course manually call this function everytime something changes, but it would be nice not to have to call this function in every part of the application that can change the data.
Using template function reference with auto change detection
You can use this function output on template:
carOutput(): cars[] {
this.calculateAvailableCars()
return this.availableCars;
}
and use output on template:
<p>My car ratio is {{ carOutput() }} </p>
However this will trigger very aggressive change detection strategy on this variable. This solution is the simpliest one, but from engineering perspective rather worst: consumes tons of unnecessary function calls. One note, that hosting element must not be set to detect changes onPush.
Separate data model to parent component and pass as property to child
You can store car list display in separate component, and pass new car array as input property to this component:
<car-display [cars]="availableCars"></car-display>
Then you can set changeDetetcion policy in this component to onPush, and each time input property bind to availableCars will change, <car-display> will re-render.
If update relays on some host binding
If some external host action is triggering new cars calculation, then hostBinding may help:
#hostListener(`hover`) recalculateCars() {
this.calculateAvailableCars()
}
And finally, (because you describe your use case quite cryptically, without many details, thus I'm scratching all possible scenarios) if some external component action shall trigger re-calculation, you can hook to ngLifecycle ngOnChanges() if for example external input property change shall re-trigger cars calculation.
In other words and summing all that up, it depends who and from where triggers changes, that shall re-trigger available cars recalculation.
And very important, see an answer from #chiril.sarajiu, because what we are trying to work around here can be handled automatically by single observable. This requires additional setup (service, provide observable to components, e.c.t.) but it's worth.
--- EDIT ---
If each variable change shall retrigger data
As OP clarified, that changes are related with model bound to component. So another option with mentioned by #marvstar is using set, where each model variable change will retrigger fetching function:
modelSchangeSubject: Subject<Model> = new Subject<Model>();
ngOnInitt() {
this.modelSchangeSubject
.subscribe((v: Model) => {
this.calculateAvailableCars()
})
}
/* Rest of controller code */
set modelBounded(v: Model) {
this.modelSchangeSubject.next(v);
}
You need RxJS. What you do is you create a data service, which will store an Observable (in my case a BehaviorSubject, which is mostly the same, but in my case I start with a value).
export class DataService {
private dataStorage$ = new BehaviorSubject(null); //here is the data you start with
get getDataStorage() {
return this.dataStorage$.asObservable(); // so you won't be able to change it outside the service
}
set setDataStorage(data: any) {
this.dataStorage$.next(data);
}
}
Then you subscribe to this data changes everywhere you need to:
constructor(private dataService: DataService){}
ngOnInit() {
this.dataService.getDataStorage.subscribe((data) => this.calculateAvailableCars(data));
}
calculateAvailableCars(){
this.carFactory.available.forEach(function(item){
this.availableCars.push(car.id);
}.bind(this));
}
Read more about best practices of using RxJS in Angular, as there can be quite a bit of pitfalls and problems.
Try using setter and getter.
private _YourVariable:any;
public set YourVariable(value:any){
this._YourVariable = value;
//do your logik stuff here like. calculateAvailableCars
}
public get YourVariable():any{
return this._YourVariable ;
}

Go to parent route in Angular 2

Let's say I have a component that creates or edits a thing.
The url for the creation is /things/create and the url for edition is /things/edit/4. These routes have the same parent.
Once I fill in the form, I call some webservice and then I go back to my previous state which was /things.
How do I tell angular to go back to the parent route?
I could use this.router.navigate(['../'], {relativeTo : this.route}); but ['../'] would only work for /things/create. For /things/edit/4 it would go back to /thing/edit which doesn't exist.
This component is used in two different places, one of which has an extra step before the creation so I can't use back().
Logic based on the current url
if(router.url.indexOf('create')>= 0){
this.router.navigate(['../'], {relativeTo : this.route});
}else {
this.router.navigate(['../../'], {relativeTo : this.route});
}
so if your current URL path contains the create you know you just have to go back one level, otherwise you go back two levels for the edit.
Use Input to get base URL
Another approach might be to have an #Input for the base URL to go back to. So each component that initializes your component has to pass it the parent url.
{
#Input
parentUrl;
this.router.navigate[parentUrl];
}
You can use
this.router.navigate['/things']
So you should be able to check for the id of the thing in the ActivatedRoute params to get the context if the component is creating or editing a component. If there is a param['thingId'] then go to ../../ otherwise go to ../
May be this idea a bit old school but I am sure it will work and is quite simplistic to implement. You can add query param called 'source' and navigate to 'source once the operation is done.

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?

naming states in angularjs ui-router

instead of using ng.router i am switching to ui.router in my angularjs application.
I came across a problem about state names in config. When I use '.' character in state name, routing doesn't work. Code I gave below doesn't route to 'views/subcategories.html' when I enter "localhost/#/categories/5" in my browser url.
$stateProvider
.state('categories',
{
url:'/categories',
controller:'CategoriesController',
templateUrl:'views/categories.html'
})
.state('sub.categories',{
url:'/categories/:subID',
controller: 'SubCategoriesController',
templateUrl:'views/subcategories.html'
});
However when I use 'subcategories' instead of 'sub.categories' it works. I am asking this question because I saw state names with '.' in a lot of tutorials about ui.router. Do I miss some detail here?
with dot syntax in name of states, you can a nested states. The nested state, inherit params and urls from parent state.
First, you need start the name of nested state, with parent state:
'categories.sub' // instead 'sub.categories'
Then, to localhost/#/categories/5, set your nested state like below:
$stateProvider
.state('categories',
{
url:'/categories',
controller:'CategoriesController',
templateUrl:'views/categories.html'
})
.state('categories.sub',{
url:'/:subID',
controller: 'SubCategoriesController',
templateUrl:'views/subcategories.html'
});
Note, the state 'categories.sub', in url, dont need '/categories', because this is inherit from parent state 'categories'
In ui-router, periods are used to indicate nested states.
Using sub.categories would make the categories state a child of the sub state.
More info can be found here

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

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.

Categories