Skip to content

Quick start

_gt3 edited this page Jul 22, 2017 · 2 revisions

Quick start

Imagine we're tasked to set up routes for a news portal startup. We'll use a rudimentary navigation structure for the purpose of this exercise.

                +----------+
     +----------+   root   +---------+
     |          +----------+         |
     |                               |
     |                               |
+----+-----+                   +-----+----+
| weather  |                +--+   news   +--+
+----------+                |  +----------+  |
                            |                |
                       +----+-----+    +-----+----+
                       | politics |    |  sports  |
                       +----------+    +----------+

  • Code to setup centralized routing (matching and resolution) as shown below
import { spec, match, prefixMatch, container } from 'ultra'

let next = console.log.bind(console), err = console.warn.bind(console)

// when route resolves call next on exact match, err on partial match
// log route result to the console (warn on partial match)

let matchers = [
  match(spec('/weather')(next)), //a
  prefixMatch('/news', match(spec('/', '/politics', '/sports')(next, err))), //b
  match(spec('/')(next)) //c
]

Right away you'll notice that ordering matters (like we'd expect in other forms of pattern matching). For example, there's a valid reason why c cannot precede a or b, while a or b could switch places based on the requirements given to us.

✅ Equally important to note that no more than one match ever gets resolved.

The API usage so far consists of three functions: spec, match, and prefixMatch, that are used to build our routing logic.

  • spec - An object that identifies a route with one or more path keys, and an action to invoke on match.
    • fn(...pathKeys) => fn(...callbacks) => object
    • Order of path keys matters: in b for intance, / represents the primary path key, whereas /politics and /sports are secondary and could switch places
    • Position of function arguments is fixed: 0:exact match, 1:partial match, 2:neither
  • match - Route configuration object that provides entry point to one or more specs.
    • fn(spec or [...specs], check, preMatch) => object
    • Provide a spec object or an array of spec objects
    • Arguments check and preMatch will be explored later
  • prefixMatch - Type of match object that wraps given match object with a prefix. i.e. result of the prefix match determines invocation of containing match object.
    • fn(prefix, match, preMatch) => object
    • In our example, /news path key is used as prefix for the containing match

✅ Notice how route configuration is composed out of small pieces. prefixMatch composes over match and match composes over spec.

Next we'll make a container compose over matches to enable navigation.

  • Integrate with browser's PushState API to kickoff routing
// run container to begin interacting with the browser
let ultra = container(matchers)

// use ultra to navigate
ultra.push('/news') // resolve: b.next
ultra.push('/news/sports') // resolve: b.next
ultra.push('/news/foo') // resolve: b.err

:shipit: That was easy! Although don't hit deploy just yet. The weather route could really use user's location to provide a better experience. The challenge is to figure out how to map url query string ?loc=<zip> to the weather route.

  • Treat query string and hash fragments integral to routing
import { check, parseQS, prependPath } from 'ultra'

// add :zip identifier to our path key
let weatherSpec = spec('/weather','/weather/:zip')(next, err)

// validate value of identifier with check
let zipCheck = check(':zip')(/^[0-9]{5}$/) //allow 5 digits

// extract loc value from query string and append to path
let addZip = ({qs, path}) => qs ? prependPath(parseQS(qs, ['loc']), path) : path

let weatherMatch = match(weatherSpec, zipCheck, addZip) //a*

// clone container: replace match (a -> a*), replace ultra object
ultra = container([weatherMatch, ...ultra.matchers.slice(1)], null, ultra)

// navigate
ultra.push('/weather') //resolve: a*.next

// assume query param loc is set externally
ultra.push('/weather?loc=90210') //resolve: a*.next with :zip = 90210
ultra.push('/weather?loc=abc') //resolve: a*.err

We were able to accomplish 3 critical tasks here:

  • Create a new match for /weather and /weather/:zip
  • Extract value of :zip from query string and validate
  • Update our container with the new matchers

Notice the call to match: match(weatherSpec, zipCheck, addZip) //a*

  • addZip is our preMatch callback function that's invoked before the specs
  • zipCheck, a regexp literal, provides validation to determine match

Finally a dry-run of our hard work should produce a similar result.

  • News portal navigation log

news website navigation log

Test-drive

✅ Routing code is reused in these examples. Generally a good measure to suggest that the abstraction hit a sweet spot!

This is true, in part because component-based design afforded us this level of flexibility: choosing how micro or macro you want your module to be.

We're often not seeking for more power. We're seeking for more principled ways of using our existing power.

Quote from one of my favorite React talks by Cheng Lou: The Spectrum of Abstraction #throwback

Next, explore the concepts section or try out code samples.

Clone this wiki locally