|
1 | | -Included is a router (in the orbit of Single-Page Applications) that is written entirely in Scala. |
2 | | - |
3 | | -Features |
4 | | -======== |
5 | | -* Type-safety. |
6 | | - * Links to routes are guaranteed to be valid. |
7 | | - * Routes for different pages or routing rule sets cannot be used in the wrong context. |
8 | | -* Rules |
9 | | - * Routes to views. |
10 | | - * Redirection routes. |
11 | | - * Dynamic routes. (eg. `/person/123`) |
12 | | - * URL re-writing / translation rules. (eg. can remove trailing slashes from URL.) |
13 | | - * Choose to redirect or render custom view when route is invalid / not found. |
14 | | -* Route views can be intercepted and modified. (eg. to add page headers, footers, a nav breadcrumb.) |
15 | | -* URL and view are always kept in sync. |
16 | | -* Routes are bookmarkable. |
17 | | -* Uses HTML5 History API. |
18 | | -* Routing logic is deterministic and unit-testable. |
19 | | - |
20 | | - |
21 | | -Caution |
22 | | -======= |
23 | | - |
24 | | -* If you want routes starting with slashes, you will need to configure your server appropriately. |
25 | | - There's no point having `www.blah.com/foo` have routes like `/bar` if when the server receives a request for `www.blah.com/foo/bar` it doesn't know to use the same endpoint as `www.blah.com/foo`. |
26 | | - If you don't have that control, begin with a `#` instead, like `#foo`. |
27 | | -* If you use Internet Explorer v0.1 ~ v9, the HTML5 API won't be available. But that's ok, there's no need to code like our homo heidelbergensis ancestors, just download and use a polyfill. |
28 | | - |
29 | | -Tutorial |
30 | | -======== |
31 | | - |
32 | | -The friendliest way to create a router is to use the `RoutingRules` DSL. |
33 | | -Create an object that extends `RoutingRules` and provide the only mandatory method, `notFound`. |
34 | | - |
35 | | -```scala |
36 | | -object MyPage extends RoutingRules { |
37 | | - override val notFound = render( <.h1("404!!") ) |
38 | | -} |
39 | | -``` |
40 | | - |
41 | | -#### Root view |
42 | | - |
43 | | -Now lets wire up a view for the root route. |
44 | | -Assuming you have a React component called `RootComponent` somewhere, add this to your `RoutingRules` object: |
45 | | - |
46 | | -```scala |
47 | | - val root = register(rootLocation(RootComponent)) |
48 | | -``` |
49 | | - |
50 | | -#### Redirect 404 to root view |
51 | | - |
52 | | -Instead of showing a 404 when an invalid route is accessed, lets redirect to the root view. |
53 | | -All that's needed is |
54 | | -```scala |
55 | | - override val notFound = redirect(root, Redirect.Replace) |
56 | | -``` |
57 | | - |
58 | | -`Redirect.Replace` means that the window URL is replaced with the new URL without the old URL going into history. |
59 | | -Use `Redirect.Push` to store the old URL in browser history when the redirect occurs. |
60 | | - |
61 | | -Also, be careful that you don't refer to a `val` that hasn't been initialised yet (ie. a forward reference). |
62 | | -Order your rules appropriately or use lazy vals. |
63 | | - |
64 | | -So now we have: |
65 | | -```scala |
66 | | -object MyPage extends RoutingRules { |
67 | | - val root = register(rootLocation(RootComponent)) |
68 | | - override val notFound = redirect(root, Redirect.Replace) |
69 | | -} |
70 | | -``` |
71 | | - |
72 | | -#### Adding static routes |
73 | | - |
74 | | -Routes can either render a view, or redirect. Here are examples of both. |
75 | | - |
76 | | -```scala |
77 | | - // Wire a route #hello to a view |
78 | | - val hello = register(location("#hello", HelloComponent)) |
79 | | - |
80 | | - // Redirect #hey to #hello |
81 | | - register(redirection("#hey", hello, Redirect.Replace)) |
82 | | -``` |
83 | | - |
84 | | -#### Links |
85 | | - |
86 | | -To create safe links to routes, you'll need access to a `Router` in your component's render function. |
87 | | -`Router` has a method `link` that creates links to valid routes. |
88 | | - |
89 | | -The `render()` method in your `RoutingRules` accepts: |
90 | | -* plain old `ReactElement` |
91 | | -* `Router => ReactElement` |
92 | | -* Components with the `Router` as their props type. |
93 | | - |
94 | | -Putting this altogether we can have: |
95 | | -```scala |
96 | | -object MyPage extends RoutingRules { |
97 | | - val root = register(rootLocation(RootComponent)) |
98 | | - val hello = register(location("#hello", <.h1("Hello!") )) |
99 | | - override val notFound = redirect(root, Redirect.Replace) |
100 | | -} |
101 | | - |
102 | | -val RootComponent = ReactComponentB[MyPage.Router]("Root") |
103 | | - .render(router => |
104 | | - <.div( |
105 | | - <.h2("Router Demonstration"), |
106 | | - <.div(router.link(MyPage.root) ("The 'root' route")), |
107 | | - <.div(router.link(MyPage.hello)("The 'hello' route"))) |
108 | | - ).build |
109 | | -``` |
110 | | - |
111 | | -Note that the `Router` type is prefixed as `ReactComponentB[MyPage.Router]` and not `ReactComponentB[Router]`. |
112 | | -Routers are not interchangable between routing rule sets. |
113 | | - |
114 | | -#### Renmdering your Router |
115 | | - |
116 | | -Before a `Router` can be created it needs to the base URL, which is the prefix portion of the URL that is the same for all your pages routes. |
117 | | - |
118 | | -It needn't be absolute at compile-time, but it needs to be absolute at runtime. `BaseUrl.fromWindowOrigin` will give you the protocol, domain and port at runtime, after which you should append a path if necessary. Example: `BaseUrl.fromWindowOrigin / "my_page"` |
119 | | - |
120 | | -Once you have a your `BaseUrl`, call `<RoutingRules>.router(baseUrl)` to get a standard React component of your router. |
121 | | - |
122 | | -Note: You can enable console logging by providing a `Router.consoleLogger` to `router()`. |
123 | | - |
124 | | -Example: |
125 | | -```scala |
126 | | -val baseUrl = BaseUrl.fromWindowOrigin / "my_page" |
127 | | -val component = MyPage.router(baseUrl, Router.consoleLogger) |
128 | | -``` |
129 | | - |
130 | | -#### Dynamic Routes |
131 | | - |
132 | | -Use `register( parser {...} ... )` to register dynamic routes. |
133 | | - |
134 | | -`parser` takes a partial function that attempts to match a portion of a URL and capture some part of it. |
135 | | -If successful, you can create a dynamic response using the captured part. |
136 | | - |
137 | | -Unlike static routes, if you want to create a link to a dynamic page you need to specify an additional function that generates the dynamic route path. Use `.dynLink` in your routing rules for this purpose. |
138 | | - |
139 | | -Examples: |
140 | | -```scala |
141 | | -object MyPage extends RoutingRules { |
142 | | - ... |
143 | | - |
144 | | - // This example matches /name/<anything> |
145 | | - |
146 | | - private val namePathMatch = "^/name/(.+)$".r |
147 | | - register(parser { case namePathMatch(n) => n }.location(n => NameComponent(n))) |
148 | | - val name = dynLink[String](n => s"/name/$n") |
149 | | - |
150 | | - // This example matches /person/<number> |
151 | | - // and redirects on /person/<not-a-number> |
152 | | - |
153 | | - private val personPathMatch = "^/person/(.+)$".r |
154 | | - register(parser { case personPathMatch(p) => p }.thenMatch { |
155 | | - case matchNumber(idStr) => render(PersonComponent(PersonId(idStr.toLong))) |
156 | | - case _ /* non-numeric id */ => redirect(root, Redirect.Push) |
157 | | - }) |
158 | | - val person = dynLink[PersonId](id => s"/person/${id.value}") |
159 | | -} |
160 | | -``` |
161 | | - |
162 | | -Note that we don't store the results of `register` for dynamic routes. This is why `dynLink` is necessary. |
163 | | - |
164 | | -#### View interception |
165 | | - |
166 | | -To customise all views rendered by a routing rule set, override the `interceptRender` method and follow the types. |
167 | | - |
168 | | -This example adds a back button to all pages except the root: |
169 | | -```scala |
170 | | -override protected def interceptRender(i: InterceptionR): ReactElement = |
171 | | - if (i.loc == root) |
172 | | - i.element |
173 | | - else |
174 | | - <.div( |
175 | | - <.div(i.router.link(root)("Back", ^.cls := "back")), |
176 | | - i.element) |
177 | | -``` |
178 | | - |
179 | | -### URL rewriting |
180 | | - |
181 | | -`RoutingRules` comes with a built-in (although inactive) URL rewriting rule called `removeTrailingSlashes`. |
182 | | -It can be installed via `register()` and is a good example of how to create dynamic matching rules. |
183 | | - |
184 | | -Its implementation is simple: |
185 | | -```scala |
186 | | -def removeTrailingSlashes: DynamicRoute = { |
187 | | - val regex = "^(.*?)/+$".r |
188 | | - parser { case regex(p) => p }.redirection(p => (Path(p), Redirect.Replace)) |
189 | | -} |
190 | | -``` |
| 1 | +Included is a router (in the orbit of Single-Page Applications) that is written entirely in Scala. |
| 2 | + |
| 3 | +Features |
| 4 | +======== |
| 5 | +* Type-safety. |
| 6 | + * Links to routes are guaranteed to be valid. |
| 7 | + * Routes for different pages or routing rule sets cannot be used in the wrong context. |
| 8 | +* Rules |
| 9 | + * Routes to views. |
| 10 | + * Redirection routes. |
| 11 | + * Dynamic routes. (eg. `/person/123`) |
| 12 | + * URL re-writing / translation rules. (eg. can remove trailing slashes from URL.) |
| 13 | + * Choose to redirect or render custom view when route is invalid / not found. |
| 14 | +* Route views can be intercepted and modified. (eg. to add page headers, footers, a nav breadcrumb.) |
| 15 | +* URL and view are always kept in sync. |
| 16 | +* Routes are bookmarkable. |
| 17 | +* Uses HTML5 History API. |
| 18 | +* Routing logic is deterministic and unit-testable. |
| 19 | + |
| 20 | + |
| 21 | +Caution |
| 22 | +======= |
| 23 | + |
| 24 | +* If you want routes starting with slashes, you will need to configure your server appropriately. |
| 25 | + There's no point having `www.blah.com/foo` have routes like `/bar` if when the server receives a request for `www.blah.com/foo/bar` it doesn't know to use the same endpoint as `www.blah.com/foo`. |
| 26 | + If you don't have that control, begin with a `#` instead, like `#foo`. |
| 27 | +* If you use Internet Explorer v0.1 ~ v9, the HTML5 API won't be available. But that's ok, there's no need to code like our homo heidelbergensis ancestors, just download and use a polyfill. |
| 28 | + |
| 29 | +Tutorial |
| 30 | +======== |
| 31 | + |
| 32 | +The friendliest way to create a router is to use the `RoutingRules` DSL. |
| 33 | +Create an object that extends `RoutingRules` and provide the only mandatory method, `notFound`. |
| 34 | + |
| 35 | +```scala |
| 36 | +object MyPage extends RoutingRules { |
| 37 | + override val notFound = render( <.h1("404!!") ) |
| 38 | +} |
| 39 | +``` |
| 40 | + |
| 41 | +#### Root view |
| 42 | + |
| 43 | +Now lets wire up a view for the root route. |
| 44 | +Assuming you have a React component called `RootComponent` somewhere, add this to your `RoutingRules` object: |
| 45 | + |
| 46 | +```scala |
| 47 | + val root = register(rootLocation(RootComponent)) |
| 48 | +``` |
| 49 | + |
| 50 | +#### Redirect 404 to root view |
| 51 | + |
| 52 | +Instead of showing a 404 when an invalid route is accessed, lets redirect to the root view. |
| 53 | +All that's needed is |
| 54 | +```scala |
| 55 | + override val notFound = redirect(root, Redirect.Replace) |
| 56 | +``` |
| 57 | + |
| 58 | +`Redirect.Replace` means that the window URL is replaced with the new URL without the old URL going into history. |
| 59 | +Use `Redirect.Push` to store the old URL in browser history when the redirect occurs. |
| 60 | + |
| 61 | +Also, be careful that you don't refer to a `val` that hasn't been initialised yet (ie. a forward reference). |
| 62 | +Order your rules appropriately or use lazy vals. |
| 63 | + |
| 64 | +So now we have: |
| 65 | +```scala |
| 66 | +object MyPage extends RoutingRules { |
| 67 | + val root = register(rootLocation(RootComponent)) |
| 68 | + override val notFound = redirect(root, Redirect.Replace) |
| 69 | +} |
| 70 | +``` |
| 71 | + |
| 72 | +#### Adding static routes |
| 73 | + |
| 74 | +Routes can either render a view, or redirect. Here are examples of both. |
| 75 | + |
| 76 | +```scala |
| 77 | + // Wire a route #hello to a view |
| 78 | + val hello = register(location("#hello", HelloComponent)) |
| 79 | + |
| 80 | + // Redirect #hey to #hello |
| 81 | + register(redirection("#hey", hello, Redirect.Replace)) |
| 82 | +``` |
| 83 | + |
| 84 | +#### Links |
| 85 | + |
| 86 | +To create safe links to routes, you'll need access to a `Router` in your component's render function. |
| 87 | +`Router` has a method `link` that creates links to valid routes. |
| 88 | + |
| 89 | +The `render()` method in your `RoutingRules` accepts: |
| 90 | +* plain old `ReactElement` |
| 91 | +* `Router => ReactElement` |
| 92 | +* Components with the `Router` as their props type. |
| 93 | + |
| 94 | +Putting this altogether we can have: |
| 95 | +```scala |
| 96 | +object MyPage extends RoutingRules { |
| 97 | + val root = register(rootLocation(RootComponent)) |
| 98 | + val hello = register(location("#hello", <.h1("Hello!") )) |
| 99 | + override val notFound = redirect(root, Redirect.Replace) |
| 100 | +} |
| 101 | + |
| 102 | +val RootComponent = ReactComponentB[MyPage.Router]("Root") |
| 103 | + .render(router => |
| 104 | + <.div( |
| 105 | + <.h2("Router Demonstration"), |
| 106 | + <.div(router.link(MyPage.root) ("The 'root' route")), |
| 107 | + <.div(router.link(MyPage.hello)("The 'hello' route"))) |
| 108 | + ).build |
| 109 | +``` |
| 110 | + |
| 111 | +Note that the `Router` type is prefixed as `ReactComponentB[MyPage.Router]` and not `ReactComponentB[Router]`. |
| 112 | +Routers are not interchangable between routing rule sets. |
| 113 | + |
| 114 | +#### Renmdering your Router |
| 115 | + |
| 116 | +Before a `Router` can be created it needs to the base URL, which is the prefix portion of the URL that is the same for all your pages routes. |
| 117 | + |
| 118 | +It needn't be absolute at compile-time, but it needs to be absolute at runtime. `BaseUrl.fromWindowOrigin` will give you the protocol, domain and port at runtime, after which you should append a path if necessary. Example: `BaseUrl.fromWindowOrigin / "my_page"` |
| 119 | + |
| 120 | +Once you have a your `BaseUrl`, call `<RoutingRules>.router(baseUrl)` to get a standard React component of your router. |
| 121 | + |
| 122 | +Note: You can enable console logging by providing a `Router.consoleLogger` to `router()`. |
| 123 | + |
| 124 | +Example: |
| 125 | +```scala |
| 126 | +val baseUrl = BaseUrl.fromWindowOrigin / "my_page" |
| 127 | +val component = MyPage.router(baseUrl, Router.consoleLogger) |
| 128 | +``` |
| 129 | + |
| 130 | +#### Dynamic Routes |
| 131 | + |
| 132 | +Use `register( parser {...} ... )` to register dynamic routes. |
| 133 | + |
| 134 | +`parser` takes a partial function that attempts to match a portion of a URL and capture some part of it. |
| 135 | +If successful, you can create a dynamic response using the captured part. |
| 136 | + |
| 137 | +Unlike static routes, if you want to create a link to a dynamic page you need to specify an additional function that generates the dynamic route path. Use `.dynLink` in your routing rules for this purpose. |
| 138 | + |
| 139 | +Examples: |
| 140 | +```scala |
| 141 | +object MyPage extends RoutingRules { |
| 142 | + ... |
| 143 | + |
| 144 | + // This example matches /name/<anything> |
| 145 | + |
| 146 | + private val namePathMatch = "^/name/(.+)$".r |
| 147 | + register(parser { case namePathMatch(n) => n }.location(n => NameComponent(n))) |
| 148 | + val name = dynLink[String](n => s"/name/$n") |
| 149 | + |
| 150 | + // This example matches /person/<number> |
| 151 | + // and redirects on /person/<not-a-number> |
| 152 | + |
| 153 | + private val personPathMatch = "^/person/(.+)$".r |
| 154 | + register(parser { case personPathMatch(p) => p }.thenMatch { |
| 155 | + case matchNumber(idStr) => render(PersonComponent(PersonId(idStr.toLong))) |
| 156 | + case _ /* non-numeric id */ => redirect(root, Redirect.Push) |
| 157 | + }) |
| 158 | + val person = dynLink[PersonId](id => s"/person/${id.value}") |
| 159 | +} |
| 160 | +``` |
| 161 | + |
| 162 | +Note that we don't store the results of `register` for dynamic routes. This is why `dynLink` is necessary. |
| 163 | + |
| 164 | +#### View interception |
| 165 | + |
| 166 | +To customise all views rendered by a routing rule set, override the `interceptRender` method and follow the types. |
| 167 | + |
| 168 | +This example adds a back button to all pages except the root: |
| 169 | +```scala |
| 170 | +override protected def interceptRender(i: InterceptionR): ReactElement = |
| 171 | + if (i.loc == root) |
| 172 | + i.element |
| 173 | + else |
| 174 | + <.div( |
| 175 | + <.div(i.router.link(root)("Back", ^.cls := "back")), |
| 176 | + i.element) |
| 177 | +``` |
| 178 | + |
| 179 | +### URL rewriting |
| 180 | + |
| 181 | +`RoutingRules` comes with a built-in (although inactive) URL rewriting rule called `removeTrailingSlashes`. |
| 182 | +It can be installed via `register()` and is a good example of how to create dynamic matching rules. |
| 183 | + |
| 184 | +Its implementation is simple: |
| 185 | +```scala |
| 186 | +def removeTrailingSlashes: DynamicRoute = { |
| 187 | + val regex = "^(.*?)/+$".r |
| 188 | + parser { case regex(p) => p }.redirection(p => (Path(p), Redirect.Replace)) |
| 189 | +} |
| 190 | +``` |
0 commit comments