import * as React from 'react'; import { Component, ValidationMap, ReactElement, ReactNode } from 'react'; import * as PropTypes from 'prop-types'; import { Store, RouterStoreState } from './Store'; import { Route, RouteProps, RouteChildContext } from './Route'; import { Redirect, RedirectProps } from './Redirect'; import { } from './RouterProvider'; import { execValOrFunc, Match } from './utils'; import isEqual = require('deep-equal'); export type SwitchProps = { children?: ReactNode; renderContainer?: (children: ReactElement<(RouteProps | RedirectProps)>[]) => JSX.Element; }; export type SwitchContext = { routerStore: Store; }; function isComponentType

(child: ReactElement, type: any): child is ReactElement

{ return child.type === type; } /** * Render the first Route that match * To do so, Switch take the passed children and test their match props * It find the first match and pass in a swtichIndex in context */ export class Switch extends Component { static displayName: string = 'Switch'; private routerStore: Store; private validChildren: ReactElement<(RouteProps|RedirectProps)>[] = []; static childContextTypes: ValidationMap = { routerStore: PropTypes.instanceOf(Store), }; static contextTypes: ValidationMap = { routerStore: PropTypes.instanceOf(Store), }; context: SwitchContext; private unsubscribe: () => void; private isUnmounted: boolean = false; constructor(props: SwitchProps, context: SwitchContext) { super(props, context); if (!context.routerStore || !context.routerStore.getState()) { throw new Error('Switch need a RouterProvider as ancestor'); } this.unsubscribe = context.routerStore.subscribe(() => { this.update(this.props); }); this.update(props, false); } getChildContext(): RouteChildContext { return { routerStore: this.routerStore, }; } UNSAFE_componentWillReceiveProps(nextProps: SwitchProps): void { this.update(nextProps, false); } componentWillUnmount(): void { this.isUnmounted = true; if (this.unsubscribe) { this.unsubscribe(); } } render(): JSX.Element { const { renderContainer = this.defaultRenderContainer } = this.props; const childrenWithIndex: ReactElement<(RouteProps | RedirectProps)>[] = this.validChildren.map((child, index) => ( React.cloneElement<(RouteProps | RedirectProps), { switchIndex?: number }>( child, { switchIndex: index }, ) )); return renderContainer(childrenWithIndex); } private defaultRenderContainer(children: ReactElement<(RouteProps | RedirectProps)>[]): JSX.Element { return (

{React.Children.toArray(children)}
); } private updateValidChildren(props: SwitchProps): void { this.validChildren = []; React.Children.forEach(props.children, (child) => { if (!React.isValidElement(child)) { return; } if ( isComponentType(child, Route) || isComponentType(child, Redirect) ) { this.validChildren.push(child); } else { console.warn(`Switch only accept Route or Redirect components as children`); } }); } private update(props: SwitchProps, forceUpdate: boolean = true): void { this.updateValidChildren(props); const parentRouterState: RouterStoreState = this.context.routerStore.getState(); let match: Match = null; let matchIndex: number | false = false; this.validChildren.forEach((child, index) => { if (matchIndex !== false) { return; } let tmpMatch: Match = execValOrFunc( child.props.match, parentRouterState.location, parentRouterState.match, ); if (tmpMatch !== false) { match = tmpMatch; matchIndex = index; } }); const newState: RouterStoreState = { location: parentRouterState.location, match: parentRouterState.match, switch: { match, matchIndex }, }; if (!this.routerStore) { // update is called in constructor to init this.routerStore = new Store(newState); if (forceUpdate && !this.isUnmounted) { this.forceUpdate(); } } else { if (!isEqual(this.routerStore.getState(), newState)) { this.routerStore.setState(newState); if (forceUpdate && !this.isUnmounted) { this.forceUpdate(); } } } } }