Skip to content

Instantly share code, notes, and snippets.

@dralletje
Created June 10, 2017 16:31
Show Gist options
  • Select an option

  • Save dralletje/2054345388fff74d3a5308e3c6dd9a72 to your computer and use it in GitHub Desktop.

Select an option

Save dralletje/2054345388fff74d3a5308e3c6dd9a72 to your computer and use it in GitHub Desktop.
My "simple" react router
// @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;
// @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;
// @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),
});
},
};
};
// @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