[Navigator] Binding the navigation bar with the underlying scene #2615

Closed
gpbl opened this Issue Sep 9, 2015 · 25 comments

Comments

Projects
None yet
@gpbl
Contributor

gpbl commented Sep 9, 2015

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 the ref 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.

@gpbl

This comment has been minimized.

Show comment
Hide comment
@gpbl

gpbl Sep 9, 2015

Contributor

For example, here's a <LoginForm> with a submit() function, and an action button to start the login process from the navigation bar:

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 }
      />
    );
  }

}
Contributor

gpbl commented Sep 9, 2015

For example, here's a <LoginForm> with a submit() function, and an action button to start the login process from the navigation bar:

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 }
      />
    );
  }

}
@brentvatne

This comment has been minimized.

Show comment
Hide comment
Collaborator

brentvatne commented Sep 9, 2015

@gpbl gpbl changed the title from [Navigator] How to bind the navigation bar with the underlying scene to [Navigator] Binding the navigation bar with the underlying scene Sep 9, 2015

@ericvicenti

This comment has been minimized.

Show comment
Hide comment
@ericvicenti

ericvicenti Sep 9, 2015

Contributor

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}
          />
        }
      />
    );
  }
}
Contributor

ericvicenti commented Sep 9, 2015

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}
          />
        }
      />
    );
  }
}
@gpbl

This comment has been minimized.

Show comment
Hide comment
@gpbl

gpbl Sep 10, 2015

Contributor

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:

qtxyd

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 configureScene.

Contributor

gpbl commented Sep 10, 2015

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:

qtxyd

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 configureScene.

@ericvicenti

This comment has been minimized.

Show comment
Hide comment
@ericvicenti

ericvicenti Sep 10, 2015

Contributor

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.

Contributor

ericvicenti commented Sep 10, 2015

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.

@jedlau

This comment has been minimized.

Show comment
Hide comment
@jedlau

jedlau Sep 11, 2015

Contributor

@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 componentWillMount(), which gets called (thankfully) before the navigationBarRouteMapper does its work.

So, for instance, I have a scene that configures its navigation bar like this (in componentWillMount()):

    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():

    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 componentWillMount() gets called before the navigationBarRouteMapper functions.

But, on the flip side, things work mostly as they did in ObjC, when we could manipulate self.navigationController.navigationItem in viewDidLoad(). (I'm currently looking for a way to hide the navigation bar on a per-scene basis, which I don't seem to be able to do at the moment.)

Would be very interested in any feedback on this approach.

Contributor

jedlau commented Sep 11, 2015

@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 componentWillMount(), which gets called (thankfully) before the navigationBarRouteMapper does its work.

So, for instance, I have a scene that configures its navigation bar like this (in componentWillMount()):

    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():

    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 componentWillMount() gets called before the navigationBarRouteMapper functions.

But, on the flip side, things work mostly as they did in ObjC, when we could manipulate self.navigationController.navigationItem in viewDidLoad(). (I'm currently looking for a way to hide the navigation bar on a per-scene basis, which I don't seem to be able to do at the moment.)

Would be very interested in any feedback on this approach.

@MikaelCarpenter

This comment has been minimized.

Show comment
Hide comment
@MikaelCarpenter

MikaelCarpenter Sep 11, 2015

if it helps, gb-native-router lets you talk to your navBar from the scene using this.props.setRightProps and this.props.setLeftProps. You can check out index.js to see how it's done if you'd like.

if it helps, gb-native-router lets you talk to your navBar from the scene using this.props.setRightProps and this.props.setLeftProps. You can check out index.js to see how it's done if you'd like.

@ehd

This comment has been minimized.

Show comment
Hide comment
@ehd

ehd Sep 16, 2015

Would be very interested in any feedback on this approach.

@jedlinlau This is interesting, but I really don't want to mutate this.props 😄 Also props are being frozen starting react 0.14

We could extend it with the Animated API and start giving it more of these features.

@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 commented Sep 16, 2015

Would be very interested in any feedback on this approach.

@jedlinlau This is interesting, but I really don't want to mutate this.props 😄 Also props are being frozen starting react 0.14

We could extend it with the Animated API and start giving it more of these features.

@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!

@jedlau

This comment has been minimized.

Show comment
Hide comment
@jedlau

jedlau Sep 16, 2015

Contributor

@ehd: Makes sense. I like @MikaelCarpenter 's suggestion of passing the required callbacks down as props.

Contributor

jedlau commented Sep 16, 2015

@ehd: Makes sense. I like @MikaelCarpenter 's suggestion of passing the required callbacks down as props.

@dhrrgn

This comment has been minimized.

Show comment
Hide comment
@dhrrgn

dhrrgn Sep 19, 2015

Contributor

Coming in way late on this, but we use events for communicating between the navbar and scene, simple and effective.

Contributor

dhrrgn commented Sep 19, 2015

Coming in way late on this, but we use events for communicating between the navbar and scene, simple and effective.

@ahanriat

This comment has been minimized.

Show comment
Hide comment
@ahanriat

ahanriat Sep 22, 2015

Contributor

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 😃

Contributor

ahanriat commented Sep 22, 2015

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 😃

@despairblue

This comment has been minimized.

Show comment
Hide comment
@despairblue

despairblue Oct 17, 2015

Contributor

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()
    }
  })
Contributor

despairblue commented Oct 17, 2015

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()
    }
  })
@niftylettuce

This comment has been minimized.

Show comment
Hide comment
@niftylettuce

niftylettuce Nov 2, 2015

Contributor

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?

Contributor

niftylettuce commented Nov 2, 2015

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?

@despairblue

This comment has been minimized.

Show comment
Hide comment
@despairblue

despairblue Nov 2, 2015

Contributor

@niftylettuce

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 HomeScreen you can then push a route that does not hide the navbar:

// ...
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.

Contributor

despairblue commented Nov 2, 2015

@niftylettuce

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 HomeScreen you can then push a route that does not hide the navbar:

// ...
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.

@jihopark

This comment has been minimized.

Show comment
Hide comment
@jihopark

jihopark Nov 5, 2015

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?

jihopark commented Nov 5, 2015

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?

@jihopark

This comment has been minimized.

Show comment
Hide comment
@jihopark

jihopark Nov 6, 2015

var PlainNavigator = React.createClass({
  ...
  _navBarRouter: {
    Title: (route, navigator, index, navState) => {
     ...
    },
    LeftButton: (route, navigator, index, navState) => {
     ...
     return (<NavigationTextButton
                    buttonText={routes.getCurrentRoute().leftButtonText}
                    onPress={() => navigator.props.leftButtonSubject.onNext(routes)} />);
       ...
    },
    RightButton: (route, navigator, index, navState) => {
      ...
          return (<NavigationTextButton
                    buttonText={routes.getCurrentRoute().rightButtonText}
                    onPress={() => navigator.props.rightButtonSubject.onNext(routes)}/>);
       ...
    }
  },
  _renderScene: function(route, navigator) {
    ...
    <Screen
          //subscribe to these subjects if need to receive left,right button events
          leftButtonSubject={this._leftButtonSubject}
          rightButtonSubject={this._rightButtonSubject}
          routes={routes}
          navigator={navigator}
          api_domain={this.props.api_domain} />
      );
    ...
  },
  _leftButtonSubject: new Rx.Subject(),
  _rightButtonSubject: new Rx.Subject(),
  render: function() {
    return (
      <Navigator
        leftButtonSubject={this._leftButtonSubject}
        rightButtonSubject={this._rightButtonSubject}
        initialRouteStack={this.getInitialRouteStack(this.props.uri)}
        renderScene={this._renderScene}
        navigationBar={
          <Navigator.NavigationBar
            routeMapper={this._navBarRouter}/>
        }
      />
    );
  }
});

Using RxJs, I created RxSubjects so that bottom components of the Navigator can also receive button events by subscribing to the RxSubjects.

jihopark commented Nov 6, 2015

var PlainNavigator = React.createClass({
  ...
  _navBarRouter: {
    Title: (route, navigator, index, navState) => {
     ...
    },
    LeftButton: (route, navigator, index, navState) => {
     ...
     return (<NavigationTextButton
                    buttonText={routes.getCurrentRoute().leftButtonText}
                    onPress={() => navigator.props.leftButtonSubject.onNext(routes)} />);
       ...
    },
    RightButton: (route, navigator, index, navState) => {
      ...
          return (<NavigationTextButton
                    buttonText={routes.getCurrentRoute().rightButtonText}
                    onPress={() => navigator.props.rightButtonSubject.onNext(routes)}/>);
       ...
    }
  },
  _renderScene: function(route, navigator) {
    ...
    <Screen
          //subscribe to these subjects if need to receive left,right button events
          leftButtonSubject={this._leftButtonSubject}
          rightButtonSubject={this._rightButtonSubject}
          routes={routes}
          navigator={navigator}
          api_domain={this.props.api_domain} />
      );
    ...
  },
  _leftButtonSubject: new Rx.Subject(),
  _rightButtonSubject: new Rx.Subject(),
  render: function() {
    return (
      <Navigator
        leftButtonSubject={this._leftButtonSubject}
        rightButtonSubject={this._rightButtonSubject}
        initialRouteStack={this.getInitialRouteStack(this.props.uri)}
        renderScene={this._renderScene}
        navigationBar={
          <Navigator.NavigationBar
            routeMapper={this._navBarRouter}/>
        }
      />
    );
  }
});

Using RxJs, I created RxSubjects so that bottom components of the Navigator can also receive button events by subscribing to the RxSubjects.

@rahuljiresal

This comment has been minimized.

Show comment
Hide comment
@rahuljiresal

rahuljiresal Nov 13, 2015

Contributor

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

Contributor

rahuljiresal commented Nov 13, 2015

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

@gre

This comment has been minimized.

Show comment
Hide comment
@gre

gre Dec 15, 2015

Contributor

BTW if you are using RN 0.16 you might reach a bug where elements inside navbar are not touchable.
I've fixed it in #4786

Contributor

gre commented Dec 15, 2015

BTW if you are using RN 0.16 you might reach a bug where elements inside navbar are not touchable.
I've fixed it in #4786

@machard

This comment has been minimized.

Show comment
Hide comment
@jsntghf

This comment has been minimized.

Show comment
Hide comment
@jsntghf

jsntghf Feb 26, 2016

@gpbl I have a scene that configures its navigation bar like this in componentWillMount()

this.props.route.title = "Scene Title";

Then, the functions in the navigationBarRouteMapper

Title: function(route) { return ( <Text style={[styles.navBarText, styles.navBarTitleText]}> {route.title} </Text> ); }

but got an error: this.props attempted to assign to readonly property.

RN: 0.20.0

how to fix it?

jsntghf commented Feb 26, 2016

@gpbl I have a scene that configures its navigation bar like this in componentWillMount()

this.props.route.title = "Scene Title";

Then, the functions in the navigationBarRouteMapper

Title: function(route) { return ( <Text style={[styles.navBarText, styles.navBarTitleText]}> {route.title} </Text> ); }

but got an error: this.props attempted to assign to readonly property.

RN: 0.20.0

how to fix it?

@jondot jondot referenced this issue in ericvicenti/navigation-rfc Apr 19, 2016

Open

Navbar and child component communication #70

@oldashes

This comment has been minimized.

Show comment
Hide comment
@oldashes

oldashes Jun 1, 2016

@machard it's great ! Thank you !

oldashes commented Jun 1, 2016

@machard it's great ! Thank you !

@pallzoltan

This comment has been minimized.

Show comment
Hide comment
@pallzoltan

pallzoltan Jul 14, 2016

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:

var RegistrationPage = React.createClass({

    render: function() {
        return <Text>Done</Text>
    },

    statics: {
        leftButtonMapper: function(route, navigator, index, navState) {
            return <NavigationBackButton onPress={() =>
                navigator.pop()
            } />
        },
    },
})

And my navigation component contains these:

var AuthProcess = React.createClass({

    render: function() {
        return (
            <Navigator
                initialRoute={{page: 'login'}}
                renderScene={this.renderScene}
                navigationBar={this.navigationBar()}
            />
        )
    },

    renderScene: function(route, navigator) {
        // ...
    }

    navigationBar: function() {
        return <Navigator.NavigationBar
            routeMapper={{
                LeftButton: this.leftButtonMapper,
                RightButton: this.rightButtonMapper,
                Title: this.titleMapper
            }}
        />
    },

    leftButtonMapper: function(route, navigator, index, navState) {
        return this.navigatorItem('leftButtonMapper', route, navigator, index, navState);
    },

    rightButtonMapper: function(route, navigator, index, navState) {
        return this.navigatorItem('rightButtonMapper', route, navigator, index, navState);
    },

    titleMapper: function(route, navigator, index, navState) {
        return this.navigatorItem('titleMapper', route, navigator, index, navState);
    },

    navigatorItem: function(functionName, route, navigator, index, navState) {
        let classDefinition;
        classDefinition = this.classForRoute(route);

        if(classDefinition && classDefinition[functionName]) {
            return classDefinition[functionName](route, navigator, index, navState);
        } else {
            return null;
        }
    },

    classForRoute: function(route) {
        switch(route['page']) {
            case 'login':
                return LoginPage;
            case 'signUpWithEmail':
                return RegistrationPage;
            default:
                return null;

        }
    }
})

I'm new to RN, so maybe this is breaking some patterns. I'm quite curious for feedback.

Z.

pallzoltan commented Jul 14, 2016

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:

var RegistrationPage = React.createClass({

    render: function() {
        return <Text>Done</Text>
    },

    statics: {
        leftButtonMapper: function(route, navigator, index, navState) {
            return <NavigationBackButton onPress={() =>
                navigator.pop()
            } />
        },
    },
})

And my navigation component contains these:

var AuthProcess = React.createClass({

    render: function() {
        return (
            <Navigator
                initialRoute={{page: 'login'}}
                renderScene={this.renderScene}
                navigationBar={this.navigationBar()}
            />
        )
    },

    renderScene: function(route, navigator) {
        // ...
    }

    navigationBar: function() {
        return <Navigator.NavigationBar
            routeMapper={{
                LeftButton: this.leftButtonMapper,
                RightButton: this.rightButtonMapper,
                Title: this.titleMapper
            }}
        />
    },

    leftButtonMapper: function(route, navigator, index, navState) {
        return this.navigatorItem('leftButtonMapper', route, navigator, index, navState);
    },

    rightButtonMapper: function(route, navigator, index, navState) {
        return this.navigatorItem('rightButtonMapper', route, navigator, index, navState);
    },

    titleMapper: function(route, navigator, index, navState) {
        return this.navigatorItem('titleMapper', route, navigator, index, navState);
    },

    navigatorItem: function(functionName, route, navigator, index, navState) {
        let classDefinition;
        classDefinition = this.classForRoute(route);

        if(classDefinition && classDefinition[functionName]) {
            return classDefinition[functionName](route, navigator, index, navState);
        } else {
            return null;
        }
    },

    classForRoute: function(route) {
        switch(route['page']) {
            case 'login':
                return LoginPage;
            case 'signUpWithEmail':
                return RegistrationPage;
            default:
                return null;

        }
    }
})

I'm new to RN, so maybe this is breaking some patterns. I'm quite curious for feedback.

Z.

@love4shen

This comment has been minimized.

Show comment
Hide comment
@love4shen

love4shen Jul 27, 2016

My solution is instead of storing input data in scene component state, and call setSetate in onChangeText callback function, onChangeText calls a function passed from props which updates the route object. This solution even eliminates the local state in child component, which can then be make pure. However, I do notice lag when typing. Below is some part of my code:

// 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 :)

My solution is instead of storing input data in scene component state, and call setSetate in onChangeText callback function, onChangeText calls a function passed from props which updates the route object. This solution even eliminates the local state in child component, which can then be make pure. However, I do notice lag when typing. Below is some part of my code:

// 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 :)

@maluramichael

This comment has been minimized.

Show comment
Hide comment
@maluramichael

maluramichael Aug 16, 2016

@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;

@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;
@ericvicenti

This comment has been minimized.

Show comment
Hide comment
@ericvicenti

ericvicenti Oct 24, 2016

Contributor

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.

Contributor

ericvicenti commented Oct 24, 2016

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment