add support for route state #1480

This commit is contained in:
Leo Horie 2016-12-17 21:50:04 -05:00
parent 75e83ea565
commit de07a54881
10 changed files with 367 additions and 196 deletions

View file

@ -14,6 +14,9 @@
- [Typical usage](#typical-usage)
- [Navigating to different routes](#navigating-to-different-routes)
- [Routing parameters](#routing-parameters)
- [Key parameter](#key-parameter)
- [Variadic routes](#variadic-routes)
- [History state](#history-state)
- [Changing router prefix](#changing-router-prefix)
- [Advanced component resolution](#advanced-component-resolution)
- [Wrapping a layout component](#wrapping-a-layout-component)
@ -70,6 +73,8 @@ Argument | Type | Required | Description
`path` | `String` | Yes | The path to route to, without a prefix. The path may include slots for routing parameters
`data` | `Object` | No | Routing parameters. If `path` has routing parameter slots, the properties of this object are interpolated into the path string
`options.replace` | `Boolean` | No | Whether to create a new history entry or to replace the current one. Defaults to false
`options.state` | `Object` | No | The `state` object to pass to the underlying `history.pushState` / `history.replaceState` call. This state object becomes available in the `history.state` property, and is merged into the [routing parameters](#routing-parameters) object. Note that this option only works when using the pushState API, but is ignored if the router falls back to hashchange mode (i.e. if the pushState API is not available)
`options.title` | `String` | No | The `title` string to pass to the underlying `history.pushState` / `history.replaceState` call.
**returns** | | | Returns `undefined`
##### route.get
@ -254,6 +259,28 @@ It's possible to have multiple arguments in a route, for example `/edit/:project
In addition to routing parameters, the `attrs` object also includes a `path` property that contains the current route path, and a `route` property that contains the matched routed.
#### Key parameter
When a user navigates from a parameterized route to the same route with a different parameter (e.g. going from `/page/1` to `/page/2` given a route `/page/:id`, the component would not be recreated from scratch since both routes resolve to the same component, and thus result in a virtual dom in-place diff. This has the side-effect of triggering the `onupdate` hook, rather than `oninit`/`oncreate`. However, it's relatively common for a developer to want to synchronize the recreation of the component to the route change event.
To achieve that, it's possible to combine route parameterization with the virtual dom [key reconciliation](keys.md) feature:
```javascript
m.route(document.body, "/edit/1", {
"/edit/:key": Edit,
})
```
This means that the [vnode](vnodes.md) that is created for the root component of the route has a route parameter object `key`. Route parameters become `attrs` in the vnode. Thus, when jumping from one page to another, the `key` changes and causes the component to be recreated from scratch (since the key tells the virtual dom engine that old and new components are different entities).
You can take that idea further to create components that recreate themselves when reloaded:
`m.route.set(m.route.get(), {key: Date.now()})`
Or even use the [`history state`](#history-state) feature to achieve reloadable components without polluting the URL:
`m.route.set(m.route.get(), null, {state: {key: Date.now()}})`
#### Variadic routes
It's also possible to have variadic routes, i.e. a route with an argument that contains URL pathnames that contain slashes:
@ -264,6 +291,44 @@ m.route(document.body, "/edit/pictures/image.jpg", {
})
```
#### History state
It's possible to take full advantage of the underlying `history.pushState` API to improve user's navigation experience. For example, an application could "remember" the state of a large form when the user leaves a page by navigating away, such that if the user pressed the back button in the browser, they'd have the form filled rather than a blank form.
For example, you could create a form like this:
```javascript
var state = {
term: "",
search: function() {
// save the state for this route
// this is equivalent to `history.replaceState({term: state.term}, null, location.href)`
m.route.set(m.route.get(), null, {replace: true, state: {term: state.term}})
// navigate away
location.href = "https://google.com/?q=" + state.term
}
}
var Form = {
oninit: function(vnode) {
state.term = vnode.attrs.term || "" // populated from the `history.state` property if the user presses the back button
},
view: function() {
return m("form", [
m("input[placeholder='Search']", {oninput: m.withAttr("value", function(v) {state.term = v}), value: state.term}),
m("button", {onclick: state.search}, "Search")
])
}
}
m.route(document.body, "/", {
"/": Form,
})
```
This way, if the user searches and presses the back button to return to the application, the input will still be populated with the search term. This technique can improve the user experience of large forms and other apps where non-persisted state is laborious for a user to produce.
---
### Changing router prefix
@ -409,7 +474,7 @@ m.route(document.body, "/user/list", {
"/user/list": {
oninit: state.loadUsers,
view: function() {
return state.users.length > 0 ? state.users.map(function() {
return state.users.length > 0 ? state.users.map(function(user) {
return m("div", user.id)
}) : "loading"
}
@ -435,7 +500,7 @@ m.route(document.body, "/user/list", {
"/user/list": {
onmatch: state.loadUsers,
render: function() {
return state.users.length > 0 ? state.users.map(function() {
return state.users.length > 0 ? state.users.map(function(user) {
return m("div", user.id)
}) : "loading"
}