Skip to content

Instantly share code, notes, and snippets.

@skiadas
Last active December 19, 2015 21:58
Show Gist options
  • Save skiadas/6023597 to your computer and use it in GitHub Desktop.
Save skiadas/6023597 to your computer and use it in GitHub Desktop.
Brainstorming for a wire.js API/DSL for routing. See https://github.com/cujojs/wire/issues/124

Ok here's some thoughts:

Routing/History protocol

Basic routing format

Basic routing mechanism: A "routes" facet you can add to any object. E.g:

routes = {
	'aroute': 'acomponentmethod',
	....
}

Route pattern language

The following specifies the form route patterns may take.

  1. 'object/new' a literal match.
  2. 'foo/{id}' a variable name enclosed in braces. Will match anything up to a slash, e.g. 'foo/bar'.
  3. 'foo/{id+}' a star will make the variable match the rest of the expression (i.e. including any slashes).
  4. 'foo/{id?}' question mark makes the braced expression optional (i.e. will match 'foo' as well as 'foo/bar').
  5. 'foo/{id*}' optionally matches multiple extra path segments.

Are there things the above won't cover?

Hm these start feeling like mini-regexps.

Are there any real use cases for allowing arbitrary regular expressions in the pattern language? I would not want to make it needlessly complicated.

Accepted routes

This section may need some reworking. See previous section for routing pattern language.

Routes follow the following patterns (this deviates a bit from the regexp approach):

  1. Literal Match: 'object/edit'
  2. Colon-prefixed parameters match between hashes: 'object/:id'. Multiple such parameters allowed
  3. Star-prefixed parameters match rest of expression, or empty: 'object/*rest'
  4. Star itself matches rest of expression, but does not store in special property. E.g. '*' will match all routes.

Question 1: Are there other conditions that would make sense? For example specifying that a match needs to be an integer. Perhaps allow semi-arbitrary regexs with a format like: ':id:regex'. Or perhaps better to keep it simple.

Question 1': Perhaps add a way for optional parameters, and a way to match query strings? Or perhaps matching query strings should be automatic?

Question 2: Should these obey a "first to match activates" rule, or should we let all that match run? If the former, what guarantees do we have on different javascript environments that the order in which the routes are specified is the same as the order in which they will be accessed through a "for in" loop? If the latter, do we mind the fact that there is no easy way to specify a "default" action, only to be ran if others don't match? And what sense would "default action" make, if developers are allowed to attach routes facets on different components, and have different routes rules on each? Would we be talking about "one rule per component"? If not, how do we specify priority amongst components? If we go with "one rule per component", perhaps we can allow a '$default' route option, to run only if no other routes in that component matched.

Question 3: How much do we care about trying to offer a behavior that exactly matches what popular packages that offer routing currently do?

Question 4: Should a route be activated the first time a page loads, if it matches the current URI? Or only on subsequent changes to the URI? Should this be a configurable option?

Question 5: What support should we offer for programmatically adjusting the routes?

Method signatures

Question 6: How should the methods handling routes receive the matched route information? Some possibilities:

  1. First argument is an array of the matches, with the first element being the entire route matched, and subsequent elements matching any other parameters in order. Parameter names are lost. Not my favorite approach.
  2. First argument is an object literal of the matched parameters only. Entire route is lost.
  3. First argument is the entire route, second argument is an object literal of the matched parameters.
  4. First argument is an object literal of the matched parameters. A special key, say $route in it designates the full route.
  5. One argument for each parameter in order. Possibly starting with the full route match. This is what Backbone does, without the full route first.

Option 4 is probably what I would lean towards.

Question 7: Regardless, it is worth thinking as to whether extra optional arguments should be allowed, one matching the "previous/current route", a "from" field so to speak, and possibly another matching the state one could have stored via pushState, and gotten back via popState. But more on that later.

History.pushState

Opt-in or Opt-out

Question 8: Should using the HTML5 History API be opt-in or opt-out? This should likely be a configuration option on the plugin, something akin to:

$plugins: {
    { module: 'wire/route', legacy: false }
}

where legacy: false means that we do NOT try to use the new stuff if it is available. Or perhaps forceHash: true instead?

To me it makes sense to try to use the new stuff if it is available. An issue with that might be someone using a "new url", without hashes, that they got from a new browser, on an older browser. The new stuff sort of also implies your server is in a position to support direct requests to the corresponding URIs. Perhaps then it should be an opt-in thing, i.e. forceHash defaults to true (or legacy defaults to false). Kinda torn on this. I guess it depends on who we expect would be using this.

One possibility: Keep the history stuff as a separate plugin. Let 'wire/route' do its thing as usual, using hashes. But if 'wire/history' is also loaded, it tries to use the HTML5 API. I rather think a single module makes more sense.

Here's how I envision the history plugin might act:

  1. Whenever it has to deal with a uri hash, it checks to see if there is an anchor named with that hash, in which case it just lets it through and doesn't trigger any routing.
  2. All other hashes it turns into absolute URIs. This includes hashes that might have arisen on an old browser and then copy pasted, that were starting somewhere deeper in the application then also had a hash to them. For example www.foo.com/bar#baz would turn into www.foo.com/baz (if there is no anchor named 'baz' that is).
  3. It registers itself to listen to all hashchange events as well as popState events. It turns the hashchange event into a pushState call, and calls appropriate routes. It similarly calls those routes on popState.
  4. It provides a route! reference resolver meant to be used on its own, returning an object with a set method which either tries to change the hash or uses History.pushState depending on the setup, and still triggers the necessary routes. Use for programmatically setting the route. It also offers a replace method for when you want to change the hash/location/trigger routes without creating a new history item.
  5. Alternately, it could instead provide setRoute! and replaceRoute! resolvers that directly return functions.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment