npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

asr-iso

v1.0.4

Published

Abstract State Router wrapper with server side and client side support

Downloads

12

Readme

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 = require('asr-iso')
var stateRouter = StateRouter(clientRender, rootLocation /* e.g. #here */, options);

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 = {}

function clientRenderer(stateRouter) {
    const asr = {
        makePath: stateRouter.makePath,
        stateIsActive: stateRouter.stateIsActive,
    }

    async function render(context, cb) {
        let {element: target, template, content} = context
        if (typeof target === 'string') {
            target = document.querySelector(target)
        }
        const rendererSuppliedOptions = Object.assign({}, defaultOptions, {
            target,
            data: Object.assign(content, defaultOptions.data, {asr}),
        })

        function construct(component, options) {
            return options.methods
                ? instantiateWithMethods(component, options, options.methods)
                : new component(options)
        }

        let svelte

        try {
            if (typeof template === 'string') {
                let constructor = await dynamic(template)
                svelte = construct(constructor.default, rendererSuppliedOptions)
            } else {
                throw new Error("Must supply a string template to ensure server side and client side rendering match")
            }
        } catch (e) {
            cb(e)
            return
        }

        function onRouteChange() {
            svelte.set({
                asr,
            })
        }

        stateRouter.on('stateChangeEnd', onRouteChange)

        svelte.on('destroy', () => {
            stateRouter.removeListener('stateChangeEnd', onRouteChange)
        })

        svelte.mountedToTarget = target
        return svelte
    }

    return {
        render,
        reset: async function reset(context, cb) {
            const svelte = context.domApi
            const element = svelte.mountedToTarget

            svelte.teardown()

            const renderContext = Object.assign({element}, context)

            await render(renderContext, cb)
        },
        destroy: function destroy(svelte, cb) {
            svelte.teardown()
            cb()
        },
        getChildElement: function getChildElement(svelte, cb) {
            try {
                const element = svelte.mountedToTarget
                const child = element.querySelector('ui-view') || element.querySelector('[ui-view]')
                cb(null, child)
            } catch (e) {
                cb(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.on('add', function (state, isServer) {
    state.activate = svelteActivate;
});

function svelteActivate(context) {
    if (context.isServer) {
        var dom = context.domApi;
        dom.data = Object.assign({}, context.data, context.parameters, dom.context)               
        dom.css = dom.templateInstance.renderCss().css
        dom.element = dom.templateInstance.render(dom.data);
    } else {
        /* 
        The following code presumes that a window.__context contains the global scope,
        this is set by state.go
        */
        context.domApi.set(Object.assign({}, context.data, context.parameters, 
            typeof window !== 'undefined' ? window.__context : null))
    }
}

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.on('add', function (state, isServer) {
    state.activate = svelteActivate
    if (!isServer) {
        var resolve = state.resolve
        if (resolve) {
            state.resolve = function (data) {
                if (window.dataIslands) {
                    if (dataIslands[state.name]) {
                        Object.assign(data, dataIslands[state.name])
                        delete dataIslands[state.name]
                        return Promise.resolve(data)
                    }
                }
                return resolve.apply(state, Array.prototype.slice.call(arguments))
            }
        }
    }
})

Adding States

Adding states is then the same on the client and server:

var StateRouter = require('asr-iso')
var stateRouter = StateRouter(clientRenderer, '#here')

function clientRenderer(stateRouter) {
    // Renderer code ...
}

stateRouter.on('add', function (state, isServer) {
    state.activate = svelteActivate //for example
})

stateRouter.addState({
    name: 'app',
    route: '/',
    data: {
        name: 'mike'
    },
    template: 'holder' //Dynamically resolve 'holder'
})

function delay(time) {
    return new Promise(function (resolve) {
        setTimeout(resolve, time)
    })
}

stateRouter.addState({
    name: 'app.home',
    route: 'home',
    data: {
        surname: 'talbot'
    },
    template: 'basic',  //Dynamically resolve 'basic'
    resolve: async function (data) {
        await delay(1000) //Simulate server delay
        data.company = "3radical"
    }
})

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.go(user.state || 'app.home', {id: 123}, null, user)

So a full example using Svelte, Express with cookies and Redis might look like:

Express route

require('svelte/ssr/register')
var express = require('express');
var router = express.Router();

var stateRouter = require('./states') // Defines the isomorphic stateRouter
var shortid = require('shortid')      // ID generator
var redis = require('./redis')        // Configured redis client
var events = require('./events')      // Wildcard hook events

router.get('/', async function (req, res) {
    var id = req.cookies.routerId
    
    // Get or create the user representation
    var user
    if (!id) {
        id = shortid.generate()
        user = {}
        // Allow hook(s) to set initial values
        events.emit(`initialize:${id}`, user)
    } else {
        user = JSON.parse((await redis.get(`--router-state--${id}`)) || "{}")
    }
    // Allow hook(s) to update the values
    events.emit(`retrieve:${id}`, user)
    
    // Use a cookie to manage the user representation
    res.cookie('routerId', id, {maxAge: 1000 * 60 * 60 * 24 * 7 * 12})
    
    // Render the state
    var state = await stateRouter.go(user.state || 'app.home', {id: 123}, null, user)
    
    // Store the user representation
    await redis.set(`--router-state--${id}`, JSON.stringify(user))
    
    // Output the page
    res.render('index', {
        contents: state.html, 
        styles: state.css, 
        context: JSON.stringify(user)
    });
});

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:

import stateRouter from '../states'

stateRouter.evaluateCurrentRoute(
    window.__context.state || 'app.home', 
    window.__context.stateParameters
)

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 = StateRouter(clientRenderer, '#here', {
    templateConstructor: function (state) {
        //Import a template on the client with import() and 
        //require on Node
        return dynamic(state.template)
    }
})

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:

function load(src) {
    return import(`../${src}/index.html`)
}

module.exports = load

And this for Node:

function load(src) {
    return require(`../${src}/index.html`)
}

module.exports = 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: [
    //...
    new webpack.DefinePlugin({
        BROWSER: JSON.stringify(true)
    }),
],
externals: {
    "parse5": "parse5"
}

More Information

For more information on designing states and the other APIs see Abstract State Router