asr-iso
An isomorphic Server and Client side wrapper for Abstract State Router.
Goal
This module aims to allow the creation of isomorphic state definitions that can be used to render ASR states on the browser and the server. It's designed for use in Progressive Web Apps that want to serve the proper state experience without the need for Javascript to be ready before the page looks right.
Using this library you can serve a page and then enhance it with Javascript without the user having to wait before they can start consuming the content. When coupled with an isomorphic event wrapper you can also provide an interactive experience for clicks and forms that work even in the absence of Javascript on the client's browser.
Installing
npm install --save asr-iso
Usage
The library wraps Abstract State Router on the client and provides a compatible state definition interface on the server, but replaces .go(state, stateParameters)
with a method that returns a promise for the HTML for a state.
Additionally it provides extra parameters which exist on both Client and Server to allow a context object to be passed into the state rendering functions that describes the current application state. Utilising this property along with helper methods to alter the rendering on server and client allows the core state functions to operate equally well on both.
Define a router
var StateRouter = var stateRouter = ;
You supply a client rendering function for the library of your choice. The server side renderer is based on Parse5 and is supplied for you. For example this is a Svelte client renderer, based on TehShrike's ASR Svelte Renderer but modified to be compatible with the SSR renderer in asr-iso.
var defaultOptions = {} { const asr = makePath: stateRoutermakePath stateIsActive: stateRouterstateIsActive { let element: target template content = context if typeof target === 'string' target = document const rendererSuppliedOptions = Object { return optionsmethods ? : options } let svelte try if typeof template === 'string' let svelte = construct(constructor.default, rendererSuppliedOptions) } else throw "Must supply a string template to ensure server side and client side rendering match" catch e return { svelte } stateRouter svelte sveltemountedToTarget = target return svelte return render { const svelte = contextdomApi const element = sveltemountedToTarget svelte const renderContext = Object await } { svelte } { try const element = sveltemountedToTarget const child = element || element catch e } }
rootLocation specification
We will normally use an id
or class
CSS selector for the target so that the server side may render it and the client side find it when it wires up. The default SSR Renderer recognises targets starting with .
or #
Wiring up data into the template
ASR uses a state's activate
method to wire up data. Libraries like Svelte have a different API on the server and client sides so you will probably need to supply different methods. However, if you are going to populate the templates using data supplied by the resolve
method, existing data, state parameters or the global context then this can often be the same boiler plate code for every state.
Firstly each state may declare an activateClient
and activateServer
method that will be used appropriately. In addition the standard activate
method is passed a second parameter for isServer
which is true on the server and falsey on the client and the general context for activate
contains an isServer
property.
The stateRouter
also fires an event each time a state is added allowing you to wire up boilerplate code easily. Here's an example for Svelte
stateRouter; { if contextisServer var dom = contextdomApi; domdata = Object domcss = domtemplateInstancecss domelement = domtemplateInstance; else /* The following code presumes that a window.__context contains the global scope, this is set by state.go */ contextdomApi }
Server Side Rendering API
In the server side your activate
function is passed a htmlFragment
in the context.domApi
property. You set the element
property of this to the HTML to render.
You may also set the .css
property if CSS is rendered separately.
Child views are flagged with either a <ui-view>
element or a container element with a ui-view
attribute.
There is also .data
property. If you set this to an Object then it will be serialized into a dataIsland
on the client with a key of the related state name. You can use this to wire up the data when the Javascript loads to save another round trip to the server.
For example you could add boiler plate code to overide the .resolve
method of states:
stateRouter
Adding States
Adding states is then the same on the client and server:
var StateRouter = var stateRouter = { // Renderer code ...} stateRouter stateRouter { return { }} stateRouter
Setting a state
On the server using .go
will render the HTML and CSS for a state into an object:
/* user contains server side variables for the user */ var state = await stateRouter
So a full example using Svelte, Express with cookies and Redis might look like:
Express route
var express = ;var router = express; var stateRouter = // Defines the isomorphic stateRoutervar shortid = // ID generatorvar redis = // Configured redis clientvar events = // Wildcard hook events router;
Pug Template
extends layout block styles style !{styles} script window.__context = !{context} block content .content !{contents} script(src='index.js')
Where index.js
is the webpack bundled client version.
Client side state setting
The API for the client side is exactly the same.
If rehydrating state from the server you'd normally include something like this to run when the code is ready:
stateRouter
Dynamic construction of templates (optional)
We can provide an extra option to asr-iso when it constructs a router teaching it how to find a dynamic template. This is very useful if you will utilise code splitting to create chunks to be loaded on the client only when a state is activated, further reducing the download burden.
var stateRouter =
The templateConstructor
can return a promise (and so also by async
)
Using this method we can pass a template as the "name" of a file to be dynamically loaded as the representation of a state.
For example loading a Svelte component from a file system in which the component lives in a folder with its name and is defined in an index.html
file - dynamic
might look like this for the browser:
{ return import`..//index.html`} moduleexports = load
And this for Node:
{ return } moduleexports = load
Or any other way you wish to make it work for both.
WebPack client version
Ensure that Parse5 is not included in the WebPack build by using the Ignore Plugin
or specifying it as an external
. It isn't required on the client side and adds unnecessary bloat.
You should also use the Define Plugin
to specify that the build is for the browser like this:
plugins: //... BROWSER: JSON externals: "parse5": "parse5"
More Information
For more information on designing states and the other APIs see Abstract State Router