-
Notifications
You must be signed in to change notification settings - Fork 3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[bug] Child state with stateParams will load controller twice if stateParam absent #125
Comments
The problem is that the 'id' parameter is required, but since you're not providing it it gets set to null inside transitionTo(). When $state then updates $location, the URL ends up as /baz/, which then triggers $state again to see what state that location maps to; in this case it maps to foo.baz with id='', which is different from the current state (because '' != null) so another transition is performed. The bug is that transitionTo() should validate that all parameters have been specified, even though currently $state doesn't know if a particular parameter is optional or not. The other option would be treating '' and null the same, or maybe normalizing one to the other, but none of those seems ideal. |
@nateabele what are your thoughts on null vs empty string in this context to work around these types of issues? |
The simplest thing would be to treat Again, the URL is an imperfect representation of application state. I think it's better to explicitly acknowledge this, and let the state manager worry about the details of going from URL to state and vice-versa. The other idea that this suggests is explicit parameter typing, i.e. |
Forgot to conclude: this opens the possibility of having |
@nateabele I really like this suggestion as the type safety added would allow for some interesting defaulting patterns. Speaking of defaults do you see the default values here as configurable on initialization of the state, the stateProvider or none of the above. |
IMO the best way would be to allow |
It is odd that you can only specify params when a URL is also specified. |
So something like the following? .state('mystate', {
params: [
{param1: {default: 'xyz', type: 'integer'}},
{param2: {type: 'string'}}
]
})
or (with url)
.state('mystate', {
url : '/path/to/{param1:integer}/with/{param2:string}',
params: [
{param1: {default: 'xyz'}
]
}) with both of these resulting in the same $stateParams. |
Unless we're planning to allow params to be decorated with some information other than a default value, I think |
Doh, and great thought :) |
You can specify state.params even when there is no URL, but currently it's just an array of param names. I think it should look something like this:
I think most of the responsibility for this stuff should be with UrlMatcher; the optional parameters map would be passed into the UrlMatcherFactory where suitable defaults and normalization would be applied. In simple cases UrlMatcher would infer most options from the string pattern. $state can access the meta-data via UrlMatcher.parameters() as it does now, to get at flags like 'optional' or 'dynamic' (dynamic being an example of parameter meta-data that UrlMatcher itself is not interested in and just passing through unmodified). One feature that is somewhat difficult to cover is wildcard query parameter support -- i.e. the ability to have all query parameters show up in $stateParams. The ability to explicitly enumerate all valid parameters is very useful, and without it deciding changes in which parameters apply to which state is difficult. Maybe UrlMatcher could support a special syntax like "...?" to enable wild card parameter support, but map this to a single parameter with custom encode/decode so that all query parameter get decoded into a single state parameter called '' that happens to be a key-value map. So $state itself would see it as one parameter. This also solves the problem of wildcard search parameters trampling over the names of path parameters. The only added complexity for $state would be to support equality tests between complex parameter values (which could be delegated to a param.equals() function). One thing I'm not sure is a good idea is default values -- if you've got "/place/:name" and name defaults to 'home', wouldn't that imply that navigating to "/place/" would have to do redirect to "/place/home"? I suppose this might not be too hard to arrange. In case any defaulted parameters are empty, UrlMatcher.exec() would (optionally) return a redirect string instead of the parameters map, and $urlRouter would simply apply the redirect instead of calling through to the handler -- or some other similar mechanism. |
Yeah, I meant you can't specify both, because currently it throws an error.
The I see a few things that I think could be improved though:
From my experience of writing and rewriting URL routers for major web frameworks at least 4 times over the past ~6 years, mixing optional vs. required parameters with wildcard parameters coming from different places is extremely dangerous territory. You end up trying to hack around logical inconsistencies that are impossible to resolve. I definitely concur with your idea of keeping query parameters separate, though maybe with a legal identifier like
Trust me, they're super-useful, and simplify a bunch of things (some of which are listed above). :-)
Nope, that would mean This is sort of the basis for how route design is done server-side, and it should map pretty easily to state trees. Let me know if you need a more in-depth explanation. |
Hm yeah combining encode/decode is a good idea. Why don't we just call it the "type" of the parameter, and have the encode and decode methods on the type object, as well as any other ones we might need (equals() comes to mind for comparing complex values). The type could also provide a regex pattern so that it doesn't need to be repeated each time you use e.g. an 'int' parameter (obviously the concrete pattern can still override). I think the problem with handling default without a redirect is that the URL will change anyway as soon as the change of a dynamic parameter causes an update to the location. E.g. if you had '/places/{name}?orderby' with orderby being dynamic, changing orderby would cause the state to be serialized again and you'd end up changing it from '/places' to '/places/home?orderby=whatever'. It's not a huge problem obviously, but I think it's nicer if the URL reflects the canonical representation of the state (which is also good practice when doing it server-side in terms of SEO etc). We could extend the curly-brace syntax for UrlMatcher so that a type name can be specified instead of a regexp (they should be easy to distinguish by restricting type names to identifiers), and allow them to be registered via $urlMatcherFactory.registerType() or some such method. Possibly even default values (as long as they don't contain special characters). Could end up with something like
I don't really like having 'reloadOn' as a separate array, because I think to reload on a parameter should be the default behaviour as it is now, and just having the ability to specify a params object that gets merged with whatever options we parse out of the URL pattern is more generic. I think 'dynamic' would also imply a more sophisticated behaviour than simply not reloading, namely the two-way binding to stateParams. So the above would be equivalent to
|
That works.
Okay I see what you mean. That's fine with me. I think for URL parameters the default values can be inserted in the URL, but for querystring parameters, if the value matches the default, it should just be omitted.
I'm wary of making the syntax overly complex. I think if we just allow |
Hey Guys! I'm going to take a run at the type encoding part of this implementation and send in a pull request. I plan to implement the url: '/myrouter/:id?startOn'
params: [
"id": {
// supports integer, boolean
type: "integer",
def: 0
},
"startOn": {
type: {
// evaluated for equality.
equals: function (otherObj) {},
// returns complex type from $location string
decode: function () {},
// returns string representation of complex type to be put in $location
encode: function () {},
def: new Date()
}
}
] Here is how I believe I should implement this, but I would appreciate your thoughts:
Thanks!! |
I've already been able to implement a work around for this, but it would be great if we could embed it into the actual router. I used this: http://mjtemplate.org/examples/rison.html I wrapped it in a service, then created a wrapper service that injected $state and $rison (see above). This allowed me to treat stateful parameters as a hash table, with the following functions:
I chose a hash table because I felt as though there wasn't any real use case that it couldn't handle. However, you can really encode infinitely deep objects using the default get, set, and del, the hash functionality is simply there for convenience to access 1st level object members. I suggest that with UI-Router, we allow the developer to pass in a plain javascript object as a parameter that gets parsed using some sort of syntax (like Rison), and when fetched is parsed back into object state. If it isn't an object, it just works as it always has and thus, no regression. However, what I think would be a real win in terms of functionality would be the ability to inject URL parameters with a function (lets call it ".alter(state,params,options)" ) that manipulates the URL without actually RELOADING the state (if oldState == newState, do nothing!, but ONLY for this method). Then, emit some sort of event on $rootScope to allow controllers to handle the state parameters change in their own way. This is ABSOLUTELY ESSENTIAL for any sort of breadcrumbs functionality on large-scale apps (like the one I am spearheading at work, and USING THIS MODULE), because the user needs to be able to reproduce a given state to the Nth degree of detail when copying and pasting the URL to a friend or bookmarking (limitless encoding, and sharing ALL reproduce-able stateful data with the state parameters so that the precise user experience can be reproduced seamlessly with a URL). This would need to work vertically as well, allowing the parameters of parent states to be altered by children, and the parameters of children to be altered by parents (the tricky part). A way that I would suggest to accomplish this would be to provide a separate namespace within the $state service that allows the developer to change to a particular state as 'focused' (as described above), and allows the developer to manipulate stateful parameters and flush these changes to the URL. This extends the API a bit on the $state service, and would be regression proof because the functionality would not interact with what is already in existence (and thus being used currently). Let me know what you guys think! I think this approach will be a lot of work, but I think it will unlock huge potential with this module. Consider this an RFC |
@toddhgardner Right on, that sounds perfect. One thing @ksperling and I discussed in a separate issue (I'll see if I can find it) is the idea of making type definitions reusable by attaching them to $urlMatcherFactoryProvider.param("typeName", {
equals: function (typeObj, otherObj) {},
decode: function (typeObj) {},
encode: function (value) {}
}); @thebigredgeek Seems like the most important parts of what you're looking for are accomplishable by combining the above, with two-way binding for |
@nateabele that sounds great--could totally use that as well, and should be easy enough to add in if we already have support for builtin types like |
I had to make a few minor environment fixes just to get development started. Do you want me to send a pull request in?
Also, I see 4 tests failing in Firefox 23. |
@toddhgardner Yeah, go ahead, thanks. I normally do my testing with |
@janders223 Well-played, sir. Good on you. I'm fine merging this with the caveat that it's gonna get broken in 0.4 when we finish off typed parameters. That cool with everyone? |
@janders223 Alternatively, if you wanna pick up the torch on the typed-parameter PR I can guide you on where to go with it. |
@janders223 and @nateabele I'm fine with that. It atleast solves a need that many seem to be having and if broken in 0.4 we can try to solve the issue again. |
@nateabele just for some context, Jim, Michael and I were all hanging out having some beers. Michael mentioned his complaint about the lack of this feature. I told him to +1 it so we could keep track of popularity. You replied in only a way that you could. Motivated us to see if we could actually add the feature and blam! |
@nateabele I'm cool with trying to run with that. I would love to give back to a project that I love. |
Fixes #125 Add reloadOnSearch property to stateConfig
@gamegenius86 @janders223 Merged #593. To be clear, when typed parameters are implemented, it's not that this feature will go away, it's just that the implementation will be different (read: better :-)). |
@nateabele Roger that. I should have some time coming to dig into that thread, and look into the typed parameters. |
Did ui.router come up with a solution to allow $state.go() to update url :params w/out reloading the state controller? |
@itsleeowen not yet. Only search params, |
@timkindberg can you explain? i didnt understand what you mean by Only search params, ?param. |
So if you have: .state('search', {
url: '/search?query',
reloadOnSearch: false
}) Then in you app you can use |
The spec may help too, though its not so clear to follow. Lines 152 to 162 in a100dea
|
Thanks. Well I did do some tests and it seems reloadOnSearch=false works also for URL params. Here's a plunk - http://plnkr.co/edit/2I8jFk?p=preview I can then listen on locationChangeSuccess event for changes on parameters. One issue, of course, is how to extract the parameters from the location.path(). Any ideas, by any chance? |
Interesting! It wasn't really meant to work with path params, so you are kind of on your own with that one, but you could always parse them yourself from the $location.path() value. |
@timkindberg If it's not meant to work with path params, does that make it a bug? In my case, I want to prevent reload on search, but still reload on param changes. I can't do that because of this issue. |
Yeah technically, but it will probably be fixed by just rewriting the parameter stuff. We are gonna do typed parameters one of these days. |
We were using 0.2.10, updating to 0.2.13 fixed it without us having to do anything about it. |
yeh |
Hard to explain, so I've whipped up a minimal repro
Go to
index.html#/bar
Click the
baz
button and watch the console.log. It will print out the scope twice. Whats worse is one of these scopes is orphaned after the digest and loses its $parent.If the
baz 1
is clicked, theid
route param is supplied and the bug is not occur.Also, would it be worth defaulting
toParams
intransitionTo(to, toParams)
to{}
so the user does not need to specify the empty object for when params should be empty.I tried finding the bug myself but I couldn't find it and don't have the time to investigate further.
Thanks :)
The text was updated successfully, but these errors were encountered: