Gaurav Arora
Gaurav Arora

Reputation: 17264

React Native: Toggle drawer & navigation bar visibility in routes

I've an app that needs Drawer & Navigator. To achieve this, I've a Drawer component as parent in index.ios.js which encapsulates everything else.

Now the use case that I'm trying to solve is, I've to disable Drawer or hide NavigationBar in some routes. To achieve that, I'm updating parent component's state & re-rendering on every navigation. However, that doesn't seem to help.

index.io.js :

class MyApp extends Component{

  state = {drawerEnabled:true, navigationBarEnabled: true};

  constructor(){
    super();
    this.navigate = this.navigate.bind(this);
    this.navigateForDrawer = this.navigateForDrawer.bind(this);
    this.getNavigator = this.getNavigator.bind(this);
    this.renderScene = this.renderScene.bind(this);
  }

  navigateForDrawer(route) {
      this.refs.navigator.resetTo(route);
      this.refs.drawer.close();
  }

  getNavigationBar(){
    return (
      this.state.navigationBarEnabled ?
      <Navigator.NavigationBar
      style={styles.navBar}
      transitionStyles={Navigator.NavigationBar.TransitionStylesIOS}
      routeMapper={NavigationBarRouteMapper}
      /> : null);
  }

  getNavigator(){
    return (
      <Navigator
        ref="navigator"
        style={styles.navigator}
        initialRoute={{id:'Login'}}
        renderScene={this.renderScene}
        navigationBar={this.getNavigationBar()}
      />
    );
  }

  navigate(route, method){
    if(route)
    switch (route.id) {
        case 'Login':
        this.state = {drawerEnabled: false, navigationBarEnabled: false};
        break;

        case 'NewBooking':
        this.state = {drawerEnabled: true, navigationBarEnabled: true};
        break;

        case 'MyBookings':
        this.state = {drawerEnabled: true, navigationBarEnabled: true};
        break;

        case 'AboutUs':
        this.state = {drawerEnabled: true, navigationBarEnabled: true};
        break;

        case 'ConfirmBooking':
        this.state = {drawerEnabled: false, navigationBarEnabled: true};
        break;

        case 'BookingComplete':
        this.state = {drawerEnabled: false, navigationBarEnabled: true};
        break;
    }

      this.forceUpdate();
      this.refs.navigator[method](route);
  }

  renderScene(route, navigator){
    navigator.navigate = this.navigate;
    switch (route.id) {
        case 'Login':
        return <Login navigate={this.navigate}/>;

        case 'NewBooking':
        route.title = 'New Booking';
        return <NewBooking navigate={this.navigate}/>;

        case 'MyBookings':
        route.title = 'My Bookings';
        return <MyBookings navigate={this.navigate}/>;

        case 'AboutUs':
        route.title = 'About Us';
        return <AboutUs navigate={this.navigate}/>;

        case 'ConfirmBooking':
        route.title = 'Booking Summary';
        return <ConfirmBooking navigate={this.navigate} booking={route.booking}/>;

        case 'BookingComplete':
        route.title = 'Booking Complete';
        return <BookingComplete navigate={this.navigate} booking={route.booking}/>;

      default:
    }
  }

  render() {
    return (
      <Drawer
        ref="drawer"
        disabled={!this.state.drawerEnabled}
        content={<ControlPanel navigate={this.navigateForDrawer}/>}
        tapToClose={true}
        openDrawerOffset={0.2} // 20% gap on the right side of drawer
        panCloseMask={0.2}
        closedDrawerOffset={-3}
        styles={{
          main: {paddingLeft: 3}
        }}
        tweenHandler={(ratio) => ({
          main: { opacity:(2-ratio)/2 }
        })}
        >
        {this.getNavigator()}
      </Drawer>
    )
  }

}

To Navigate to a new route :

this.props.navigate({id: 'ConfirmBooking', booking: booking}, 'push');

PS: The drawer that I'm using is react-native-drawer

Upvotes: 3

Views: 6376

Answers (2)

andy9775
andy9775

Reputation: 364

I had to do the same thing with a navigator bar appearing on certain pages and I did the following:

First I wrote my own NavBar which is simply a screen with flex:1 and has two inner components. The first is the Navigation bar which has flex:1 and contains three inner components, left button, title and right button with flex:1, flex:2, flex:1 respectively. This way I can have a custom navigation bar on each screen.

Here's the code:

React.createClass({

   propTypes: {

   /* Specifies the background color of the navigation bar*/
   backgroundColor: React.PropTypes.string,

   /* The view to be displayed */
   view: React.PropTypes.element,

   /* A component representing the left button */
   leftButton: React.PropTypes.element,

   /* Title component that shows up in the middle of the screen */
   title: React.PropTypes.element,

   /* Right button component */
   rightButton: React.PropTypes.element,

   /* including a modal ensures that the modal is centered in the screen */
   modal: React.PropTypes.element,

   /* Callback that is called from the Navigator bar screen. Any arguments
    * passed, represent the positioning and dimensions of the Navigation Bar
    */
   onLayout: React.PropTypes.func,

   /*The separator that shows up between the Navigation Bar and the view*/
   separator: React.PropTypes.element,
   },

   render(){
     return (
       <View style={styles.modalWrapper}>
         <View
           style={styles.container}>
              <View
             style={[styles.navBar, {backgroundColor: this.props.backgroundColor}]}
             onLayout={(e) =>{
             this.props.onLayout && this.props.onLayout(e)
             }}>
          <View
            style={styles.leftButton}>
            {this.props.leftButton}
          </View>
          <View
            style={styles.title}>
            {this.props.title }
          </View>
          <View
            style={styles.rightButton}>
            {this.props.rightButton}
          </View>
        </View>

        <View style={styles.viewWrapper}>
          {this.props.separator }
          {this.props.view}
        </View>
    </View>
    {this.props.modal}
  </View>);
  },
});

And the styles:

StyleSheet.create({
  modalWrapper: {
    width: device.width,
    height: device.height
  },
  container: {
    flex: 1,
    flexDirection: 'column',
  },
  navBar: {
    flex: 1,
    flexDirection: 'row',
    alignItems: 'center'
  },
  viewWrapper: {
    flex: 8
  },
  rightButton: {
    top: STATUS_BAR_OFFSET,
    flex: 1,
    alignItems: 'flex-end',
    justifyContent: 'center',
    paddingRight: PADDING,
  },
  leftButton: {
    top: STATUS_BAR_OFFSET,
    flex: 1,
    alignItems: 'center',
    justifyContent: 'flex-start',
    paddingLeft: PADDING,
  },
  title: {
    top: STATUS_BAR_OFFSET,
    flex: 2,
    alignItems: 'center',
    justifyContent: 'center',
    paddingRight: PADDING / 2,
    paddingLeft: PADDING / 2,
  }
});

onStartShouldSetResponderCapture

Next, while I haven't had a change to try this yet, I would suggest usint onStartShouldSetResponderCapture. If you try and run the following example you'll see the order that nested views capture touch events. Under react native onStartShouldSetResponder starts at the lowest child view and the the touch events bubble up to the top if no child responds. However, onStartShouldSetResponderCapture is called first on the outer most view.

React.createClass({
  render() {
    return (
      <View onStartShouldSetResponderCapture={() => this.text('1')}
            onStartShouldSetResponder={() => this.text('4')}
            style={{flex:1}}>
        <View onStartShouldSetResponderCapture={() => this.text('2')}
              onStartShouldSetResponder={() => this.text('3')}
              style={{flex:1}}>
        </View>
      </View>
    );


  },

  text(text) {
    console.log(text);
    //return true; // comment this to see the order or stop at '1'
  }
});

so in your case I would do the following:

render() {
        return (
          <View onStartShouldSetResponderCapture={() => !this.state.drawerEnabled}>
            <Drawer
              ref="drawer"
              disabled={!this.state.drawerEnabled}
              content={<ControlPanel navigate={this.navigateForDrawer}/>}
              tapToClose={true}
              openDrawerOffset={0.2} // 20% gap on the right side of drawer
              panCloseMask={0.2}
              closedDrawerOffset={-3}
              styles={{
          main: {paddingLeft: 3}
        }}
              tweenHandler={(ratio) => ({
          main: { opacity:(2-ratio)/2 }
        })}
            >
              {this.getNavigator()}
            </Drawer>
          </View>
        );
      }

setState

Finally, rather than calling forceUpdate() I would call this.setState({drawerEnabled: true}). Also, don't forget to bind in your constructor.

Upvotes: 1

Daniel Schmidt
Daniel Schmidt

Reputation: 11921

I assume that you mean DrawerLayoutAndroid (or DrawerLayout) as drawer component.

You could simply use the following line in your code to programmatically open or close the drawer on route change:

this.refs.drawer.openDrawer(); // opens the drawer
this.refs.drawer.closeDrawer(); // closes the drawer

I would include these calls in your navigate function, this seems to be the best place.

Upvotes: 3

Related Questions