Skip to content

Commit

Permalink
feat: Next generation auto routing
Browse files Browse the repository at this point in the history
BREAKING CHANGE: Remove NAVIGATE action from your charts
  • Loading branch information
mikaelkaron committed Jun 24, 2019
1 parent db00008 commit d3fe0df
Show file tree
Hide file tree
Showing 8 changed files with 210 additions and 105 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Component, State } from '@stencil/core';
import { Machine, assign, send } from 'xstate';
import { Machine, assign } from 'xstate';
import 'stencil-xstate-router';

type Context = {
Expand Down Expand Up @@ -34,37 +34,17 @@ export class XStateRouterTest {
authenticated: {
initial: 'home',
states: {
home: {
entry: send({
type: 'NAVIGATE',
url: '/'
})
},
account: {
entry: send({
type: 'NAVIGATE',
url: '/account'
})
},
home: {},
woot: {},
account: {},
test: {
initial: 'overview',
states: {
overview: {
entry: send({
type: 'NAVIGATE',
url: '/tests'
})
},
overview: {},
details: {
entry: [
assign({
params: (ctx, event) => event.params || ctx.params
}),
send(ctx => ({
type: 'NAVIGATE',
url: `/tests/${ctx.params.testId}`
}))
]
entry: assign({
params: (ctx, event) => event.params || ctx.params
})
}
},
on: {
Expand Down Expand Up @@ -108,6 +88,7 @@ export class XStateRouterTest {
actions: [assign({ authenticated: false })],
target: 'anonymous'
},
WOOT: 'authenticated.woot',
ROUTE: [
{
target: 'authenticated.home',
Expand Down Expand Up @@ -149,6 +130,6 @@ export class XStateRouterTest {
);

render() {
return <xstate-router-navigo machine={this.machine} />;
return <xstate-router-navigo machine={this.machine} routes={{ROUTE: '', WOOT: '/woot'}} />;
}
}
20 changes: 18 additions & 2 deletions packages/stencil-xstate-router/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ export namespace Components {
*/
'root'?: string;
/**
* Routes to register
*/
'routes'?: Record<string, string>;
/**
* State renderer
*/
'stateRenderer'?: StateRenderer<any, any, RouteEvent>;
Expand Down Expand Up @@ -84,6 +88,10 @@ export namespace Components {
*/
'root'?: string;
/**
* Routes to register
*/
'routes'?: Record<string, string>;
/**
* State renderer
*/
'stateRenderer'?: StateRenderer<any, any, RouteEvent>;
Expand Down Expand Up @@ -117,7 +125,11 @@ export namespace Components {
/**
* Callback for route subscriptions
*/
'route': RouteHandler<any, any, RouteEvent>;
'route': RouteHandler;
/**
* Routes to register
*/
'routes': Record<string, string>;
/**
* State renderer
*/
Expand Down Expand Up @@ -147,7 +159,11 @@ export namespace Components {
/**
* Callback for route subscriptions
*/
'route'?: RouteHandler<any, any, RouteEvent>;
'route'?: RouteHandler;
/**
* Routes to register
*/
'routes'?: Record<string, string>;
/**
* State renderer
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
| `machine` _(required)_ | -- | An XState machine | `StateMachine<any, any, EventObject>` | `undefined` |
| `options` | -- | Interpreter options | `RouterInterpreterOptions` | `undefined` |
| `root` | `root` | The main URL of your application. | `string` | `undefined` |
| `routes` | -- | Routes to register | `{ [x: string]: string; }` | `undefined` |
| `stateRenderer` | -- | State renderer | `(component: Element \| Element[], current: State<any, RouteEvent>, send: (event: SingleOrArray<OmniEvent<RouteEvent>>, payload?: Record<string, any> & { type?: undefined; }) => State<any, RouteEvent>, service: Interpreter<any, any, RouteEvent>) => Element \| Element[]` | `undefined` |
| `useHash` | `use-hash` | If useHash set to true then the router uses an old routing approach with hash in the URL. Fall back to this mode if there is no History API supported. | `boolean` | `false` |

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,19 @@ import RouteRecognizer from 'route-recognizer';
shadow: false
})
export class XstateRouterNavigo implements ComponentInterface {
@State() private routes = new RouteRecognizer();
@State() private recognizer = new RouteRecognizer();
@State() private router: Navigo;

/**
* An XState machine
*/
@Prop() machine!: StateMachine<any, any, EventObject>;

/**
* Routes to register
*/
@Prop() routes?: Record<string, string>;

/**
* Interpreter options
*/
Expand Down Expand Up @@ -88,7 +93,7 @@ export class XstateRouterNavigo implements ComponentInterface {
// that nobody else handled this already
!event.defaultPrevented &&
// that we clicked an anchor,
el.tagName.toUpperCase() === 'A' &&
el.tagName.toLowerCase() === 'a' &&
// that the link has a `href` attribute
el.hasAttribute('href')
) {
Expand All @@ -97,7 +102,7 @@ export class XstateRouterNavigo implements ComponentInterface {
.getAttribute('href')
.replace(new RegExp(`^${this.hash}`), '');
// check if we recognize this url
if (this.routes.recognize(href)) {
if (this.recognizer.recognize(href)) {
// stop default click action
event.preventDefault();
// navigate to the url
Expand All @@ -107,38 +112,43 @@ export class XstateRouterNavigo implements ComponentInterface {
}

render() {
const { options, stateRenderer, componentRenderer } = this;
const { options, routes, stateRenderer, componentRenderer } = this;
return (
<xstate-router
machine={this.machine}
route={(routes, send) =>
routes
? routes
// map paths to unsubscribe callbacks
.map(({ path }) => {
const handler = (params: Record<string, any>) =>
send({
type: 'ROUTE',
path,
params
});
// add path to this.routes
this.routes.add([{ path, handler }]);
// subscribe path to history changes
this.router.on(path, handler);
// return unsubscribe handler
return () => this.router.off(path, handler);
})
: []
}
navigate={url =>
route={routes => {
// add routes to recognizer
routes.forEach(route => this.recognizer.add([route]));
// add routes to router
this.router.on(
routes.reduce(
(acc, { path, handler }) => ({
...acc,
[path]: handler
}),
{}
)
);
// return unsubscribe
return () =>
routes.forEach(({ path, handler }) =>
this.router.off(path, handler)
);
}}
navigate={(path, params) => {
// replace params in path
const url = path.replace(/:(\w+)/g, (_, key) => params[key]);
// compare hash/pathname with url and navigate if no match
(this.useHash
? location.hash !== `${this.hash}${url}`
: location.pathname !== url) && this.router.navigate(url)
}
if (
this.useHash
? location.hash !== `${this.hash}${url}`
: location.pathname !== url
) {
this.router.navigate(url);
}
}}
// pass down config to router
{...{ options, stateRenderer, componentRenderer }}
{...{ options, routes, stateRenderer, componentRenderer }}
>
<slot />
</xstate-router>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@
| ---------------------- | --------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------- |
| `componentRenderer` | -- | Component renderer | `(component: string, props?: ComponentProps<any, any, EventObject>) => Element \| Element[]` | `renderComponent` |
| `machine` _(required)_ | -- | An XState machine | `StateMachine<any, any, EventObject>` | `undefined` |
| `navigate` | -- | Callback for url changes | `(url: string) => void` | `() => {}` |
| `navigate` | -- | Callback for url changes | `(path: string, params?: Record<string, any>) => void` | `() => {}` |
| `options` | -- | Interpreter options | `RouterInterpreterOptions` | `undefined` |
| `route` | -- | Callback for route subscriptions | `(routes: [{ [key: string]: any; path: string; }], send: (event: SingleOrArray<OmniEvent<RouteEvent>>, payload?: Record<string, any> & { type?: undefined; }) => State<any, RouteEvent>) => VoidFunction[]` | `() => []` |
| `route` | -- | Callback for route subscriptions | `(routes: Route[]) => VoidFunction` | `() => () => {}` |
| `routes` | -- | Routes to register | `{ [x: string]: string; }` | `{ ROUTE: '' }` |
| `stateRenderer` | -- | State renderer | `(component: Element \| Element[], current: State<any, RouteEvent>, send: (event: SingleOrArray<OmniEvent<RouteEvent>>, payload?: Record<string, any> & { type?: undefined; }) => State<any, RouteEvent>, service: Interpreter<any, any, RouteEvent>) => Element \| Element[]` | `undefined` |


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,14 @@ export type Send<
TEvent extends EventObject
> = Interpreter<TContext, TSchema, TEvent>['send'];

export type NavigationHandler = (url: string) => void;
export type NavigationHandler = (path: string, params?: Record<string, any>) => void;

export type RouteHandler<
TContext,
TSchema extends StateSchema,
TEvent extends RouteEvent
> = (
routes: [{ path: string; [key: string]: any }],
send: Send<TContext, TSchema, TEvent>
) => VoidFunction[];
export type RouteHandler = (routes: Route[]) => VoidFunction;

export type Route = {
path: string;
handler: (params?: Record<string, any>) => any;
};

export type StateRenderer<
TContext,
Expand Down Expand Up @@ -79,9 +77,14 @@ export type RouteEvent = EventObject & {

export type NavigationEvent = EventObject & {
/**
* URL routed to
* Path routed to
*/
path?: string;

/**
* Route params
*/
url?: string;
params?: Record<string, any>
};

export type RouterProps<
Expand All @@ -93,7 +96,7 @@ export type RouterProps<
options?: RouterInterpreterOptions;
stateRenderer?: StateRenderer<any, any, RouteEvent>;
componentRenderer?: ComponentRenderer<any, any, EventObject>;
route?: RouteHandler<any, any, RouteEvent>;
route?: RouteHandler;
navigate?: NavigationHandler;
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { EventObject } from 'xstate';
import { EventObject, StateMachine } from 'xstate';
import {
ComponentRenderer,
RouteConditionPredicate,
RouteEvent
RouteEvent,
RouteTransitionDefinition
} from './types';

/**
Expand Down Expand Up @@ -40,3 +41,40 @@ export const routeGuard: RouteConditionPredicate<any, RouteEvent> = (
event,
{ cond }
) => event.path === cond.path;

export const getPathById = <TContext, TSchema, TEvent extends EventObject>(
machine: StateMachine<TContext, TSchema, TEvent>,
id: string
) => machine.getStateNodeById(id).path.join('.');

export const getTarget = <TContext, TEvent extends EventObject>(
transition: RouteTransitionDefinition<TContext, TEvent>
) => {
const target = transition.target;
if (target.length !== 1) {
throw new Error(
`expected target.length to be 1, current: ${target.length}`
);
}
return target[0];
};

export const getTransition = <TContext, TEvent extends EventObject>(
transitions: RouteTransitionDefinition<TContext, TEvent>[]
) => {
if (transitions.length !== 1) {
throw new Error(
`expected transitions.length to be 1, current: ${transitions.length}`
);
}
return transitions[0];
};

export const getPath = <TContext, TEvent extends EventObject>(
transition: RouteTransitionDefinition<TContext, TEvent>
) => {
if (!transition.cond || !transition.cond.path) {
throw new Error('expected transition.cond.path to exist');
}
return transition.cond.path;
};
Loading

0 comments on commit d3fe0df

Please sign in to comment.