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.listen
handles the $locationChangeSuccess event- The url has changed (from
""
to"/foo/"
) - Find the correct matcher (it finds
foo
state'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
foo
with 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.listen
handles the $locationChangeSuccess event- The url has changed (from
""
to"/foo"
) - Find the correct matcher (it finds
foo
state'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
foo
with 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: true
when 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
$urlRouter
following a successful transition, do not attempt to resynchronize in response to a$locationChangeSuccess
event.- 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