app-manager
Script for managing the lifecycles of multiple apps on a single page
Use Case
- Your app has been built with a microservices architecture.
- Your front-end code has become sufficiently complex that it warrants splitting up and/or is isomorphically rendered
- You wish to keep your app 'feeling' like an SPA
Minimal Example
config.js
slots: APP: querySelector: '.app' fragments: EXAMPLE1_FRAGMENT: slot: 'APP' async { return ; } async { return /* @html */` <div class=""> </div> `; } EXAMPLE2_FRAGMENT: slot: 'APP' async { return ; } async { return /* @html */` <div class=""> </div> `; } routes: EXAMPLE1_APP: path: '/apps/example1' fragment: 'EXAMPLE1_FRAGMENT' EXAMPLE2_APP: path: '/apps/example2' fragment: 'EXAMPLE2_FRAGMENT'
client.js
; ; // ... const options = importTimeout: 3000; ;
server.js
; ; const getSlotsMarkup = ; // ... app;
Api
app-manager
exports a function that takes three parameters:
config
- describes your app to app-managerevents
- a event emitter module with the same API as the native node.js moduleoptions
- Optional options object
Config
An object containing maps of the slots, fragments, and routes that comprise your app.
slots: SLOT_NAME: slot // As below fragments: FRAGMENT_NAME: fragment // As below routes: ROUTE_NAME: route // As below
State
The state object is passed as a parameter into almost every function within and without app-manager. It contains the useful current state of the browser and the derived state of the application.
resource: '/app/some-path?query=value' // The pathname and query string currently displayed in the browser title: 'My Page' // The current document page title historyState: null // The current contents of the history state object eventTitle: 'hc-initialise' // The name of the event (as below) that caused the current action to occur route // The config for the current route (as below) prevRoute // The config for the route that was previous to the current one. Is null when app is first loaded. ...additionalState // Any additional state returned from the getAdditionalState function passed into options (as below)
Route
A route
determines which fragments are displayed on which path
.
path: '/app/:path' // Path or... paths: '/app/:path1' '/app/:path2' // ...paths that uniquely identifies this app. Analogous to a route in express. fragment: 'FRAGMENT_NAME' // Fragment name or... fragments: 'APP_FRAGMENT_NAME' 'HEADER_FRAGMENT_NAME' // ...ordered array of fragment names to be displayed for that route. If two fragments occupy the same slot, the first will take precedence.
Slot
A slot
is a wrapper for a DOM element on a page.
querySelector: '.app' // Uniquely returns a single DOM element when passed to document.querySelector. async { // Optional function that should return some markup to be displayed in a slot between a fragment being unmounted and a new one being mounted in its place. return /* @html */`<img src="/spinner.gif" alt="Loading " />` } async { // Optional function that returns a markup string to be displayed in a slot if one of the lifecycle methods for a script throws an error. return /* @html */`<p class="error">An error occurred while browsing to .`; } async { // Optional function that's called if options.ssrHaltOnError is false and ssrGetMarkup for a fragment which is being loaded into this slot throws an error. Should return a markup string. return /* @html */` <div class=""> <p class="error">An error occurred while loading .</p> </div> `; }
Fragment
A fragment
is the container for your script
.
slot: 'SLOT_NAME' // Slot name or... slots: 'LEFT_SLOT_NAME' 'RIGHT_SLOT_NAME' // ...ordered array of slot names. Fragment will be loaded in the first empty slot possible. async { // * Optional function (only needed if using app-manager to manage client-side lifecycle) that fetches the script (as below) for your fragment. // ... return script; } async { // * Optional function (only needed if using app-manager for server-side rendering) that fetches the markup needed to render your fragment into a DOM. // * Should contain any serialised app state you wish to use to hydrate the app on the client, any inline styles, etc. // * Function is called with the querySelector of the slot into which it is being mounted, state (as below), and any arguments you pass into the 'appManagerServer.getSlotsMarkup' function. // ... return markupString; }
Script
A script
is the entry-point to your code.
It should contain the lifecycle methods to be called as the user browses around your site:
version: 6 // Lets app-manager know which schema to expect from your script async { // * Optional function that will be called if the parent fragment is included on the page on first load // * If rendering your app isomorphically, you will likely wish to read app state from the DOM here. // * Function is called with the element into which the fragment is to be mounted and state (as below) const appState = window'__your_app_state__' ReactDOM; } async { // * Optional function that will be called if the user browses onto an path for which the parent fragment is to be mounted. // * If your app relies on initial state, you should fetch it here // * Function is called with the element into which the fragment is to be mounted and state (as below) const props = await ; ReactDOM; } async { // * Optional function that will be called when an event is fired from the history api, and the fragment is to remain mounted on the page // * If your fragment has multiple views that should be routed between (for example with path params), this lifecycle method will be where routing is managed. // * Function is called with state (as below) await ; } async { // * Optional function that will be called when the user browses away from an path on which the parent fragment is mounted. // * Useful to clear up any event listeners, and to reset any mutable state. // * Function is called with the element into which the fragment is to be mounted and state (as below) ReactDOM; }
Events
Suggested: EventEmitter3
{ // ... } { // ... } { // ... }
List of events emitted by app-manager
- hc-halted
- hc-error
- hc-warning
- hc-beforeunload
- hc-popstate
- hc-replacestate
- hc-pushstate
- hc-statechange
- hc-initialise
Options
All properties in the options object are optional.
importTimeout: 4000 // Timeout for loading scripts and finding elements in the DOM. Defaults to 4000 ssrHaltOnError: false // Boolean value that determines whether the getSlotsMarkup function should throw on error or call the slot's ssrGetErrorMarkup. Defaults to false. parentQuerySelector: '.app' // Query selector that is passed into the getElement function (below) to find the parent element of your app. If not provided, document.body is used. async { // Function that takes a state object and returns the name of a route derived from that state. // Defaults to a function that scans the list of routes passed in config and performs an equality check between the current resource (pathname + query) and each route's path const params = await ; return paramsrouteName; } async { // Function that allows you to return an object of arbitrary values that will be passed into the state object const params = await ; return params ; } async { // Function that takes a parent DOM Element and the query selector for the slot we're trying to find on the page, and should return a DOM Element. // Defaults to container.querySelector(querySelector) return container; } async { // Function that is called when browsing onto an app-manager controlled page without hydrating. // Allows you to set the default static DOM into which the app will be rendered. // Useful when nesting app-manager instances. return /* @html */`<div class="app"></div>`; }