UNPKG

16.3 kBMarkdownView Raw
1[ui-router](https://github.com/angular-ui/ui-router/wiki) is fantastic, and I would use it in all of my projects if it wasn't tied to AngularJS.
2
3But I don't want to use AngularJS - I want to use *[my favorite templating/dom manipulation libraries here]*.
4
5Thus, this library! Written to work with [browserify](https://github.com/substack/node-browserify), it lets you create nested "states" that correspond to different parts of the url path.
6
7If you've never used AngularJS's ui-router before, check out this post: [Why your webapp needs a state-based router](http://joshduff.com/#!/post/2015-06-why-you-need-a-state-router.md).
8
9To see an example app implemented with a couple of different browser rendering libraries, [click here to visit the state-router-example on Github Pages](http://tehshrike.github.io/state-router-example).
10
11If you have any questions, [ask me on Gitter](https://gitter.im/TehShrike/abstract-state-router)! [![Join the chat at https://gitter.im/TehShrike/abstract-state-router](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/TehShrike/abstract-state-router)
12
13# Current renderer implementations
14
15- [RactiveJS](https://github.com/TehShrike/ractive-state-router)
16- [Riot](https://github.com/TehShrike/riot-state-renderer)
17- [virtual-dom](https://github.com/ArtskydJ/virtualdom-state-renderer)
18- [Knockout](https://github.com/crissdev/knockout-state-renderer)
19
20If you want to use the state router with some other templating/dom manipulation library, [read these docs](https://github.com/TehShrike/abstract-state-router/blob/master/renderer.md)! It's not too bad to get started.
21
22# Install
23
24Using npm + your favorite CommonJS bundler is easiest.
25
26```sh
27
28npm install abstract-state-router
29
30```
31
32You can also [download the stand-alone build from wzrd.in](https://wzrd.in/standalone/abstract-state-router@latest). If you include it in a `<script>` tag, a `abstractStateRouter` function will be included on the global scope.
33
34Want to use the abstract-state-router without messing with bundlers or package managers? Check out the minimum-viable-project code (in a single HTML file!) over at the [simplest-abstract-state-router-usage](https://github.com/TehShrike/simplest-abstract-state-router-usage).
35
36# API
37
38```js
39var createStateRouter = require('abstract-state-router')
40
41var stateRouter = createStateRouter(makeRenderer, rootElement, options)
42```
43
44The `makeRenderer` should be a function that returns an object with four properties: render, destroy, getChildElement, and reset. Documentation is [here](https://github.com/TehShrike/abstract-state-router/blob/master/renderer.md) - see [test/support/renderer-mock.js](https://github.com/TehShrike/abstract-state-router/blob/master/test/helpers/renderer-mock.js) for an example implementation.
45
46The `rootElement` is the element where the first-generation states will be created.
47
48## options
49
50Possible properties of the `options` object are:
51
52- `pathPrefix` defaults to `'#'`. If you're using HTML5 routing/pushState, you'll most likely want to set this to an empty string.
53- `router` defaults to an instance of a [hash brown router](https://github.com/TehShrike/hash-brown-router/). The abstract-state-router unit tests use the [hash brown router stub](https://github.com/TehShrike/hash-brown-router/#testability). To use pushState, pass in a hash brown router created with [sausage-router](https://github.com/TehShrike/sausage-router).
54- `throwOnError` defaults to true, because you get way better stack traces in Chrome when you throw than if you `console.log(err)` or emit `'error'` events. The unit tests disable this.
55
56## stateRouter.addState({name, route, defaultChild, data, template, resolve, activate, querystringParameters, defaultParameters})
57
58The addState function takes a single object of options. All of them are optional, unless stated otherwise.
59
60`name` is parsed in the same way as ui-router's [dot notation](https://github.com/angular-ui/ui-router/wiki/Nested-States-%26-Nested-Views#dot-notation), so 'contacts.list' is a child state of 'contacts'. **Required.**
61
62`route` is an express-style url string that is parsed with a fork of [path-to-regexp](https://github.com/pillarjs/path-to-regexp). If the state is a child state, this route string will be concatenated to the route string of its parent (e.g. if 'contacts' state has route ':user/contacts' and 'contacts.list' has a route of '/list', you could visit the child state by browsing to '/tehshrike/contacts/list').
63
64`defaultChild` is a string (or a function that returns a string) of the default child's name. If you attempt to go directly to a state that has a default child, you will be directed to the default child. (For example, you could set 'list' to be the default child of 'contacts'. Then doing `state.go('contacts')` will actually do `state.go('contacts.list')`. Likewise, browsing to '/tehshrike/contacts' would bring you to '/tehshrike/contacts/list'.)
65
66`data` is an object that can hold whatever you want - it will be passed in to the resolve and activate functions.
67
68`template` is a template string/object/whatever to be interpreted by the render function. **Required.**
69
70`resolve` is a function called when the selected state begins to be transitioned to, allowing you to accomplish the same objective as you would with ui-router's [resolve](https://github.com/angular-ui/ui-router/wiki#resolve).
71
72`activate` is a function called when the state is made active - the equivalent of the AngularJS controller to the ui-router.
73
74`querystringParameters` is an array of query string parameters that will be watched by this state.
75
76`defaultParameters` is an object whose properties should correspond to parameters defined in the `querystringParameters` option or the route parameters. Whatever values you supply here will be used as the defaults in case the url does not contain any value for that parameter.
77
78For backwards compatibility reasons, `defaultQuerystringParameters` will work as well (though it does not function any differently).
79
80### resolve(data, parameters, callback(err, content).redirect(stateName, [stateParameters]))
81
82The first argument is the data object you passed to the addState call. The second argument is an object containing the parameters that were parsed out of the route and the query string.
83
84If you call `callback(err, content)` with a truthy err value, the state change will be cancelled and the previous state will remain active.
85
86If you call `callback.redirect(stateName, [stateParameters])`, the state router will begin transitioning to that state instead. The current destination will never become active, and will not show up in the browser history.
87
88### activate(context)
89
90The activate function is called when the state becomes active. It is passed an event emitter named `context` with four properties:
91
92- `domApi`: the DOM API returned by the renderer
93- `data`: the data object given to the addState call
94- `parameters`: the route/querystring parameters
95- `content`: the object passed into the resolveFunction's callback
96
97The `context` object is also an event emitter that emits a `'destroy'` event when the state is being transitioned away from. You should listen to this event to clean up any workers that may be ongoing.
98
99### addState examples
100
101```js
102stateRouter.addState({
103 name: 'app',
104 data: {},
105 route: '/app',
106 template: '',
107 defaultChild: 'tab1',
108 resolve: function(data, parameters, cb) {
109 // Sync or asnyc stuff; just call the callback when you're done
110 isLoggedIn(function(err, isLoggedIn) {
111 cb(err, isLoggedIn)
112 })
113 }, activate: function(context) {
114 // Normally, you would set data in your favorite view library
115 var isLoggedIn = context.content
116 var ele = document.getElementById('status')
117 ele.innerText = isLoggedIn ? 'Logged In!' : 'Logged Out!'
118 }
119})
120
121stateRouter.addState({
122 name: 'app.tab1',
123 data: {},
124 route: '/tab_1',
125 template: '',
126 resolve: function(data, parameters, cb) {
127 getTab1Data(cb)
128 }, activate: function(context) {
129 document.getElementById('tab').innerText = context.content
130
131 var intervalId = setInterval(function() {
132 document.getElementById('tab').innerText = 'MORE CONTENT!'
133 }, 1000)
134
135 context.on('destroy', function() {
136 clearInterval(intervalId)
137 })
138 }
139})
140
141stateRouter.addState({
142 name: 'app.tab2',
143 data: {},
144 route: '/tab_2',
145 template: '',
146 resolve: function(data, parameters, cb) {
147 getTab2Data(cb)
148 }, activate: function(context) {
149 document.getElementById('tab').innerText = context.content
150 }
151})
152```
153
154## stateRouter.go(stateName, [stateParameters, [options]])
155
156Browses to the given state, with the current parameters. Changes the url to match.
157
158The options object currently supports just one option "replace" - if it is truthy, the current state is replaced in the url history.
159
160If a state change is triggered during a state transition, and the DOM hasn't been manipulated yet, then the current state change is discarded, and the new one replaces it. Otherwise, it is queued and applied once the current state change is done.
161
162If `stateName` is `null`, the current state is used as the destination.
163
164```js
165stateRouter.go('app')
166// This actually redirects to app.tab1, because the app state has the default child: 'tab1'
167```
168
169## stateRouter.evaluateCurrentRoute(fallbackStateName, [fallbackStateParameters])
170
171You'll want to call this once you've added all your initial states. It causes the current path to be evaluated, and will activate the current state. If the current path doesn't match the route of any available states, the browser gets sent to the fallback state provided.
172
173```js
174stateRouter.evaluateCurrentRoute('app.tab2')
175```
176
177## stateRouter.stateIsActive(stateName, [stateParameters])
178
179Returns true if `stateName` is the current active state, or an ancestor of the current active state...
180
181...And all of the properties of `stateParameters` match the current state parameter values.
182
183```js
184// Current state name: app.tab1
185// Current parameters: { fancy: 'yes', thing: 'hello' }
186stateRouter.stateIsActive('app.tab1', { fancy: 'yes' }) // => true
187stateRouter.stateIsActive('app.tab1', { fancy: 'no' }) // => false
188stateRouter.stateIsActive('app') // => true
189```
190
191## stateRouter.makePath(stateName, [stateParameters], options)
192
193Returns a path to the state, starting with an [optional](#options) octothorpe `#`, suitable for inserting straight into the `href` attribute of a link.
194
195The `options` object supports one property: `inherit` - if true, querystring parameters are inherited from the current state. Defaults to false.
196
197If `stateName` is `null`, the current state is used.
198
199```js
200stateRouter.makePath('app.tab2', { pants: 'no' })
201```
202
203## Events
204
205These are all emitted on the state router object.
206
207### State change
208
209- `stateChangeAttempt(functionThatBeginsTheStateChange)` - used by the state transition manager, probably not useful to anyone else at the moment
210- `stateChangeStart(state, parameters)` - emitted after the state name and parameters have been validated
211- `stateChangeCancelled(err)` - emitted if a redirect is issued in a resolve function
212- `stateChangeEnd(state, parameters)` - after all activate functions are called
213- `stateChangeError(err)` - emitted if an error occurs while trying to navigate to a new state - including if you try to navigate to a state that doesn't exist
214- `stateError(err)` - emitted if an error occurs in an activation function, or somewhere else that doesn't directly interfere with changing states. Should probably be combined with `stateChangeError` at some point since they're not that different?
215- `routeNotFound(route, parameters)` - emitted if the user or some errant code changes the location hash to a route that does not have any states associated with it. If you have a generic "not found" page you want to redirect people to, you can do so like this:
216
217```js
218stateRouter.on('routeNotFound', function(route, parameters) {
219 stateRouter.go('not-found', {
220 route: route,
221 parameters: parameters
222 })
223})
224```
225
226### DOM API interactions
227
228- `beforeCreateState({state, content, parameters})`
229- `afterCreateState({state, domApi, content, parameters})`
230- `beforeResetState({state, domApi, content, parameters})`
231- `afterResetState({state, domApi, content, parameters})`
232- `beforeDestroyState({state, domApi})`
233- `afterDestroyState({state})`
234
235# Testing/development
236
237To run the unit tests:
238
239- clone this repository
240- run `npm install`
241- run `npm test`
242
243Automated browser testing provided by [Browserstack](https://www.browserstack.com/).
244
245Tested in Chrome, Firefox, Safari, and IE10+ (IE9 doesn't support [replace](https://developer.mozilla.org/en-US/docs/Web/API/Location/replace)).
246
247[![Build Status](https://travis-ci.org/TehShrike/abstract-state-router.svg?branch=master)](https://travis-ci.org/TehShrike/abstract-state-router)
248
249
250# State change flow
251
252- emit stateChangeStart
253- call all resolve functions
254- resolve functions return
255- **NO LONGER AT PREVIOUS STATE**
256- destroy the contexts of all "destroy" and "change" states
257- destroy appropriate dom elements
258- reset "change"ing dom elements
259- call render functions for "create"ed states
260- call all activate functions
261- emit stateChangeEnd
262
263# Every state change does this to states
264
265- destroy: states that are no longer active at all. The contexts are destroyed, and the DOM elements are destroyed.
266- change: states that remain around, but with different parameter values - the DOM sticks around, but the contexts are destroyed and resolve/activate are called again.
267- create: states that weren't active at all before. The DOM elements are rendered, and resolve/activate are called.
268
269# HTML5/pushState routing
270
271pushState routing is technically supported. To use it, pass in an options object with a `router` hash-brown-router constructed with a [sausage-router](https://github.com/TehShrike/sausage-router), and then set the `pathPrefix` option to an empty string.
272
273```js
274var makeStateRouter = require('abstract-state-router')
275var sausage = require('sausage-router')
276var makeRouter = require('hash-brown-router')
277
278var stateRouter = makeStateRouter(makeRenderer, rootElement, {
279 pathPrefix: '',
280 router: makeRouter(sausage())
281})
282```
283
284However to use it in the real world, there are two things you probably want to do:
285
286## Intercept link clicks
287
288To get all the benefits of navigating around nested states, you'll need to intercept every click on a link and block the link navigation, calling `go(path)` on the sausage-router instead.
289
290You would need to add these click handlers whenever a state change happened.
291
292## server-side rendering
293
294You would also need to be able to render the correct HTML on the server-side.
295
296For this to even possible, your chosen rendering library needs to be able to work on the server-side to generate static HTML. I know at least Ractive.js and Riot support this.
297
298The abstract-state-router would need to be changed to supply the list of nested DOM API objects for your chosen renderer.
299
300Then to generate the static HTML for the current route, you would create an abstract-state-router, tell it to navigate to that route, collect all the nested DOM API objects, render them as HTML strings, embedding the children inside of the parents.
301
302You would probably also want to send the client the data that was returned by the `resolve` functions, so that when the JavaScript app code started running the abstract-state-router on the client-side, it wouldn't hit the server to fetch all the data that had already been fetched on the server to generate the original HTML.
303
304## Who's adding this?
305
306Track development progress in [#48](https://github.com/TehShrike/abstract-state-router/issues/48).
307
308It could be added by me, but probably not in the near future, since I will mostly be using this for form-heavy business apps where generating static HTML isn't any benefit.
309
310If I use the abstract-state-router on an app where I want to support clients without JS, then I'll start working through those tasks in the issue above.
311
312If anyone else has need of this functionality and wants to get keep making progress on it, I'd be happy to help. Stop by the [chat room](https://gitter.im/TehShrike/abstract-state-router) to ask any questions.
313
314# Maintainers
315
316- [TehShrike](https://github.com/TehShrike)
317- [ArtskydJ](https://github.com/ArtskydJ)
318
319# License
320
321[WTFPL](http://wtfpl2.com)