-
Notifications
You must be signed in to change notification settings - Fork 24.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Navigator] Binding the navigation bar with the underlying scene #2615
Comments
For example, here's a My code now looks like: class Application extends Component {
render() {
const navigationBar = (
<Navigator.NavigationBar routeMapper={{
RightButton(route, navigator) {
return (
<NavButton
text="Login"
onPress={ () => this.refs.loginScene.submit() }
/>
);
}
}} />
)
return (
<Navigator
renderScene={ () => <LoginForm ref="loginScene" /> }
navigationBar={ navigationBar }
/>
);
}
} |
It would be difficult to put contents of the nav bar within the scene, as they may have different lifespans. You could try this, where the route owns the button press events: class LoginRoute {
constructor() {
this.eventEmitter = new EventEmitter();
}
renderRightButton(navigator) {
return (
<NavButton
text="Login"
onPress={ () => this.eventEmitter.emit('loginPress') }
/>
);
}
renderScene(navigator) {
return (
<LoginScene
routeEvents={this.eventEmitter}
navigator={navigator}
/>
);
}
}
class Application extends Component {
static navBarRouteMapper = {
RightButton(route, navigator) {
return route.renderRightButton(navigator);
}
}
render() {
return (
<Navigator
initialRoute={ new LoginRoute() }
renderScene={ (route, navigator) => route.renderScene(navigator) }
navigationBar={
<Navigator.NavigationBar
routeMapper={Application.navBarRouteMapper}
/>
}
/>
);
}
} |
Thanks @ericvicenti for the idea, this pattern works much better to split the routes from the navigator container 👍 However, it doesn't help so much to control the navigation bar according to the scene state. Take as example the iOS keyboard's settings: it seems to update the scene state (applying an animation), while changing the navigation bar as well: The workaround I'd follow is to push the same route with an attribute which say the scene to "animate" its content once it is mounted. The navigation bar animation doesn't play as good as the native one, but I guess I can try to provide a custom one in the |
I agree that it looks like the state is stored in the scene, but it would actually be stored in the route because the scene is an isolated component. You're right that the Navigator.NavigationBar isn't nearly as complete as the native one. We could extend it with the Animated API and start giving it more of these features. |
@gpbl: I handle this by defining a generic navigationBarRouteMapper that takes attributes out of the route to render the navigation bar. Then, each scene is able to customize its navigation bar in So, for instance, I have a scene that configures its navigation bar like this (in this.props.route.title = "Scene Title";
this.props.route.leftButtonText = "Cancel";
this.props.route.onPressLeftButton = function() {
this.props.navigator.pop();
}.bind(this);
this.props.route.rightButtonText = "Save";
this.props.route.onPressRightButton = this.save.bind(this); Then, the functions in the navigationBarRouteMapper look for these attributes in the route. For instance, here's Title: function(route) {
return (
<Text style={[styles.navBarText, styles.navBarTitleText]}>
{route.title}
</Text>
);
} Admittedly, it goes against the grain of React's top-down flow of data: I'm passing data up to the navigationBarRouteMapper through the route. Also, it only works because But, on the flip side, things work mostly as they did in ObjC, when we could manipulate Would be very interested in any feedback on this approach. |
if it helps, |
@jedlinlau This is interesting, but I really don't want to mutate
@ericvicenti Is this something you're waiting for the community to pick up? I've seen a Navigator example in the Animated documentation. I'd be interested in contributing here, to get better animations and to figure out the communication problem between the nav bar and the scene. I do like the route approach! |
@ehd: Makes sense. I like @MikaelCarpenter 's suggestion of passing the required callbacks down as props. |
Coming in way late on this, but we use events for communicating between the navbar and scene, simple and effective. |
If you want to go a little further than events, I encourage you to use a flux architecture, you'll then just have to trigger the correct action in your Navigator or in your content View 😃 |
This is how I solved it, though I'm not really happy with it 😕 import NavigationBar from 'react-native-navbar'
<Navigator
initialRoute={{
Component: InitialComponent,
navigationBarProps: {
title: 'First'
}
}}
renderScene={(route, navigator) => {
const {
Component,
passProps,
navigationBarProps
} = route
if (!route.NavigationBar) {
route.NavigationBar = NavigationBar
}
const props = {
...this.props,
...passProps,
// XXX: this does not feel right oO
setNavigationBarProps: props => {
route.navigationBarProps = {
...route.navigationBarProps,
...props
}
setTimeout(() => this.forceUpdate(), 0)
}
}
return (
<View style={{flex: 1}}>
<route.NavigationBar
{...navigationBarProps}
navigator={navigator}
router={route}
/>
<Component {...props} navigator={navigator} />
</View> InitialComponent: onRenderSecond = () => {
this.props.navigator.push({
Component: Second,
navigationBarProps: {
title: 'Will be overridden by the component'
}
})
} Second: componentWillMount () {
this.props.setNavigationBarProps({
title: 'Second',
nextTitle 'Save',
onNext: () => {
this.props.onSave(this.state)
this.props.navigator.pop()
}
}) |
All I want to do is hide the NavigationBar for various components, e.g. Page A doesn't need navbar, but Page B does, what's the easiest way to do that? Can someone give me a code example? |
What I do now is wrapping the NavigatorNavigationBar: import React, {
Navigator,
PropTypes,
Component
} from 'react-native'
export default class ExNavigationBar extends Component {
static propTypes = {
navState: PropTypes.object.isRequired,
navigationBarHidden: PropTypes.bool
}
// this is important, if this is omitted, the navbar will render the old route again, not the new one
updateProgress (...args) {
this.state.navigationBar && this.state.navigationBar.updateProgress(...args)
}
setNavigationBarRef = navigationBar => {
this.setState({
navigationBar
})
}
render () {
if (this.props.navState.routeStack.slice(-1)[0].navigationBarHidden === true) {
return null
} else {
return (
<Navigator.NavigationBar
ref={this.setNavigationBarRef}
{...this.props}
/>
)
}
}
} That I pass to ExNavigator: <ExNavigator
{...this.props}
renderNavigationBar={props => <ExNavigationBar {...props}/> }
configureScene={route => Navigator.SceneConfigs.FloatFromBottom}
initialRoute={{
// this is for hiding the navbar
navigationBarHidden: true,
getSceneClass() {
return require('./HomeScreen');
},
getTitle() {
return 'Home'
},
}}
/> In // ...
nextScreen = () => {
this.navigator.push({
// could be omitted, since it defaults to false
navigationBarHidden: false,
getSceneClass() {
return require('./NextSreen')
},
getTitle() {
return 'Next Screen'
}
})
}
// ... This is for illustration purposes, use a factory for the route creation. For more information about ExNavigator see the medium post. If anything is unclear, please feel free to ask. I also wrote a Route class that makes it easier to create routes, defer rendering (like ExNavigator's LoadingContainer) and automatically hooks up an event emitter (and also disposes it) so the rendered scene can change the navbar's title, buttons, etc., but at the moment it's still integrated in out app and I haven't had the time to clean it up and put it into a module. But if there is enough interest I might do that next weekend. |
What do you guys think about passing down RxObservable(with some action related with Navigation Bar) for the underlying component to subscribe to that observable? |
Using RxJs, I created RxSubjects so that bottom components of the Navigator can also receive button events by subscribing to the RxSubjects. |
I solved this whole thing with a wrapper over RN's Navigator. It is available as an NPM Package here -- https://github.com/rahuljiresal/react-native-rj-navigator |
BTW if you are using RN 0.16 you might reach a bug where elements inside navbar are not touchable. |
You can check my solution here: https://github.com/machard/react-native-advanced-navigation |
@gpbl I have a scene that configures its navigation bar like this in componentWillMount()
Then, the functions in the navigationBarRouteMapper
but got an error: this.props attempted to assign to readonly property. RN: 0.20.0 how to fix it? |
@machard it's great ! Thank you ! |
Hey guys, I've read through, checked other people's solution and wasn't really happy with with any of them. After some thinking I ended up with this:
And my navigation component contains these:
I'm new to RN, so maybe this is breaking some patterns. I'm quite curious for feedback. Z. |
My solution is instead of storing input data in scene component state, and call // function to be called by onChangeText
const updateAddItemRoute = (navigator, newProp) => {
navigator.replace(Object.assign({
id: 'addItem',
title: 'Add New Sth',
text: '',
}, newProp));
}
// renderScene
case 'addItem':
return (
<AddItem
text={route.text}
nav={nav}
updateAddItemRoute={updateAddItemRoute}
/>
);
// scene component render function
render() {
const { text, updateAddItemRoute, nav } = this.props;
return (
<View style={styles.scene}>
<Text style={styles.inputLabel}>
Name
</Text>
<TextInput
style={styles.inputBox}
onChangeText={(text) => updateAddItemRoute(nav, {text})}
value={text}
autoFocus={true}
placeholder={'Enter Task Name'}
returnKeyType={'done'}
/>
</View>
)
} Still working on better solution :) |
@pallzoltan This is almost perfect. Now i just need to call methods and setState from within the component. My problem is that its a static so i don't have the correct this context. Does someone know how i can pass the current instances of the scene to the navigationBarMapper methods? My app.js static mappedRoutes = (route)=> {
const routeMap = new Map([
[Constants.Routes.CONTACT, Contact],
[Constants.Routes.DASHBOARD, Dashboard],
[Constants.Routes.DISCLAIMER, Disclaimer]
]);
if (routeMap.has(route.name)) {
return routeMap.get(route.name);
} else {
return null;
}
};
getNavigatorItem(functionName, route, navigator, index, navState) {
// i need a way to call the 'leftButton' function from the scene instance and not the static leftButton function. How can i access the current rendered scene instance?
const Route = App.mappedRoutes(route);
if (Route && Route[functionName]) {
return Route[functionName](route, navigator, index, navState);
}
return null;
}
renderScene(route, navigator) {
const Route = App.mappedRoutes(route);
if (Route) {
const selector = Route.selector ? Route.selector : ()=> {
return {}
};
const connectedRoute = connect(selector)(Route);
return React.createElement(connectedRoute, {...route.passProps, navigator: navigator});
}
return <Text>404</Text>;
}
renderNavigationBar() {
if (this.state.navigationBarHidden) {
return <View/>;
}
const navigationBarStyle = {
backgroundColor: this.state.navigationBarColor
};
return (
<Navigator.NavigationBar
routeMapper={this.createRouteMapper()}
style={[Style.navigationBar, navigationBarStyle]}
/>
);
}
mapTitleToRoute(route, navigator, index, navState) {
return (
<TextTitlebar title={route.title ? route.title.toUpperCase() : ''}
textStyle={[Style.titleBarText, route.titleTextStyle]}
containerStyle={[route.titleStyle]}
/>
);
}
mapLeftButtonToRoute(route, navigator, index, navState) {
if (index > 0) {
return <BackButton onPress={this.onPressBackButton}/>;
} else if (index === 0) {
return <MenuButton onPress={this.onPressMenuButton}/>;
}
}
mapRightButtonToRoute(route, navigator, index, navState) {
const navigatorItem = this.getNavigatorItem('rightButton', route, navigator, index, navState);
if (navigatorItem) {
return navigatorItem;
}
switch (route.name) {
case Routes.SHOPPING:
return <MenuButton onPress={this.onPressMenuButton}/>;
default:
return <View/>;
}
}
createRouteMapper() {
return {
LeftButton : this.mapLeftButtonToRoute,
RightButton: this.mapRightButtonToRoute,
Title : this.mapTitleToRoute,
}
} My dashboard component class Dashboard extends Component {
constructor(props) {
super(props);
}
render() {
return (
<View style={Style.container}>
</View>
)
}
static leftButton(){
return <Text>LEFT BUTTON</Text>
}
static rightButton(){
// i want here something like this.setState({foo: 'bar'})
return <Text>RIGHT BUTTON</Text>
}
static selector(state){
return {
User: state.User
}
}
}
export default Dashboard; |
Closing this out because we aren't changing the API of Navigator any more. @maluramichael, you could use a flux library to subscribe your header components to changing data and allow your inner scene to communicate with the header. |
In iOS, we can change title and buttons of a navigation bar from the view controller, e.g. via the
navigationItem
property. This is useful, for example, to let the view controller itself handle the right/left buttons' event.In React Native, the Navigator's
navigationBar
is rendered in the container (where<Navigator>
is placed) and decoupled from the underlying scene. Buttons event handlers must be defined in the container, where we can only have access to a scene with theref
prop. More scenes there are, more the container becomes complicated. It get even more complex when the buttons handlers depend from the scene's state.I'd prefer instead to define the navigation bar's buttons (and title) inside the scene itself, e.g. by rendering
Navigator.NavigationBar
as child of the scene component – but I couldn't get it working.I wonder then what is the best approach: am I missing the sense of NavigationBar, since it seems designed just to pop/push routes? As alternative I could adopt a special flux store to help the communication between scene and the NavigatorBar's
routeMapper
, but it seems overly complicated for a common UI element like the Navigator.The text was updated successfully, but these errors were encountered: