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.
StackMatch.js
<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>
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} />}
/>
)
}
}
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment