Skip to content

Instantly share code, notes, and snippets.

@mgenov
Created August 26, 2018 07:25
Show Gist options
  • Save mgenov/1c306fafa80e61995f7d9b2dce6b16cf to your computer and use it in GitHub Desktop.
Save mgenov/1c306fafa80e61995f7d9b2dce6b16cf to your computer and use it in GitHub Desktop.

Revisions

  1. mgenov created this gist Aug 26, 2018.
    32 changes: 32 additions & 0 deletions Example.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,32 @@
    <MatchTabs>
    <MatchTab
    pathname="/home"
    renderContent={props => {
    return <RecursiveItem rootPath="/home" />
    }}
    renderTab={({ isActive }) => (
    <Text style={{ color: isActive ? blue : null }}>Home</Text>
    )}
    />
    ))}
    <MatchTab
    pathname="/notifications"
    renderContent={props => (
    <View>
    <Text style={{ fontSize: 30 }}>Notifications</Text>
    </View>
    )}
    renderTab={({ isActive }) => (
    <Text style={{ color: isActive ? blue : null }}>
    Notifications
    </Text>
    )}
    />
    <MatchTab
    pathname="/messages"
    renderContent={props => <RecursiveItem rootPath="/messages" />}
    renderTab={({ isActive }) => (
    <Text style={{ color: isActive ? blue : null }}>Messages</Text>
    )}
    />
    </MatchTabs>
    96 changes: 96 additions & 0 deletions MatchTabs.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,96 @@
    class MatchTabs extends React.Component {
    render() {
    const { location } = this.props
    return (
    <View style={{ flex: 1 }}>
    <View style={{ flex: 1 }}>{this.props.children}</View>
    <View
    style={{
    flexDirection: 'row',
    alignItems: 'center',
    borderTopWidth: 1,
    borderTopColor: '#ddd'
    }}
    >
    {React.Children.map(this.props.children, child => (
    <Link
    to={child.props.pathname}
    component={TouchableOpacity}
    style={{ flex: 1, padding: 20 }}
    >
    {child.props.renderTab({
    isActive: location.pathname === child.props.pathname
    })}
    </Link>
    ))}
    </View>
    </View>
    )
    }
    }

    class MatchTab extends React.Component {
    render() {
    const { renderContent, pathname } = this.props
    return (
    <Route
    path={pathname}
    render={props => renderContent({ ...this.props, ...props })}
    />
    )
    }
    }

    const stuff = [
    { path: 'one', label: 'One' },
    { path: 'two', label: 'Two' },
    { path: 'three', label: 'Three' },
    { path: 'four', label: 'Four' }
    ]

    const blue = 'hsl(200, 50%, 50%)'

    class RecursiveItem extends Component {
    render() {
    const { pathname, rootPath, match } = this.props
    const pattern = rootPath ? rootPath : `${match.path}/:id`

    return (
    <StackMatch
    isRoot={!!rootPath}
    pattern={pattern}
    renderTitle={({ match }) => (
    <Text
    style={{ textAlign: 'center' }}
    ellipsizeMode="middle"
    numberOfLines={1}
    >
    {match.url}
    </Text>
    )}
    renderContent={({ location }) => (
    <ScrollView style={{ flex: 1, backgroundColor: 'white' }}>
    {stuff.map(thing => (
    <View
    key={thing.path}
    style={{ borderBottomWidth: 1, borderColor: '#ddd' }}
    >
    <Link
    component={TouchableOpacity}
    to={`${location.pathname}/${thing.path}`}
    underlayColor="#f0f0f0"
    >
    <Text style={{ padding: 15 }}>{thing.label}</Text>
    </Link>
    </View>
    ))}
    </ScrollView>
    )}
    renderChild={props => <RecursiveItem {...props} />}
    />
    )
    }
    }



    367 changes: 367 additions & 0 deletions StackMatch.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,367 @@
    import React, { Component } from 'react'
    import PropTypes from 'prop-types'
    import {
    View,
    Text,
    Animated,
    Dimensions,
    TouchableOpacity
    } from 'react-native'
    import { Route, Redirect } from 'react-router'
    import { Link } from 'react-router-native'

    const rootStoredLocations = {}

    class Stack extends Component {
    state = { previousProps: null, currentProps: null }

    animation = new Animated.Value(0)

    static getDerivedStateFromProps = (props, state) => {
    if (!state.currentProps) {
    return { currentProps: props }
    }

    const isLocationChanged = props.location !== state.currentProps.location

    if (isLocationChanged) {
    return {
    previousProps: state.currentProps,
    currentProps: props
    }
    }

    return null
    }
    componentDidUpdate(prevProps, prevState) {
    const previousProps = prevState.previousProps

    if (previousProps) {
    const { animation } = this
    animation.setValue(0)
    Animated.timing(animation, {
    toValue: 1,
    duration: 300
    }).start(({ finished }) => {
    this.setState({ previousProps: null })
    })
    }
    }

    render() {
    const { width, height } = Dimensions.get('window')
    const { direction } = this.props

    const animating = this.state.previousProps
    const bothProps = [this.props]
    if (animating) {
    bothProps.push(this.state.previousProps)
    }

    return (
    <View pointerEvents={animating ? 'none' : 'auto'} style={{ flex: 1 }}>
    <View
    style={{
    zIndex: 1,
    backgroundColor: '#f0f0f0',
    borderBottomColor: '#ccc',
    borderBottomWidth: 1,
    height: 40,
    alignItems: 'center'
    }}
    >
    {bothProps.map((props, index, arr) => (
    <Animated.View
    key={props.location.pathname}
    style={{
    opacity: this.animation.interpolate({
    inputRange: [0, 1],
    outputRange:
    arr.length > 1 && index === 0
    ? [0, 1]
    : index === 1 ? [1, 0] : [1, 1]
    }),
    flexDirection: 'row',
    alignItems: 'center',
    position: 'absolute',
    top: 0,
    left: 0,
    right: 0,
    bottom: 0
    }}
    >
    <View style={{ width: 30 }}>
    {props.parentLocation ? props.backButton : <Text>&nbsp;</Text>}
    </View>
    <View style={{ flex: 1 }}>{props.title}</View>
    <View style={{ width: 30 }} />
    </Animated.View>
    ))}
    </View>
    <View style={{ flex: 1, backgroundColor: '#ccc' }}>
    {bothProps.map((props, index, arr) => (
    <Animated.View
    key={props.location.pathname}
    style={{
    left: this.animation.interpolate({
    inputRange: [0, 1],
    outputRange:
    arr.length > 1
    ? index === 0 && direction === 'down'
    ? [width + 10, 0]
    : index === 1 && direction === 'down'
    ? [0, -100]
    : index === 0 && direction === 'up'
    ? [-100, 0]
    : index === 1 && direction === 'up'
    ? [0, width + 10]
    : [0, 0]
    : [0, 0]
    }),
    zIndex:
    arr.length > 1
    ? index === 0 && direction === 'down'
    ? 1
    : index === 1 && direction === 'down'
    ? 0
    : index === 0 && direction === 'up'
    ? 0
    : index === 1 && direction === 'up' ? 1 : 1
    : 1,
    position: 'absolute',
    width,
    height,
    top: 0,
    shadowColor: '#000000',
    shadowOpacity: 0.25,
    shadowRadius: 10,
    opacity: this.animation.interpolate({
    inputRange: [0, 1],
    outputRange:
    arr.length > 1
    ? index === 0 && direction === 'down'
    ? [1, 1]
    : index === 1 && direction === 'down'
    ? [1, 0.5]
    : index === 0 && direction === 'up'
    ? [0.5, 1]
    : index === 1 && direction === 'up'
    ? [1, 1]
    : [1, 1]
    : [1, 1]
    })
    }}
    >
    {props.content}
    </Animated.View>
    ))}
    </View>
    </View>
    )
    }
    }

    Stack.propTypes = {
    title: PropTypes.any,
    content: PropTypes.any,
    backButton: PropTypes.any,
    parentLocation: PropTypes.any,
    location: PropTypes.any
    }

    const StackContext = React.createContext('stackContext')

    class StackRootContainer extends Component {
    state = {
    title: null,
    content: null,
    parentLocation: null,
    backButton: null,
    direction: null
    }

    getChildContext() {
    return {
    stack: {
    push: ({ direction, title, content, parentLocation }) => {
    this.setState({
    direction,
    title,
    content,
    parentLocation,
    backButton: (
    <Link
    replace={true}
    component={TouchableOpacity}
    to={parentLocation}
    >
    <Text style={{ padding: 10 }}>&lt;</Text>
    </Link>
    )
    })
    }
    }
    }
    }

    componentWillUnmount() {
    rootStoredLocations[this.props.pattern] = this.props.location
    }

    render() {
    const { title, content, backButton, parentLocation, direction } = this.state
    const { children, location } = this.props

    return (
    <View style={{ flex: 1 }}>
    <Stack
    title={title}
    content={content}
    backButton={backButton}
    parentLocation={parentLocation}
    direction={direction}
    location={location}
    />
    {children}
    </View>
    )
    }
    }

    StackRootContainer.childContextTypes = {
    stack: PropTypes.any
    }

    StackRootContainer.propTypes = {
    children: PropTypes.node,
    location: PropTypes.object
    }

    class StackContainer extends Component {
    getChildContext() {
    return {
    stack: {
    ...this.context.stack,
    parentLocation: this.initialLocation
    }
    }
    }

    componentDidMount() {
    this.initialLocation = {
    ...this.props.location,
    pathname: this.props.location.pathname
    }

    this.pushToStack('down')
    }

    componentDidUpdate(prevProps) {
    const becameActive =
    this.props.isExact === true && prevProps.isExact === false

    if (becameActive) {
    this.pushToStack('up')
    }
    }

    pushToStack(direction) {
    const {
    isExact,
    renderTitle,
    renderContent,
    renderChild,
    ...rest
    } = this.props

    if (isExact) {
    this.context.stack.push({
    title: renderTitle(rest),
    content: renderContent(rest),
    parentLocation: this.context.stack.parentLocation,
    direction
    })
    }
    }

    render() {
    const {
    isExact,
    renderTitle,
    renderContent,
    renderChild,
    ...rest
    } = this.props

    return isExact ? null : renderChild ? renderChild(rest) : null
    }
    }

    StackContainer.contextTypes = {
    stack: PropTypes.any
    }

    StackContainer.childContextTypes = {
    stack: PropTypes.any
    }

    class RedirectStack extends Component {
    componentDidMount() {
    delete rootStoredLocations[this.props.pattern]
    }

    render() {
    return <Redirect to={this.props.to} />
    }
    }

    class StackMatch extends Component {
    render() {
    const { isRoot, pattern, ...rest } = this.props

    return (
    <Route
    path={pattern}
    render={props =>
    isRoot ? (
    rootStoredLocations[pattern] ? (
    <RedirectStack
    pattern={pattern}
    to={rootStoredLocations[pattern]}
    />
    ) : (
    <StackRootContainer pattern={pattern} location={props.location}>
    <StackContainer
    {...rest}
    {...props}
    isExact={props.match.isExact}
    />
    </StackRootContainer>
    )
    ) : (
    <StackContainer
    {...rest}
    {...props}
    isExact={props.match.isExact}
    />
    )
    }
    />
    )
    }
    }

    StackMatch.propTypes = {
    pattern: PropTypes.string.isRequired,
    renderTitle: PropTypes.any,
    renderContent: PropTypes.any,
    renderChild: PropTypes.any
    }

    export class StackScene extends Component {
    render() {
    return this.props.children
    }
    }

    export default StackMatch