angular ui routes state duplication due to multiple urls - javascript

I am using angularjs with angular ui routes and I have a problem with multiple urls for route.
I have a state called "bookDetails". Book have unique id and unique name and therefore can be accessed both using id or name:
/books/:bookName
/books/id/:bookId
Those two routes are actually the same state. But I can't use the same state for multiple routes. Therefore I have to split it to two states:
$stateProvider.state('booByName', {
url: '/books/:bookName',
templateUrl: '/templates/book-details.html',
controller: bookController
})
.state('bookById', {
url: '/books/id/:bookId',
templateUrl: '/templates/book-details.html',
controller: bookController
});
Although I have code duplication, I can tolerate this. The problem begins when I have another view related to book: bookReaders. bookReaders shows the user all the user that read this book.
The ideal thing is to have "bookDetails" state and "bookDetails.bookReaders" substate. But because I have multiple routes for book, I have to create another two states:
bookByName.bookReaders and bookById.bookReaders and duplicate the states as well.
What is the best practice for such thing? How can I prevent states duplication?

Are you using UI Router? If so, you can use $urlMatcherFactory and UrlMatchers instead of writing the absolute URL. Then, define one state and a matcher that will match on both ID and Name, setting the state params accordingly.

Related

Angular 6 removes query string from url

Example
http://localhost:4200/login?aUasas129198
resolves to
http://localhost:4200/login
What should I do if I want the value after '?'
I tried doing
{ path: 'login/:id', component: LoginComponent },
But it did not work
I also tried
console.log(this.route.snapshot.queryParams);
console.log(this.route.snapshot.params);
But they both return empty object. What should I do now please help
If it’s unavoidable that Angular redirects you immediately and loses the query parameters, you could subscribe to router events and on each NavigationStart for login route get a hold of route’s queryParamMap or snapshot.paramMap before they’re lost in redirection, then you can e.g. pass it to a service and do whatever you wanted to do with it.
Or, as another alternative, you could look into configuring your router with queryParamsHandling: 'preserve', in which case it should pass the query params to the next route (see the section in Angular docs linked below for more on this).
I worked with a project that made use of query params in Angular 5, IIRC I don’t think it would redirect on its own so I’d recommend to look elsewhere in your project but I may be wrong.
See also
Routing & Navigation → Query parameters and fragments in Angular docs
Angular Route Start and Route End Events on StackOverflow
Actually, You are not passing the value in any key:
http://localhost:4200/login?aUasas129198
The proper way should be:
http://localhost:4200/login?anykey=aUasas129198
// get it as
// this._ActivatedRoute.queryParams.subscribe()
If you are using the URI as you shown in your question as:
{ path: 'login/:id', component: LoginComponent }
Then you should pass the value to id as:
http://localhost:4200/login/aUasas129198
// remember the '/' after the login that you didn't use.
// get it as
// this._ActivatedRoute.snapshot.params.id

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.

In Ember.js, when you manually declare a view and its corresponding controller, is there a notion of scoping it to the current application state?

As an example, one particular application state may have a home view that just renders some background container,
App.EditView = Ember.View.extend({
templateName: 'edit-template',
})
App.EditController = Ember.ObjectController.extend({
title: 'Edit state',
})
Which are instantiated when I navigate to this state:
App.editRouter = Ember.Route.extend({
route: '/edit',
connectOutlets: function( router, context ){
router.get('applicationController').connectOutlet( 'mainOutlet', 'edit' )
}
})
Once here, the user may manually declare new div elements which map to new view and controller ( and model but that's no super relevant here ), the new div may or may not be a child of the div rendered by editView.
The current way I'm doing it
App.smallView1 = App.SmallView.create({
controller: App.smallController1
}).append()
App.smallController1 = App.SmallController.create()
As you see, nothing here indicates under what state are the view and controller declared.
What I'm confused about:
What is the relationship between this view-controller pair and the instance of EditView and EditController?
What is the relationship between the pair and the editRouter?
Should there be dependancies that need to explicitly specified?
This view-controller pair doesn't seem to be used by the router, so you'd have to create a route for them and connect the outlet with your view-controller pair, unless you want to append this view to an element that won't change per route. It also doesn't have any relationship with the other view-controller pair.
As for your questions:
What is the relationship between this view-controller pair and the
instance of EditView and EditController?
A: The code, as it stands, presents no direct relationship between the small view and controller with edit view and controller, but the difference is that the pair EditView and EditController are not "created", but "given" to the application, which will take care of instantiating that type when required through its own initialization logic (initialize) or when creating the instance of a view when it's required. The pair smallView1 and smallController1 will probably not be good for the router as their instance names end with "1" and I'm not sure if Ember expects that, but anyway, these are being instantiated and directly attached to the application as "living" objects, which is not required when using the router. Does this answer your question?
What is the relationship between the pair and the editRouter?
A: In your code, editRouter is the definition of what state (since Route extends State) your application is when you are on that route; this means that the framework understands that when you are on a given state you need some specific things to occur, for example, loading the view that state requires, loading the data that view should display, etc... this is done through connectOutlet, so for this particular route you don't have any relationship of any kind with smallView1 or smallController1 unless you use a different signature of connectOutlet to specify the viewClass and controller manually.
Should there be dependancies that need to explicitly specified?
A: Yes. When using the Router, your application must have a controller named ApplicationController, and when you call connectOutlet, the name you pass must be corresponding to one view and controller (I think the controller might not me required, but I'm not sure at the moment). So if you say connectOutlet('about'), the framework will look for a view named AboutView as per convention, then will instantiate this view and render on the appropriate outlet in the container view.
How will the controller access the router if it needs to?
A: At any point in your application you can access the router with App.router assuming your application is named "App" and your router was named "Router", so in any of your methods in your controller you can, for example, use the router to transition: App.router.transitionTo('root.index.home').

Categories