-
Notifications
You must be signed in to change notification settings - Fork 3k
Closed
Description
We've had troubles with double-transitions in many different corner cases. The root cause of this is a mismatch between the $stateParams, and the params decoded from the URL. The solution is to ensure that all values that can map to a url, also map from the url exactly.
Old example (already fixed).
Given the state .state('foo', { url: "/foo/:bar" }) and the transition $state.go("foo", { bar: undefined })
- transitionTo('foo', { bar: undefined })
- $state.params is set to
{ bar: undefined } - $state.$current.url.format($state.params)) yields
/foo/ - the url is set via
$urlRouter.push("/foo/");
- $state.params is set to
$urlRouter.listenhandles the $locationChangeSuccess event- The url has changed (from
""to"/foo/") - Find the correct matcher (it finds
foostate's matcher) - The UrlMatcher parses
"/foo/"and matches the empty string. It returns{ bar: "" } - Call transitionTo with the matched details
- The url has changed (from
- $state.transitionTo("foo", { bar: "" });
- Check if toParams match fromParams
{bar: undefined}does not equal{ bar: "" }, so we have "new params"- continue transition to
foowith params{bar: ""}<--- Double transition
New example
Given the state .state('foo', { url: "/foo", params: { bar: null } }) and the transition $state.go("foo", { bar: { blah: 45 } })
- transitionTo('foo', { bar: { blah: 45 } })
- $state.params is set to
{ bar: { blah: 45 } } - $state.$current.url.format($state.params)) yields
/foo - the url is set via
$urlRouter.push("/foo");
- $state.params is set to
$urlRouter.listenhandles the $locationChangeSuccess event- The url has changed (from
""to"/foo") - Find the correct matcher (it finds
foostate's matcher) - The UrlMatcher parses
"/foo"and returns any matched params. It returns{ }because there are no params in the url string. - Call transitionTo with the matched details
- The url has changed (from
- $state.transitionTo("foo", { });
- Check if toParams match fromParams
{bar: { blah: 45 } }does not equal{ }, so we have "new params"- continue transition to
foowith params{ }<--- Double transition
Approach
Part 1
- Ensure parameters map cleanly between a type and a url string
- The Type system provides the mechanism.
- Values in $stateParams are always typed
- transitionTo gets decodes the incoming toParams before setting $stateParams
- Ensure all parameters map cleanly between empty string, null, and undefined
- Preprocess parameters sent to the transitionTo method and apply explicit mappings
- Param.$value applies the replacement mapping before decoding the value using the Type
- getReplace function:
function getReplace(config, arrayMode, isOptional, squash) {
var replace, configuredKeys, defaultPolicy = [
{ from: "", to: (isOptional || arrayMode ? undefined : "") },
{ from: null, to: (isOptional || arrayMode ? undefined : "") }
];
replace = isArray(config.replace) ? config.replace : [];
if (isString(squash))
replace.push({ from: squash, to: undefined });
configuredKeys = map(replace, function(item) { return item.from; } );
return filter(defaultPolicy, function(item) { return indexOf(configuredKeys, item.from) === -1; }).concat(replace);
}
Part 2
Transitions to states with non-url parameters should be handled correctly too. This is problematic when we re-synchronize the URL, because non-url parameters are obviously not addressed by parsing the URL.
- Set
inherit: truewhen performing URL matching- When transitioning to the same state via re-sync url, or via a url match to a child state, the non-url parameters are inherited correctly.
- Potential for unintended inherits?
- After pushing the URL to
$urlRouterfollowing a successful transition, do not attempt to resynchronize in response to a$locationChangeSuccessevent.- The transition was the cause of the location change, so re-synchronizing via the URL should be unnecessary.
- This is a safety mechanism only. The approach in part 1 should allow url-resync to exactly match the current state anyway.
Metadata
Metadata
Assignees
Labels
No labels