Created
June 10, 2017 16:31
-
-
Save dralletje/2054345388fff74d3a5308e3c6dd9a72 to your computer and use it in GitHub Desktop.
My "simple" react router
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // @flow | |
| import React from 'react'; | |
| import { View, Animated, Dimensions } from 'react-native'; | |
| import { startAnimationAsync, requestAnimationFrameAsync } from './utils'; | |
| import type { AnimationInput } from './transitions'; | |
| import TransitionDelayer from './TransitionDelayer'; | |
| type EndResult = { finished: boolean }; | |
| type EndCallback = (result: EndResult) => void; | |
| export type CompositeAnimation = { | |
| start: (callback?: ?EndCallback) => void, | |
| stop: () => void, | |
| }; | |
| /* eslint-disable */ | |
| export type RouteIdentifier = ReactClass<any> | string; | |
| export type RouteStack = RouteIdentifier[]; | |
| export type Transition = { | |
| previous: ?Object, | |
| next: ?Object, | |
| animation: () => CompositeAnimation, | |
| }; | |
| export type TransitionFactory = (input: AnimationInput) => Transition; | |
| export type NavigatorConfig = { | |
| pop: () => void, | |
| popTo: (identifier: RouteIdentifier) => void, | |
| popN: (n: number) => void, | |
| onPageFocus: (routeStack: RouteStack) => void, | |
| }; | |
| const { width, height } = Dimensions.get('window'); | |
| // type ExtenderFunction = <T, Object>(value: T, object: Object) => T; | |
| // type NavigatorConfigExtender = $Shape<{ | |
| // pop: ExtenderFunction<NavigatorConfig.pop>, | |
| // popTo: ExtenderFunction<NavigatorConfig.popTo>, | |
| // popN: ExtenderFunction<NavigatorConfig.popN>, | |
| // onPageFocus: ExtenderFunction<NavigatorConfig.onPageFocus>, | |
| // }> | |
| type Falsy = null | typeof undefined | false; | |
| type Props = { | |
| children?: any, // eslint-disable-line | |
| identifier: RouteIdentifier, | |
| childPage?: Falsy | React.Element<*>, | |
| keepRendered?: boolean, | |
| transition?: ?TransitionFactory, | |
| onFocus?: (routeStack: RouteStack) => void, | |
| }; | |
| type State = { | |
| childPageMemorial: Falsy | React.Element<*>, | |
| activeTransition: Transition, | |
| }; | |
| /* eslint-enable */ | |
| const NO_TRANSITION = { | |
| previous: null, | |
| next: null, | |
| animation: (): any => { | |
| throw new Error('Trying to get animation from NO_TRANSITION!'); | |
| }, | |
| }; | |
| class ConsoleGroup { | |
| active: boolean; | |
| name: string; | |
| constructor(name: string) { | |
| this.name = name; | |
| } | |
| on() { | |
| if (this.active) { | |
| return; | |
| } | |
| if (console.group) { | |
| console.group(this.name); | |
| } | |
| this.active = true; | |
| } | |
| off() { | |
| if (!this.active) { | |
| return; | |
| } | |
| if (console.groupEnd) { | |
| console.groupEnd(); | |
| } | |
| this.active = false; | |
| } | |
| } | |
| const debug = () => {} | |
| // const debug = (...args) => console.log('Router:', ...args); | |
| class Page extends React.Component { | |
| static wrapNavigator = ( | |
| previousNavigator: NavigatorConfig, | |
| options: { | |
| onPop: () => void, | |
| identifier: RouteIdentifier, | |
| }, | |
| ): NavigatorConfig => { | |
| return { | |
| pop: options.onPop, | |
| popN: n => { | |
| if (n - 1 === 0) { | |
| options.onPop(); | |
| } else { | |
| previousNavigator.popN(n - 1); | |
| } | |
| }, | |
| popTo: identifier => { | |
| if (identifier === options.identifier) { | |
| options.onPop(); | |
| } else { | |
| previousNavigator.popTo(identifier); | |
| } | |
| }, | |
| onPageFocus: routeStack => | |
| previousNavigator.onPageFocus([...routeStack, options.identifier]), | |
| }; | |
| }; | |
| /* YES THIS PART IS GLOBAL AND STATIC I KNOW | |
| but it will, I think/hope never collide because of two Page's, if so | |
| I'll need to find a better/novel solution | |
| */ | |
| static forcedNextTransition: ?TransitionFactory; | |
| // Meant to be called in the component, just before setState | |
| static setNextTransition(transitionFactory: TransitionFactory) { | |
| Page.forcedNextTransition = transitionFactory; | |
| } | |
| // Meant to be called in transition setup: will reset `forcedNextTransition` | |
| static getNextTransition(): ?TransitionFactory { | |
| let forcedNextTransition = this.forcedNextTransition; | |
| Page.forcedNextTransition = null; | |
| return forcedNextTransition; | |
| } | |
| state: State; | |
| props: Props; | |
| consolegroup: ConsoleGroup; | |
| constructor(props: Props) { | |
| super(props); | |
| this.state = { | |
| childPageMemorial: null, | |
| activeTransition: NO_TRANSITION, | |
| }; | |
| this.consolegroup = new ConsoleGroup(this.name()); | |
| this.consolegroup.on(); | |
| } | |
| notifyFocus() { | |
| let { onFocus, childPage, identifier } = this.props; | |
| if (onFocus && !childPage) { | |
| onFocus([identifier]); | |
| } | |
| } | |
| name(): string { | |
| let { identifier } = this.props; | |
| if (typeof identifier === 'string') { | |
| return identifier; | |
| } else if (identifier.displayName) { | |
| return identifier.displayName; | |
| } else { | |
| return identifier.name; | |
| } | |
| } | |
| componentDidMount() { | |
| this.notifyFocus(); | |
| } | |
| componentWillUnmount() { | |
| this.consolegroup.off(); | |
| } | |
| // Check for unmounting of child, if so, let parent know | |
| componentDidUpdate(prevProps: Props) { | |
| if (!this.props.childPage && prevProps.childPage) { | |
| this.notifyFocus(); | |
| } | |
| } | |
| componentWillReceiveProps(nextProps: Props) { | |
| let childPage = nextProps.childPage; | |
| let oldChildPage = this.props.childPage; | |
| let hasChildPage = Boolean(childPage); | |
| let hadChildPage = Boolean(oldChildPage); | |
| // We either got or lost a childPage | |
| if (hadChildPage !== hasChildPage) { | |
| debug('Ready for transition...'); | |
| debug('hadChildPage:', hadChildPage); | |
| const nextTransition = nextProps.transition || Page.getNextTransition(); | |
| const transitionResults = | |
| nextTransition && nextTransition(Dimensions.get('window')); | |
| if (typeof transitionResults === 'function') { | |
| debug('transitionResults:', transitionResults.toString()); | |
| console.error(`You've not applied a curried animation function`); | |
| return; | |
| } | |
| if (nextTransition) debug('nextTransition:', typeof nextTransition); | |
| // DEBUG | |
| if (hasChildPage) { | |
| this.consolegroup.off(); | |
| } else { | |
| this.consolegroup.on(); | |
| } | |
| if (!transitionResults) { | |
| return; | |
| } | |
| debug('transitionResults:', transitionResults.toString()); | |
| this.setState({ | |
| activeTransition: transitionResults, | |
| childPageMemorial: hadChildPage ? oldChildPage : childPage, | |
| }); | |
| } | |
| } | |
| shouldComponentUpdate(nextProps: Props, nextState: State) { | |
| if (nextState.activeTransition === NO_TRANSITION) { | |
| return true; | |
| } | |
| if (this.state.activeTransition === NO_TRANSITION) { | |
| return true; | |
| } | |
| debug('Update prevented'); | |
| return false; | |
| } | |
| render() { | |
| let { childPageMemorial, activeTransition } = this.state; | |
| let { childPage, keepRendered, children } = this.props; | |
| let animating = | |
| activeTransition.next !== NO_TRANSITION.next && | |
| activeTransition.previous !== NO_TRANSITION.previous; | |
| let pointerEvents = animating ? 'none' : 'auto'; | |
| let showFrame = animating || keepRendered || !childPage; | |
| let transition = childPage | |
| ? activeTransition | |
| : { | |
| previous: { | |
| ...activeTransition.next, | |
| zIndex: 100, | |
| }, | |
| next: activeTransition.previous, | |
| }; | |
| debug('showFrame:', showFrame); | |
| debug('Child:', childPage || childPageMemorial); | |
| let renderChildPage = () => { | |
| if (!childPage && !childPageMemorial) { | |
| return null; | |
| } | |
| return childPage || childPageMemorial; | |
| }; | |
| return ( | |
| <TransitionDelayer | |
| active={animating} | |
| onLoad={async () => { | |
| await requestAnimationFrameAsync(); | |
| debug('Starting animation'); | |
| await startAnimationAsync(activeTransition.animation()); | |
| debug('Animation done'); | |
| this.setState({ | |
| activeTransition: NO_TRANSITION, | |
| childPageMemorial: null, | |
| }); | |
| }} | |
| > | |
| <View | |
| style={{ | |
| flex: 1, | |
| }} | |
| > | |
| {showFrame && | |
| <Animated.View | |
| style={[ | |
| { overflow: 'hidden' }, | |
| { position: 'absolute', top: 0, bottom: 0, left: 0, right: 0 }, | |
| // transition.previous, | |
| ]} | |
| pointerEvents={pointerEvents} | |
| children={children} | |
| />} | |
| {/* TODO Allow a component to be inserted here, by the transition */} | |
| {(childPage || childPageMemorial) && | |
| <Animated.View | |
| style={[ | |
| { overflow: 'hidden' }, | |
| { position: 'absolute', top: 0, bottom: 0, left: 0, right: 0 }, | |
| , | |
| transition.next, | |
| ]} | |
| pointerEvents={pointerEvents} | |
| children={renderChildPage()} | |
| />} | |
| </View> | |
| </TransitionDelayer> | |
| ); | |
| } | |
| } | |
| export default Page; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // @flow | |
| import React from 'react'; | |
| import Promise from 'bluebird'; | |
| import { createDeferred } from './utils'; | |
| type Props = { | |
| onLoad: () => any, | |
| active: boolean, | |
| children?: any, | |
| }; | |
| export class DelayTransition extends React.Component { | |
| static contextTypes = contextTypes; | |
| resolver: () => void; | |
| // $FlowFixMe | |
| constructor(props, context) { | |
| super(props, context); | |
| this.resolver = | |
| typeof context.requestTransitionDelay === 'function' | |
| ? context.requestTransitionDelay() | |
| : () => {}; | |
| } | |
| render() { | |
| return this.props.children(this.resolver); | |
| } | |
| } | |
| export const contextTypes = { | |
| requestTransitionDelay: React.PropTypes.func, | |
| }; | |
| const NOOP = Function.prototype; | |
| class TransitionDelayer extends React.Component { | |
| transitionDelays = ([]: Array<Promise<void>>); | |
| props: Props; | |
| static childContextTypes = contextTypes; | |
| getChildContext() { | |
| const { active } = this.props; | |
| return { | |
| requestTransitionDelay: () => { | |
| if (active) { | |
| let { resolve, promise }: { resolve: () => void, promise: Promise<void> } = createDeferred(); | |
| this.transitionDelays.push(promise); | |
| return resolve; | |
| } else { | |
| return NOOP; | |
| } | |
| }, | |
| }; | |
| } | |
| // componentWillReceiveProps(props) { | |
| // console.log('props:', props); | |
| // } | |
| componentDidUpdate(prevProps: Props): any { | |
| let { active, onLoad } = this.props; | |
| if (active) { | |
| Promise.all(this.transitionDelays).then(() => { | |
| onLoad(); | |
| }); | |
| } | |
| } | |
| render() { | |
| return React.Children.only(this.props.children); | |
| } | |
| } | |
| export default TransitionDelayer; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // @flow | |
| import { Animated, Easing } from 'react-native'; | |
| export type AnimationInput = { width: number, height: number }; | |
| const interpol = ( | |
| value: Animated.Value, | |
| to: number[], | |
| from: number[] = [0, 1], | |
| ) => { | |
| // eslint-disable-line | |
| return value.interpolate({ | |
| inputRange: from, | |
| outputRange: to, | |
| extrapolate: 'clamp', | |
| }); | |
| }; | |
| export const bounceInHorizontal = (from: 'left' | 'right') => ({ | |
| width, | |
| }: AnimationInput) => { | |
| const endPosition = from === 'right' ? width : -width; | |
| const value = new Animated.Value(0); | |
| return { | |
| previous: { | |
| transform: [{ translateX: interpol(value, [0, -endPosition]) }], | |
| }, | |
| next: { | |
| transform: [{ translateX: interpol(value, [endPosition, 0]) }], | |
| }, | |
| animation: () => { | |
| return Animated.timing(value, { | |
| toValue: 1, | |
| easing: Easing.inOut(Easing.exp), | |
| useNativeDriver: true, | |
| }); | |
| }, | |
| }; | |
| }; | |
| export const bounceInVertical = (from: 'top' | 'bottom') => ({ | |
| height, | |
| }: AnimationInput) => { | |
| const endPosition = from === 'bottom' ? height : -height; | |
| const value = new Animated.Value(0); | |
| return { | |
| previous: { | |
| transform: [{ translateY: interpol(value, [0, -endPosition]) }], | |
| }, | |
| next: { | |
| transform: [{ translateY: interpol(value, [endPosition, 0]) }], | |
| }, | |
| animation: () => { | |
| return Animated.timing(value, { | |
| toValue: 1, | |
| // easing: Easing.inOut(Easing.ease), | |
| // easing: Easing.bezier(0.44, 1.3, 0.54, -0.31), | |
| duration: 500, | |
| useNativeDriver: true, | |
| }); | |
| }, | |
| }; | |
| }; | |
| export const fadeIn = () => { | |
| const value = new Animated.Value(0); | |
| return { | |
| previous: {}, | |
| next: { | |
| opacity: value, | |
| }, | |
| animation: () => { | |
| return Animated.timing(value, { | |
| toValue: 1, | |
| duration: 500, | |
| useNativeDriver: true, | |
| }); | |
| }, | |
| }; | |
| }; | |
| export const zoomInTo = ( | |
| dimensions: { width: number, height: number, x: number, y: number }, | |
| ) => ({ width, height }: AnimationInput) => { | |
| const value = new Animated.Value(0); | |
| const scale = Math.max(dimensions.width / width, dimensions.height / height); | |
| const deltaX = 0.5 * width * (1 - scale) - dimensions.x; | |
| const deltaY = 0.5 * height * (1 - scale) - dimensions.y - 9; | |
| return { | |
| previous: { | |
| transform: [ | |
| { translateY: interpol(value, [0, deltaY * (1 / scale)]) }, | |
| { translateX: interpol(value, [0, deltaX * (1 / scale)]) }, | |
| { scale: interpol(value, [1, 1 / scale]) }, | |
| ], | |
| }, | |
| next: { | |
| transform: [ | |
| { translateY: interpol(value, [-deltaY, 0]) }, | |
| { translateX: interpol(value, [-deltaX, 0]) }, | |
| { scale: interpol(value, [scale, 1]) }, | |
| ], | |
| opacity: interpol(value, [0, 1], [0, 0.5]), | |
| }, | |
| animation: () => { | |
| return Animated.timing(value, { | |
| toValue: 1, | |
| duration: 1000, | |
| easing: Easing.inOut(Easing.cubic), | |
| }); | |
| }, | |
| }; | |
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // @flow | |
| /* eslint-disable */ | |
| import { Animated } from 'react-native'; | |
| import type React from 'react'; | |
| export type Dimension = { width: number, height: number, x: number, y: number }; | |
| type EndResult = { finished: bool }; | |
| type EndCallback = (result: EndResult) => void; | |
| export type CompositeAnimation = { | |
| start: (callback?: ?EndCallback) => void, | |
| stop: () => void, | |
| }; | |
| export const callOrReturn = <T>(fn: mixed, args: mixed[], orValue: T): T => { | |
| if (typeof fn === 'function') { | |
| let result = fn(...args); | |
| return result; | |
| } else { | |
| return orValue; | |
| } | |
| }; | |
| type DimensionCallback = (size: Dimension) => void; | |
| // Measure a ref fully, in a way that react-native does not support nicely now :( | |
| // (with absolute x and y) | |
| export const measureRef = (ref: any, cb: DimensionCallback) => { | |
| if (ref === null) { | |
| return; | |
| } | |
| setTimeout(() => { | |
| ref.measure((ox, oy, refWidth, refHeight, x, y) => { | |
| cb({ x, y, width: refWidth, height: refHeight }); | |
| }); | |
| }); | |
| }; | |
| // Same measure but in a beautiful functional way | |
| export const measureFP = (cb: DimensionCallback) => (ref: any) => { | |
| return measureRef(ref, cb); | |
| }; | |
| export const componentName = (Component: Class<React.Component<*,*,*>>): string => Component.displayName || Component.name || ''; | |
| /* Create a defered object, sometimes you need this */ | |
| type Defered<T> = { | |
| resolve: (value: T) => void, | |
| reject: (err: mixed) => void, | |
| promise: Promise<T>, | |
| }; | |
| export const createDeferred = <T>(): Defered<T> => { | |
| let resolve = () => {}; | |
| let reject = () => {}; | |
| let promise = new Promise((yell, cry) => { | |
| resolve = yell; | |
| reject = cry; | |
| }); | |
| return { promise, resolve, reject }; | |
| }; | |
| /* | |
| Functions that change all kinds of (React) actions to promises. | |
| Allows to use them with ease using async/await :D | |
| */ | |
| // Wrap setState in a promise that resolves when it is applied | |
| type StateSetter<State> = | |
| | $Shape<State> | |
| | (currentState: State) => $Shape<State> | |
| ; | |
| export const setStateAsync = <State>(that: React.Component<any, any, State>, state: StateSetter<State>): Promise<void> => { | |
| return new Promise(yell => that.setState((state: any), yell)); | |
| }; | |
| // Start an Animated animation and resolve the promise when the animation is done | |
| export const startAnimationAsync = (animation: CompositeAnimation): Promise<{ finished: boolean }> => { | |
| return new Promise(yell => animation.start(yell)); | |
| }; | |
| // Stop an Animated animation and resolve when actually done | |
| export const stopAnimationAsync = (animatedValue: Animated.Value): Promise<number> => { | |
| return new Promise(yell => animatedValue.stopAnimation(yell)); | |
| }; | |
| // | |
| export const requestAnimationFrameAsync = (): Promise<void> => { | |
| return new Promise(yell => requestAnimationFrame(yell)); | |
| }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment