diff --git a/README.md b/README.md index 2cd75e1..701d2bc 100644 --- a/README.md +++ b/README.md @@ -10,12 +10,13 @@ Visualizes the state tree and transitions in UI-Router 1.0+. This script augments your app with two components: -1) State Visualizer: Your UI-Router state tree, showing the active state and its active ancestors (green nodes) +1. State Visualizer: Your UI-Router state tree, showing the active state and its active ancestors (green nodes) + - Clicking a state will transition to that state. - If your app is large, state trees can be collapsed by double-clicking a state. - - Supports different layouts and zoom. + - Supports different layouts and zoom. -2) Transition Visualizer: A list of each transition (from one state to another) +2. Transition Visualizer: A list of each transition (from one state to another) - Color coded Transition status (success/error/ignored/redirected) - Hover over a Transition to show which states were entered/exited, or retained during the transition. @@ -28,37 +29,37 @@ Register the plugin with the `UIRouter` object. ### Locate the Plugin -- Using a ` - ``` - - The visualizer Plugin can be found (as a global variable) on the window object. - - ```js - var Visualizer = window['@uirouter/visualizer'].Visualizer; - ``` - -- Using `require` or `import` (SystemJS, Webpack, etc) - - Add the npm package to your project - - ``` - npm install --save @uirouter/visualizer - ``` - - - Use `require` or ES6 `import`: - - ```js - var Visualizer = require('@uirouter/visualizer').Visualizer; - ``` - - ```js - import { Visualizer } from '@uirouter/visualizer'; - ``` +- Using a ` + ``` + + The visualizer Plugin can be found (as a global variable) on the window object. + + ```js + var Visualizer = window['@uirouter/visualizer'].Visualizer; + ``` + +- Using `require` or `import` (SystemJS, Webpack, etc) + + Add the npm package to your project + + ``` + npm install --save @uirouter/visualizer + ``` + + - Use `require` or ES6 `import`: + + ```js + var Visualizer = require('@uirouter/visualizer').Visualizer; + ``` + + ```js + import { Visualizer } from '@uirouter/visualizer'; + ``` ### Register the plugin @@ -79,18 +80,52 @@ var pluginInstance = uiRouterInstance.plugin(Visualizer); ### Configuring the plugin -Optionally you can pass configuration to how the visualizer displays the state tree and the transitions. +You can pass a configuration object when registering the plugin. +The configuration object may have the following fields: + +- `state`: (boolean) State Visualizer is not rendered when this is `false` +- `transition`: (boolean) Transition Visualizer is not rendered when this is `false` +- `stateVisualizer.node.label`: (function) A function that returns the label for a node +- `stateVisualizer.node.classes`: (function) A function that returns classnames to apply to a node + +#### `stateVisualizer.node.label` + +The labels for tree nodes can be customized. + +Provide a function that accepts the node object and the default label and returns a string: + +``` +function(node, defaultLabel) { return "label"; } +``` + +This example adds ` (future)` to future states. +_Note: `node.self` contains a reference to the state declaration object._ + +```js +var options = { + stateVisualizer: { + node: { + label: function (node, defaultLabel) { + return node.self.name.endsWith('.**') ? defaultLabel + ' (future)' : defaultLabel; + }, + }, + }, +}; -The state tree visualizer can be configured to style each node specifically. +var pluginInstance = uiRouterInstance.plugin(Visualizer, options); +``` -Example below marks every node with angular.js view with is-ng1 class. +#### `stateVisualizer.node.classes` + +The state tree visualizer can be configured to add additional classes to nodes. +Example below marks every node with angular.js view with `is-ng1` class. ```js var options = { stateVisualizer: { node: { classes(node) { - return Object.entries(node.views || {}).some(routeView => routeView[1] && routeView[1].$type === 'ng1') + return Object.entries(node.views || {}).some((routeView) => routeView[1] && routeView[1].$type === 'ng1') ? 'is-ng1' : ''; }, @@ -109,8 +144,8 @@ Inject the `$uiRouter` router instance in a run block. ```js // inject the router instance into a `run` block by name -app.run(function($uiRouter) { - var pluginInstance = $uiRouter.plugin(Visualizer); +app.run(function ($uiRouter) { + var pluginInstance = $uiRouter.plugin(Visualizer); }); ``` @@ -124,7 +159,7 @@ import { Visualizer } from "@uirouter/visualizer"; ... -export function configRouter(router: UIRouter) { +export function configRouter(router: UIRouter) { var pluginInstance = router.plugin(Visualizer); } @@ -137,7 +172,6 @@ export function configRouter(router: UIRouter) { #### React (Imperative) - Create the UI-Router instance manually by calling `new UIRouterReact();` ```js @@ -147,7 +181,7 @@ var pluginInstance = router.plugin(Visualizer); ``` #### React (Declarative) - + Add the plugin to your `UIRouter` component ```js diff --git a/src/statevis/interface.ts b/src/statevis/interface.ts index a6a0ed9..0b0d2b4 100644 --- a/src/statevis/interface.ts +++ b/src/statevis/interface.ts @@ -1,3 +1,4 @@ +import { NodeOptions } from './tree/StateTree'; import { StateVisNode } from './tree/stateVisNode'; export interface NodeDimensions { @@ -19,7 +20,7 @@ export interface Renderer { // Applies a layout to the nodes layoutFn(rootNode: StateVisNode): void; // Renders a state label - labelRenderFn(x: number, y: number, node: StateVisNode, renderer: Renderer): any; + labelRenderFn(x: number, y: number, node: StateVisNode, nodeOptions: NodeOptions, renderer: Renderer): any; // Renders an edge edgeRenderFn(rootNode: StateVisNode, renderer: Renderer): any; diff --git a/src/statevis/renderers.tsx b/src/statevis/renderers.tsx index 60d30e2..37d1ade 100644 --- a/src/statevis/renderers.tsx +++ b/src/statevis/renderers.tsx @@ -1,9 +1,10 @@ import { h } from 'preact'; import { Renderer } from './interface'; import { hierarchy, cluster as d3cluster, tree as d3tree, HierarchyPointNode } from 'd3-hierarchy'; +import { NodeOptions } from './tree/StateTree'; import { StateVisNode } from './tree/stateVisNode'; // has or is using -export const RENDERER_PRESETS = { +export const RENDERER_PRESETS: { [name: string]: Partial } = { Tree: { layoutFn: TREE_LAYOUT, sortNodesFn: TOP_TO_BOTTOM_SORT, @@ -75,7 +76,7 @@ export function CLUSTER_LAYOUT(rootNode: StateVisNode) { /** For RADIAL_LAYOUT: projects x/y coords from a cluster layout to circular layout */ function project(x, y) { - let angle = (x - 90) / 180 * Math.PI, + let angle = ((x - 90) / 180) * Math.PI, radius = y; const CENTER = 0.5; return { x: CENTER + radius * Math.cos(angle), y: CENTER + radius * Math.sin(angle) }; @@ -86,13 +87,13 @@ export function RADIAL_LAYOUT(rootNode: StateVisNode) { let layout = d3cluster() .size([360, 0.4]) - .separation(function(a, b) { + .separation(function (a, b) { return (a.parent == b.parent ? 1 : 2) / a.depth; }); let nodes = layout(root); - nodes.each(function(node) { + nodes.each(function (node) { let projected = project(node.x, node.y); let visNode: StateVisNode = node.data; visNode.layoutX = node.x; @@ -104,7 +105,7 @@ export function RADIAL_LAYOUT(rootNode: StateVisNode) { /** Mutates each StateVisNode by copying the new x/y values from the d3 HierarchyPointNode structure */ function updateNodes(nodes: HierarchyPointNode) { - nodes.each(node => { + nodes.each((node) => { node.data.layoutX = node.data.x = node.x; node.data.layoutY = node.data.y = node.y; }); @@ -115,13 +116,11 @@ function updateNodes(nodes: HierarchyPointNode) { // STATE NAME LABEL /////////////////////////////////////////// -export function RADIAL_TEXT(x, y, node: StateVisNode, renderer: Renderer) { +export function RADIAL_TEXT(x, y, node: StateVisNode, nodeOptions: NodeOptions, renderer: Renderer) { let { baseFontSize, zoom } = renderer; let fontSize = baseFontSize * zoom; - let segments = node.name.split('.'); - let name = segments.pop(); - if (name == '**') name = segments.pop() + '.**'; + const label = nodeOptions?.label ? nodeOptions.label(node, defaultLabel(node)) : defaultLabel(node); let angle = node.layoutX || 0; @@ -134,25 +133,30 @@ export function RADIAL_TEXT(x, y, node: StateVisNode, renderer: Renderer) { return ( {' '} - {name}{' '} + {label}{' '} ); } -export function SLANTED_TEXT(x, y, node: StateVisNode, renderer: Renderer) { - let { baseRadius, baseFontSize, baseStrokeWidth, baseNodeStrokeWidth, zoom } = renderer; - let r = baseRadius * zoom; - let fontSize = baseFontSize * zoom; +export function defaultLabel(node: StateVisNode) { let segments = node.name.split('.'); let name = segments.pop(); if (name == '**') name = segments.pop() + '.**'; + return name; +} + +export function SLANTED_TEXT(x, y, node: StateVisNode, nodeOptions: NodeOptions, renderer: Renderer) { + let { baseFontSize, zoom } = renderer; + let fontSize = baseFontSize * zoom; + + const label = nodeOptions?.label ? nodeOptions.label(node, defaultLabel(node)) : defaultLabel(node); let transform = `rotate(-15),translate(0, ${-15 * zoom})`; return ( {' '} - {name}{' '} + {label}{' '} ); } diff --git a/src/statevis/tree/StateNode.tsx b/src/statevis/tree/StateNode.tsx index 8d74103..a5e670d 100644 --- a/src/statevis/tree/StateNode.tsx +++ b/src/statevis/tree/StateNode.tsx @@ -59,7 +59,7 @@ export class StateNode extends Component { )} - {renderer.labelRenderFn(x, y, node, renderer)} + {renderer.labelRenderFn(x, y, node, nodeOptions, renderer)} {node.label} diff --git a/src/statevis/tree/StateTree.tsx b/src/statevis/tree/StateTree.tsx index c407c56..72a64a2 100644 --- a/src/statevis/tree/StateTree.tsx +++ b/src/statevis/tree/StateTree.tsx @@ -8,6 +8,7 @@ import { createStateVisNode, StateVisNode } from './stateVisNode'; export interface NodeOptions { classes?(node: StateVisNode): string; + label?(node: StateVisNode, defaultLabel: string): string; } export interface IProps extends NodeDimensions, VisDimensions { @@ -80,7 +81,7 @@ export class StateTree extends Component { this.updateStates(); // Register onSuccess transition hook to toggle the SVG classes - this.deregisterHookFn = $transitions.onSuccess({}, trans => this.updateNodes(trans)); + this.deregisterHookFn = $transitions.onSuccess({}, (trans) => this.updateNodes(trans)); this.updateNodes(); let lastSuccessful = this.props.router.globals.successfulTransitions.peekTail(); @@ -127,13 +128,13 @@ export class StateTree extends Component { let nodes = this.getNodes(); if (!nodes.length) return; - let rootNode = nodes.filter(state => state.name === '')[0]; + let rootNode = nodes.filter((state) => state.name === '')[0]; this.props.renderer.layoutFn(rootNode); // Move all non-visible nodes to same x/y coords as the nearest visible parent nodes - .filter(node => !node.visible) - .forEach(node => { + .filter((node) => !node.visible) + .forEach((node) => { let visibleAncestor = node._parent; while (visibleAncestor && !visibleAncestor.visible) visibleAncestor = visibleAncestor._parent; if (visibleAncestor) { @@ -145,11 +146,11 @@ export class StateTree extends Component { let dimensions = this.dimensions(); // Transforms x coord from the tree layout to fit the viewport using scale/offset values - const transformX = xval => xval * dimensions.scaleX + dimensions.offsetX; + const transformX = (xval) => xval * dimensions.scaleX + dimensions.offsetX; // Transforms y coord from the tree layout to fit the viewport using scale/offset values - const transformY = yval => yval * dimensions.scaleY + dimensions.offsetY; + const transformY = (yval) => yval * dimensions.scaleY + dimensions.offsetY; - const getCurrentCoords = node => ({ + const getCurrentCoords = (node) => ({ x: node.animX || this.props.width / 2, y: node.animY || this.props.height / 2, }); @@ -158,19 +159,19 @@ export class StateTree extends Component { // [ x1, y1, x2, y2, x3, y3, x4, y4 ] let currentCoords = nodes .map(getCurrentCoords) - .map(obj => [obj.x, obj.y]) + .map((obj) => [obj.x, obj.y]) .reduce((acc, arr) => acc.concat(arr), []); // An array containing target x/y coords for all nodes // [ x1', y1', x2', y2', x3', y3', x4', y4' ] let targetCoords = nodes - .map(node => [transformX(node.x), transformY(node.y)]) + .map((node) => [transformX(node.x), transformY(node.y)]) .reduce((acc, arr) => acc.concat(arr), []); // xyValArray is an array containing x/y coords for all nodes, // interpolated between currentCoords and targetCoords based on time // [ x1'', y1'', x2'', y2'', x3'', y3'', x4'', y4'' ] - const animationFrame = xyValArray => { + const animationFrame = (xyValArray) => { let tupleCount = xyValArray.length / 2; for (let i = 0; i < tupleCount && i < nodes.length; i++) { let node = nodes[i]; @@ -191,35 +192,35 @@ export class StateTree extends Component { ); }; - nodeForState = (nodes, state) => nodes.filter(node => node.name === state.name)[0]; + nodeForState = (nodes, state) => nodes.filter((node) => node.name === state.name)[0]; updateStates = () => { let router = this.props.router; - let states = router.stateService.get().map(s => s.$$state()); + let states = router.stateService.get().map((s) => s.$$state()); let known = this.nodes.map(Object.getPrototypeOf); - let toAdd = states.filter(s => known.indexOf(s) === -1); - let toDel = known.filter(s => states.indexOf(s) === -1); + let toAdd = states.filter((s) => known.indexOf(s) === -1); + let toDel = known.filter((s) => states.indexOf(s) === -1); let nodes = (this.nodes = this.nodes.slice()); if (toAdd.length || toDel.length) { - toAdd.map(s => createStateVisNode(s)).forEach(n => nodes.push(n)); + toAdd.map((s) => createStateVisNode(s)).forEach((n) => nodes.push(n)); toDel - .map(del => nodes.filter(node => del.isPrototypeOf(node))) + .map((del) => nodes.filter((node) => del.isPrototypeOf(node))) .reduce((acc, x) => acc.concat(x), []) - .forEach(node => nodes.splice(nodes.indexOf(node), 1)); + .forEach((node) => nodes.splice(nodes.indexOf(node), 1)); // Rebuild each node's children array - nodes.forEach(n => (n._children = [])); - nodes.forEach(n => { + nodes.forEach((n) => (n._children = [])); + nodes.forEach((n) => { if (!n || !n.parent) return; let parentNode: any = this.nodeForState(nodes, n.parent); if (!parentNode) return; parentNode._children.push(n); n._parent = parentNode; }); - nodes.forEach(n => (n.future = !!n.lazyLoad)); + nodes.forEach((n) => (n.future = !!n.lazyLoad)); } if (!this.unmounted && !this.deregisterStateListenerFn) { @@ -231,30 +232,30 @@ export class StateTree extends Component { }; updateNodes = ($transition$?) => { - let nodes = this.nodes.map(node => Object.assign(node, resetMetadata)); - nodes.forEach(n => (n.future = !!n.lazyLoad)); + let nodes = this.nodes.map((node) => Object.assign(node, resetMetadata)); + nodes.forEach((n) => (n.future = !!n.lazyLoad)); if ($transition$) { let tc = $transition$.treeChanges(); - const getNode = node => this.nodeForState(this.nodes, node.state); + const getNode = (node) => this.nodeForState(this.nodes, node.state); tc.retained .concat(tc.entering) .map(getNode) - .filter(x => x) + .filter((x) => x) .forEach((n: StateVisNode) => (n.entered = true)); tc.retained .map(getNode) - .filter(x => x) + .filter((x) => x) .forEach((n: StateVisNode) => (n.retained = true)); tc.exiting .map(getNode) - .filter(x => x) + .filter((x) => x) .forEach((n: StateVisNode) => (n.exited = true)); tc.to .slice(-1) .map(getNode) - .filter(x => x) + .filter((x) => x) .forEach((n: StateVisNode) => { n.active = true; n.label = 'active'; @@ -270,12 +271,12 @@ export class StateTree extends Component { render() { let renderer = this.props.renderer; - let renderNodes = this.getNodes().filter(node => node.visible && node.animX && node.animY); + let renderNodes = this.getNodes().filter((node) => node.visible && node.animX && node.animY); return (
- {renderNodes.filter(node => !!node.parent).map(node => renderer.edgeRenderFn(node, renderer))} + {renderNodes.filter((node) => !!node.parent).map((node) => renderer.edgeRenderFn(node, renderer))} {renderNodes.map((node: StateVisNode) => (