window-page
Version:
Route, setup, and build web pages
275 lines (190 loc) • 7.98 kB
Markdown
# window.Page
Async chains for web page lifecycle.
Works well with:
- async navigation
- custom elements
- visibility API
- history API
`window.Page` is the current State instance.
## Install
```js
import 'window-page';
```
And use a bundler to with static or dynamic polyfills.
## Chains
- route: pathname changed and document is not prerendered; can set `state.doc`.
- ready: document is ready
- build: pathname changed and document is not prerendered
- patch: pathname changed and document is not prerendered, or query changed
- setup: visible, on first view or pathname changed
- paint: visible, on first view or pathname or query changed
- fragment: visible, location hash changed; `state.hash` is set.
- close: visible, referrer closed when pathname changed, before new state is setup
- catch: error was thrown, `state.error` is set.
A run is triggered by navigation (document.location changed in any way, or
page history methods called, see below).
Several chains are only run when document is visible - i.e. not "hidden".
This is used to prerender on server, and also prerender on client.
Route listeners can set `state.doc`: an optional document which styles and scripts are imported after `route` chain.
The `ready` chain always has `state.doc = document`.
If the `state.error` object is removed from state during the catch chain,
the navigation will continue as if the error did not happen.
## Usage
```js
Page.route(async function(state) {
const res = await fetch(page.pathname + '.json');
const data = await res.json();
// keep data during navigation
state.data = data;
state.doc = state.parseDoc(data.template);
});
Page.connect({
build(state) {
// build page
}
async patch(state) {
if (state.query.id == null) return;
const data = await (fetch('/getdata?id=' + state.query.id).json())
// do something
}
handleClick(e) {
const link = e.target.closest('a[href]');
if (!link) return;
e.preventDefault();
state.push(link.href);
}
});
```
## API
### chains
For each chain, one can add or remove a listener function that receives the
current state as argument.
- `state[chainLabel](fn)`
runs optional fn right now if the chain is reached, or wait the chain to be run.
returns a promise that resolves to corresponding state.
- `state['un'+chainLabel](fn)`
removes fn from a chain, mostly needed for custom elements.
The fn parameter can be a function or an object with a `<chain>`, or
a `chain<Chain>` method - which is a handy way to keep the value of `this`.
Functions listening for a given stage are run serially.
If a stage chain is already resolved, new listeners are just added immediately
to the promise chain.
To append a function at the end of the current chain, use:
- state.finish(fn)
fn is optional, defaults to a promise (returned) that is resolved at the end.
Avoid making a chain wait its own end, or it will deadlock.
To run a custom chain:
- state.copy().runChain(stage)
Note that custom chains do not propagate properties added to state to other chains.
### listeners and navigation
Chains are implemented through native DOM emitters and listeners, and the
emitter is either a script node in `document.head`, or a state-bound, out of tree, DOM node.
The script node can be `document.currentScript` when defined, unless the listener is a DOM node that is not itself a script node in document head.
Otherwise it is `state.emitter`. That emitter is shared between two successive states having the same pathname, and distinct otherwise.
These behaviors ensure that during navigation, a common script keeps its listeners registered, and other listeners will only be triggered during the life of the state that allowed them to be registered.
### state
The state is a subclass of Loc, which extends URL class with:
- query object
- sameDomain, samePathname, sameQuery, sameHash, samePath methods
- toString() returns a path when in the same domain
- copy() to copy a state and most of its private/public properties
useful for working with another stage of a state.
**Important**: use state.push/replace to mutate url properties.
The state history methods accept partial objects.
- state.data
data is saved in navigator history - must be JSON-serializable.
- state.referrer
the previous state, or null;
Is not related to `document.referrer`.
Page.State: the state's constructor.
### Document import
When a new document is loaded after route chain, stylesheets are loaded in parallel, and scripts are loaded serially, with parallel preloading.
Those methods are called:
- await state.mergeHead(head, prev)
- await state.mergeBody(body, prev)
The default `mergeHead` method do a basic diff to keep existing scripts and links
nodes.
The default `mergeBody` method just replaces `document.body` with the new body.
To manage page transitions, these methods can be overriden by `route` listeners.
### Integration with Custom Elements, event handlers
An object having build, patch, setup, paint, fragment, close methods can be
plugged into Page using:
- state.connect(node)
- state.disconnect(node)
Furthermore, if the object owns methods named `handle${Type}`, they will be
used as `type` event listeners on that object, receiving arguments `(e, state)`.
To use "capture" listeners, just name the methods `capture<Type>` (new in 7.1.0).
To use the same mecanism to manage event listener on another event emitter,
pass that event emitter as second argument to `state.connect(listener, emitter)`.
To simply handle or capture events on window,
use `handleAll${Type}` or `captureAll${Type}`.
These event listeners are automatically added during setup, and removed during
close (or disconnect).
```js
connectedCallback() {
Page.connect(this);
}
disconnectedCallback() {
Page.disconnect(this);
}
patch(state) {}
setup(state) {}
close() {}
captureSubmit(e, state) {}
handleClick(e, state) {}
handleAllClick(e, state) {}
```
### Using the event listener on other objects (window, document...)
- state.connect(listener, emitter)
This method accepts a second argument to configure event listeners, and
benefit from automatic removal of event listeners on `close`.
Example:
```js
setup(state) {
state.connect(this, window);
}
handleScroll(e, state) {
// got click
}
```
### History
These methods will run chains on new state and return immediately the new state:
- state.push(location or url, opts)
- state.replace(location or url, opts)
Options:
- vary (boolean, or "build", "patch", "fragment", default false)
Overrides how pathname, query, hash are compared to previous state.
`true` re-routes the page; and varying on a chain runs the next chains too.
Example: reload after a form login.
- data
Assign this data to next state.data.
- state.reload(opts)
a shortcut for `state.replace(state, opts)`,
with the correct value for `vary` set depending on state chains being
used or not.
`opts.vary` can be set, in which case it is passed as is to `replace`.
Example: does not call `setup` then `close` unless BUILD chain is not empty.
A convenient method only that only replaces current window.history entry:
- state.save()
useful to save current state.data.
### Loc methods
State inherits from Loc:
- parse(str)
parses a url into pathname, query object, hash; and protocol, hostname, port
if not the same domain as the document.
returns a Loc instance.
- format(loc)
format a location to a string with only what was defined,
converts obj.path to pathname, query then stringify query obj if any.
- sameDomain(b)
compare domains (protocol + hostname + port) of two url or objects.
- samePathname(b)
compare domains and pathname of two url or objects.
- sameQuery(b)
compare query strings of two url or objects.
- samePath(b)
compare domain, pathname, querystring (without hash) of two url or objects.
## Debug logs
Just enable `debug` level in the console.
## License
MIT, see LICENSE file.