Package Exports
- @selfage/stateful_navigator
- @selfage/stateful_navigator/init.js
This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (@selfage/stateful_navigator) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.
Readme
@selfage/stateful_navigator
Install
npm install @selfage/stateful_navigator
Overview
Written in TypeScript and compiled to ES6 with inline source map & source. See @selfage/tsconfig for full compiler options. Provides common type-safe classes to navigate between tabs with observable state, while managing browser history.
Observable state
This library is based on @selfage/message to provide an observable state, which is a data object typically generated by @selfage/cli.
We will use a state generated as below for all the following examples, assuming its file path is at ./state.ts.
import { EventEmitter } from 'events';
import { MessageDescriptor, PrimitiveType } from '@selfage/message/descriptor';
export interface State {
on(event: 'showHome', listener: (newValue: boolean, oldValue: boolean) => void): this;
on(event: 'showHistory', listener: (newValue: boolean, oldValue: boolean) => void): this;
}
export class State extends EventEmitter {
private showHome_?: boolean;
get showHome(): boolean {
return this.showHome_;
}
set showHome(value: boolean) {
let oldValue = this.showHome_;
if (value === oldValue) {
return;
}
this.showHome_ = value;
this.emit('showHome', this.showHome_, oldValue);
}
private showHistory_?: boolean;
get showHistory(): boolean {
return this.showHistory_;
}
set showHistory(value: boolean) {
let oldValue = this.showHistory_;
if (value === oldValue) {
return;
}
this.showHistory_ = value;
this.emit('showHistory', this.showHistory_, oldValue);
}
public toJSON(): Object {
return {
showHome: this.showHome,
showHistory: this.showHistory,
};
}
}
export let STATE: MessageDescriptor<State> = {
name: 'State',
factoryFn: () => {
return new State();
},
fields: [
{
name: 'showHome',
primitiveType: PrimitiveType.BOOLEAN,
},
{
name: 'showHistory',
primitiveType: PrimitiveType.BOOLEAN,
},
]
};Create and use tracker and pusher
import { createTrackerAndPusher } from '@selfage/stateful_navigator';
import { State, STATE } from './state';
let defaultState = new State();
defaultState.showHome = true;
let queryParamKey = 'q';
let [browerHistoryTracker, browserHistoryPusher] = createTrackerAndPusher(defaultState, STATE, queryParamKey);
// Now build your DOM tree and add listeners on browerHistoryTracker.state
// browerHistoryTracker.state.on('showHistory', ...)
browerHistoryTracker.initLoad();
// When the state is changed and you want a new history entry.
browserHistoryPusher.push();STATE is an instance of MessageDescriptor and State is the class type. queryParamKey is used to compose a query param q=... and to get the param value, which holds a stringified state.
createTrackerAndPusher() add a listener to popstate event to handle users clicking browser's back button, by parsing the query param q=<stringified historical state>. However, you have to add listeners to each field of browerHistoryTracker.state to actually handle the state change.
browerHistoryTracker.initLoad() should be called only once, after all listeners are added to browerHistoryTracker.state, which parses the query param q=<stringified current state> and fires initial state change events. In other words, initLoad() is essentially triggering an popstate event, but for initial page rendering, which makes listeners added to browerHistoryTracker.state handle both initial rendering and history entry popping.
browserHistoryPusher.push() should be called whenever you want a new history entry with the current state, which creates a new query param q=<stringfied current state> in the URL. It's not called automatically with every state change, because you may want to group several changes together as one history entry.
BTW, the type of browerHistoryTracker is BrowerHistoryTracker<State> by import {BrowerHistoryTracker} from '@selfage/stateful_navigator/browser_history_tracker' and the type of browserHistoryPusher is BrowserHistoryPusher by import {BrowserHistoryPusher} '@selfage/stateful_navigator/browser_history_pusher';
Tabs navigator
import { TabsNavigator, Removeable } from '@selfage/stateful_navigator/tabs';
// Supposing we created a tracker and a pusher as above.
let browerHistoryTracker, browserHistoryPusher = // ...
let homeButton: HTMLDivElement; // Supposing we created a <div> as the button going to the home page.
let homeTabFactoryFn: () => Removeable; // Supposing we have a factory function that creates a home tab.
let historyButton: HTMLDivElement; // Supposing we created a <div> as the button going to the history page.
let historyTabFactoryFn: () => Removeable; // Supposing we have a factory function that creates a history tab.
new TabsNavigator(browserHistoryPusher)
.add(
"home",
(callback) => browerHistoryTracker.state.on("showHome", callback),
(value) => (browerHistoryTracker.state.showHome = value),
(callback) => homeButton.addEventListener("click", callback),
() => homeTabFactoryFn()
)
.add(
"history",
(callback) => browerHistoryTracker.state.on("showHistory", callback),
(value) => (browerHistoryTracker.state.showHistory = value),
(callback) => historyButton.addEventListener("click", callback),
() => historyTabFactoryFn()
);
// Add other listeners to browerHistoryTracker.state.
browerHistoryTracker.initLoad();browserHistoryPusher is passed to TabsNavigator to push a new history entry whenever user switches/navigates to a new tab.
tabsNavigator.add() associates an arbitrary tab key (must be unique per TabsNavigator instance), a field of the state (handling state change as well as setting a new value), a button that triggers navigation, and a factory function that creates the tab.
Note that the fields of the state used to associate with tabs have to be booleans, indicating whether the associated tab should be shown/created or hidden/removed.
The factory functions take no arguments, and will be called every time that tab needs to be shown. The created tab will be removed every time it needs to be hidden. Thus the tab neesd to be Removeable as defined below.
interface Removeable {
remove: () => void;
}The detailed sequence when a navigation happens is as the following.
- A button is clicked.
- A boolean field of the state representing the new tab is set to
true. - A boolean field of the state representing the previous shown tab is set to
undefined. - The previous tab is hidden by being removed.
- The new tab is shown by being created.
- A new history entry is pushed.