|
| 1 | +# A Guide to `purescript-routing` |
| 2 | + |
| 3 | +A couple notes upfront: |
| 4 | + |
| 5 | +* This library facilitates hash-based routing. If you're looking to do pushstate routing with the [history](https://developer.mozilla.org/en-US/docs/Web/API/History_API) object, then you are in the wrong place. |
| 6 | +* Routes are declared using [applicative](https://pursuit.purescript.org/packages/purescript-prelude/0.1.4/docs/Prelude#t:Applicative) syntax. If you're not yet comfortable with applicatives, see this [chapter](http://learnyouahaskell.com/functors-applicative-functors-and-monoids#applicative-functors) or this [paper](http://www.staff.city.ac.uk/~ross/papers/Applicative.pdf). |
| 7 | + |
| 8 | +####Usage |
| 9 | + |
| 10 | +First, define some locations: |
| 11 | + |
| 12 | +```purescript |
| 13 | +
|
| 14 | +module SolarSystemRoutes where |
| 15 | +
|
| 16 | +import Prelude |
| 17 | +import Control.Alt ((<|>)) |
| 18 | +import Control.Apply |
| 19 | +import Data.Functor |
| 20 | +import Data.Map |
| 21 | +import Routing |
| 22 | +import Routing.Match |
| 23 | +import Routing.Match.Class |
| 24 | +import Routing.Hash |
| 25 | +import Data.Int (floor) |
| 26 | +
|
| 27 | +data Locations |
| 28 | + = Home |
| 29 | + | Jupiter -- The place where Boys go to get 'stupider'. |
| 30 | + | Mars -- Girls seek fame here. |
| 31 | + | MPC Int -- Minor Planet Circular (poor Pluto, et al.) |
| 32 | + | Moon String -- They're not all made of cheese. |
| 33 | + | PlanetWithAttributes (Map String String) -- Looking for a new home? |
| 34 | +
|
| 35 | +``` |
| 36 | +We'll use `Locations` instead of `Routes` to avoid confusion with the `Route` (singular) type exposed by the library. But let's discuss the types later. For now, we'll see it in action: |
| 37 | + |
| 38 | +```purescript |
| 39 | +
|
| 40 | +homeSlash :: Match Unit |
| 41 | +homeSlash = lit "" |
| 42 | +
|
| 43 | +int :: Match Int |
| 44 | +int = floor <$> num |
| 45 | +
|
| 46 | +home :: Match Locations |
| 47 | +home = Home <$ oneSlash |
| 48 | +
|
| 49 | +jupiter :: Match Locations |
| 50 | +jupiter = Jupiter <$ (homeSlash *> lit "jupiter") |
| 51 | +
|
| 52 | +mars :: Match Locations |
| 53 | +mars = Mars <$ (homeSlash *> lit "mars" *> lit "hollywood") |
| 54 | +
|
| 55 | +mpc :: Match Locations |
| 56 | +mpc = MPC <$> (homeSlash *> lit "mpc" *> int) |
| 57 | +
|
| 58 | +moon :: Match Locations |
| 59 | +moon = Moon <$> (homeSlash *> lit "moon" *> str) |
| 60 | +
|
| 61 | +planetWithAttributes :: Match Locations |
| 62 | +planetWithAttributes = PlanetWithAttributes <$> (homeSlash *> lit "planet" *> params) |
| 63 | +
|
| 64 | +``` |
| 65 | + |
| 66 | +So what's all this? `purescript-routing` provides a `Match` type that is an instance of the `MatchClass`. The `MatchClass` encompasses types that provide the route-matching primitives: |
| 67 | +* `lit`: A function that takes a string, and returns a `Match` that can be used to match the provided string. |
| 68 | +* `str`: A `Match` that captures a string. |
| 69 | +* `num`: A `Match` that captures a javascript number. |
| 70 | +* `param`: A function that takes a parameter key (i.e., `String`), and returns a `Match` that will pluck the value associated with the key from the query parameter block. |
| 71 | +* `params`: A `Match` that captures the query parameters. |
| 72 | +* `bool`: A `Match` for `true` or `false`. |
| 73 | + |
| 74 | + |
| 75 | +(Note that `lit ""` -- aliased above as `homeSlash` -- matches a single url forward-slash.) |
| 76 | + |
| 77 | +`Match` is functor. So we can map over it. Above, for example, we map the `floor` function over `num :: Match Number` and we get `int :: Match Int`. We map our `Locations` constructors over other `Match`s to yield a `Match Locations`. |
| 78 | + |
| 79 | +`Match` is a newtype that looks like this: |
| 80 | + |
| 81 | +```purescript |
| 82 | +newtype Match a = Match (Route -> V (Free MatchError) (Tuple Route a)) |
| 83 | +``` |
| 84 | + |
| 85 | +Don't worry too much about the scary type for now. Essentially, `Match` wraps a function that takes a `Route` (represented as a `List RoutePart`, where `RouteParts` is either the stuff between url slashes or the query parameters) and returns a `V` functor. `V` stands for 'validation' and you can think of it as an `Either` that can return more than one error upon failure. So, in essence, something of type `Match Locations` is a function from `Route -> Either Errors (Tuple (Rest-of-RoutePart-List) Locations)`. |
| 86 | + |
| 87 | +The real magic happens in `Match`'s `Apply (i.e., <*>)` implementation. |
| 88 | + |
| 89 | +The `Apply` instance for `Match` provides the plumbing that allows you to compose your routes. The `<*>`, `*>` and `<*` operators are your friends. Like any other applicative, `(leftMatch *> rightMatch)` performs the left `Match`, discards a successful result, and returns the value of the right `Match`. `(leftMatch <* rightMatch)` does the converse. So, `MPC <$> (homeSlash *> lit "mpc" *> int)` maps the `MPC` constructor function over a `Match` that will parse a slash (and discard it), parse the literal string "mpc" (discarding it too), and return the parsed integer. |
| 90 | + |
| 91 | +Routes are combined using `Match`'s `Alt (<|>)` instance. |
| 92 | + |
| 93 | +```purescript |
| 94 | +routing :: Match Locations |
| 95 | +routing = |
| 96 | + jupiter <|> |
| 97 | + mars <|> |
| 98 | + mpc <|> |
| 99 | + moon <|> |
| 100 | + home |
| 101 | +``` |
| 102 | +`jupiter (<|>) mars` means "try Jupiter, and if that fails, try Mars". Like many routing DSLs, this means that more general routes come after more specific ones. Thus, `home` (i.e., "/") comes last (unless you only ever want to go home). |
| 103 | + |
| 104 | +We've got routes. Now what do we do with them? |
| 105 | + |
| 106 | +`purescript-routing` provides a couple functions that help you make use of your `Match`s. |
| 107 | + |
| 108 | +```purescript |
| 109 | +matches :: forall e a. Match a -> (Maybe a -> a -> Eff e Unit) -> Eff e Unit |
| 110 | +``` |
| 111 | + |
| 112 | +and |
| 113 | + |
| 114 | +```purescript |
| 115 | +matchesAff :: forall e a. Match a -> Aff e (Tuple (Maybe a) a) |
| 116 | +``` |
| 117 | + |
| 118 | +`matches` and `matchesAff` provide access to the stream of hash changes. You provide a function with your `routing` (e.g., your composed `Match Locations`), and `matches` (or `matchesAff`) maps it over the stream. |
| 119 | + |
| 120 | +In our example, we'll run in `Eff`: |
| 121 | + |
| 122 | +```purescript |
| 123 | +main = do |
| 124 | + matches routing (\old new -> someAction old new) |
| 125 | + --- other stuff --- |
| 126 | + where |
| 127 | + someAction :: forall e. Maybe Locations -> Locations -> Eff e Unit |
| 128 | + someAction = ... |
| 129 | +``` |
| 130 | +The previous route (`old`) is a `Maybe Locations` and the `new` is of type `Locations`. The `someAction` you take is up to you, but you'll probably want to do something with the `new` value. |
0 commit comments