Skip to content

Double transitions - analysis and fix #1573

Closed
@christopherthielen

Description

@christopherthielen

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/");
  • $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
  • $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");
  • $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
  • $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

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions