Thursday, October 5, 2017

More Navigation and Drawer Navigation

With a simple navigation flow using StackNavigator behind us, our next step is to start playing with more advanced navigation. Honestly, from the little experience I got in the industry, "next advanced" level never stops. Needs are insatiable. LoL
From a real world perspective, we have 2 parts for an application, especially on mobile
  1. Pre-authentication flows
  2. Post-authentication flows
We may have modules that need to work before authentication, things like login form, Forgot password, register/signup, contact us, privacy policies, locate us etc. Of course we all know the post authentication flows. Complexity on each side is different. There may be cases where flow is staggered (like MFA flows) while others may be continuous. We can't say which one is more complicated than the other.

With that in mind, lets start to put together an simple flow. This sample will mix a lot of StackNavigation, in addition we will also see DrawerNavigation. One of the most used navigation in mobile world. In other words called "Hamburger menu" or Side navigation.


Here's a high-level view of what we will see

Stack Navigator - App
Stack Stack Navigator - Login
Login form
Forgot Password
Signup
Stack Navigator - Others
Contact us
Privacy policy
Dashboard Navigation
Stack Navigator - Home
Home
Stack Navigator - Module 1
Sub module 1
Sub module 2
Stack Navigator - Module 2
Sub module 1
Sub module 2

At one glance this flow may look simple, but it can be a handful, if we don't understand the concept of navigation from React native context. And trust me, if you go over web, you will see tons of questions around navigation in React native.

As in previous examples, none of the leaf pages are complete. They are place holders pages and contain nothing more than simple text to help us identify the page and a title to show in navigation bar. Our first step is to set the app up. Start by creating a new react-native application and here is the content for our first file

index.js

import { AppRegistry } from 'react-native';
import { StackNavigator } from 'react-navigation';
import React from 'react';
import App from './App';
import LoginNav from './src/screens/preauthentication/authentication/login';
import ForgotPassword from './src/screens/preauthentication/authentication/forgot_password';
import Signup from './src/screens/preauthentication/authentication/signup';
import Home from './src/screens/postauthentication/home';
import { PostAuthDrawer } from "./src/screens/postauthentication/home_drawer";
import ContactUs from "./src/screens/preauthentication/others/contactus";
import PrivacyPolicy from "./src/screens/preauthentication/others/privacy_policy";

const LogonStack = StackNavigator({
  Login: { screen: LoginNav },
  ForgotPassword: { screen: ForgotPassword },
  Signup: { screen: Signup },
}, {
  headerMode: 'screen',
});

const OthersStack = StackNavigator({
  ContactUs: { screen: ContactUs},
  PrivacyPolicy: { screen: PrivacyPolicy},
},{
  headerMode: 'screen',
});

const AppStack = StackNavigator({
  Login: { screen: ({ navigation }) => <LogonStack screenProps={{ rootNavigation: navigation, headerMode: 'none', }} /> },
  Others: { screen: ({ navigation }) => <OthersStack screenProps={{ rootNavigation: navigation, headerMode: 'none', }} /> },
  Home: { screen: PostAuthDrawer }
}, {
  headerMode: 'none'
});

AppRegistry.registerComponent('DrawerNavigation', () => AppStack);

Notice in the above code where in 'Login' and 'Others' item in 'AppStack' have a in-line function that passes the current navigation object as a parameter to 'LogonStack' and 'OthersStack'. We do that, because every stack navigator, as you'd expect has a navigation of its own. Imagine this to be a parent-child navigation. Parent can access the child navigation, because they make it happen. But child has no clue about the parent and hence cannot return back to parent navigation, unless there is a reference to parent some where. The way we pass reference of parent to child is through screenProps. If you are interested and want to know more on the potential of screenProps, please stay tuned. We will have a separate episode on that. Now, why is that we don't have a similar parameter excahnge for 'PostAuthDrawer'. Reason is simple, we don't have anything that will want to come back to 'Login' stack. If we have a log-off screen that's pare of 'Login' stack, yes we will do the same for 'Home' too. Rest of the code is pretty simple

login.js

import React from 'react';
import {
  View,
  Text,
  Button,
  StyleSheet
} from 'react-native';
import { NavigationActions } from 'react-navigation';
import Footer from "../../../components/footer";

export default class LoginNav extends React.Component {
  static navigationOptions = {
    title: 'Login',
  };
  render() {
    return (
      <View style={styles.container}>
        <View style={styles.bodyView}>
          <Text style={styles.text}>PreAuthentication - Login</Text>
          <View>
            <Button title='Login' onPress={ () => this.onLogin() }/>
            <Button title='Cancel' onPress={ () => this.onCancel() } />
            <View>
              <Button title='Forgot Password' onPress={ () => this.onForgotPassword() } />
              <Button title='Signup' onPress={ () => this.onSignup() } />
            </View>
          </View>
        </View>
        <View style={styles.footerView}>
          <Footer screenProps={{rootNavigation: this.props.screenProps.rootNavigation}}/>
        </View>
      </View>);
  }

  onLogin() {
    // go back to main, with additional parameters and NavigationActions
    this.props.screenProps.rootNavigation.dispatch(NavigationActions.reset({
      index: 0,
      actions: [
        NavigationActions.navigate({ routeName: 'Home' })
      ]
    }));

  }

  onCancel() {
    // go back to main with additional parameters and NavigationActions
    this.props.screenProps.rootNavigation.navigate('Main');
  }

  onForgotPassword() {
    console.log('ForgotPassword button pressed');
    this.props.navigation.navigate('ForgotPassword');
  }

  onSignup() {
    console.log('signup button pressed');
    this.props.navigation.navigate('Signup');
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    flexDirection: 'column',
    justifyContent: 'center',
  },
  bodyView: {
    flex: 1,
    justifyContent: 'center',
    margin: 20,
  },
  footerView: {
    borderColor: 'black',
    borderWidth: 1,
    height: 60,
  },
  text: {
    fontSize: 20,
    textAlign: 'center',
    margin: 10,
  },
});


Here we made a few changes from our previous sample, first off, we introduced a new 'Footer' component. All that a footer offers us is a way to show 'Contact us' and 'Privacy policy' at the bottom of the page. Notice how the navigation is done from within the component. To navigate between 'Forgot password' and 'Signup', we use the regular navigation object. We use rootNavigator that was passed in screenParams to navigate back to parent view/screen. 

Now that 'Footer' is a component within 'Login', navigation boundries are also defined by 'Login' (i.e. 'Forgot Password' and 'Signup'). Hence we are compelled to pass 'rootNavigator' as a parameter to jump outside the current stack. If you remember, both 'Contact Us' and 'Privacy Policy' are defined as a seperate stack and are not part of 'Login' navigator. The only way we can reach 'Others' stack navigator from within 'Login' navigator is to reach the parent navigator, from there communicate with 'Other' navigator. Hope that make sense to you.

I loved writing 'Footer' component. Although, I'd have liked it even more, if I can have in-line style for 'Footer'. It would have been a lot more easier to just add the component and see the result. As opposed to contain Footer in a view and style to view to show up in the bottom.




Footer.js

import React from 'react';
import {
  StyleSheet,
  View,
  Text,
  TouchableOpacity,
  Alert,
} from 'react-native';
import { NavigationActions } from 'react-navigation';

export default class Footer extends React.Component {
  constructor(props) {
    super(props);
  }

  render() {
    return (
      <View style={styles.buttonContainer}>
        <TouchableOpacity style={styles.buttons} onPress={() => this.onContactUs()}>
          <Text style={styles.text}>Contact Us</Text>
        </TouchableOpacity>
        <TouchableOpacity style={styles.buttons} onPress={() => this.onPrivacyPolicy()}>
          <Text style={styles.text}>Privacy Policy</Text>
        </TouchableOpacity>
      </View>);
  }

  onContactUs() {
    console.log('Tapped on Contact Us');
    this.props.screenProps.rootNavigation.dispatch(NavigationActions.navigate({
      routeName: 'Others',
      params: {},
      action: NavigationActions.navigate({ routeName: 'ContactUs'})
    }));
  }

  onPrivacyPolicy() {
    console.log('Tapped on Privacy Policy');
    this.props.screenProps.rootNavigation.dispatch(NavigationActions.navigate({
      routeName: 'Others',
      params: {},
      action: NavigationActions.navigate({ routeName: 'PrivacyPolicy'})
    }));
  }
}

const styles = StyleSheet.create({
  buttonContainer: {
    flex: 1,
    alignItems:'stretch',
    flexDirection: 'row',
    borderWidth: 0,
  },
  buttons: {
    flex:0.5,
    borderWidth: 0,
    borderColor: 'black',
    borderRadius: 5,
    alignSelf: 'stretch',
    marginLeft: 20,
    marginRight:20,
  },
  text: {
    fontSize: 15,
    textAlign: 'center',
    alignSelf:'center',
    textAlignVertical:'center',
    padding:10,
  },
});


Footer is no different from any of the component we built in the past. The only addition here will be the code to navigate. We access the navigator from screenProps and use it. rest of the code is no different from what you have used in the past



forgot_password.js

import React from 'react';
import {
  View,
  Text,
  Button,
  StyleSheet,
} from 'react-native';

export default class ForgotPassword extends React.Component {
  static navigationOptions = {
    title: 'Forgot Password',
  };
  render() {
    return (
      <View style={styles.container}>
        <Text style={styles.text}>PreAuthentication - Forgot Password!!</Text>
        <View>
          <Button title='Submit' onPress={() => this.onSubmit()}/>
          <Button title='Cancel' onPress={() => this.onCancel()}/>
        </View>
      </View>);
  }

  onSubmit() {
    console.log('ForgotPassword - submit button pressed');
  }

  onCancel() {
    console.log('ForgotPassword - submit button pressed');
    this.props.navigation.goBack();
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#F5FCFF',
  },
  text: {
    fontSize: 20,
    textAlign: 'center',
    margin: 10,
  },
});


I don't think this code has changed in anyway from our previous examples. Container changes for 'Login' does not affect any navigation in this module. This is the beginning of loosely-coupled navigation.


signup.js

import React from 'react';
import {
  View,
  Text,
  Button,
  StyleSheet
} from 'react-native';

export default class Signup extends React.Component {
  static navigationOptions = {
    title: 'Signup',
  };
  render() {
    return (
      <View style={styles.container}>
        <Text style={styles.text}>PreAuthentication - Signup</Text>
        <View>
          <Button title='Submit' onPress={() => this.onSubmit()}/>
          <Button title='Cancel' onPress={() => this.onCancel()}/>
        </View>
      </View>);
  }

  onSubmit() {
    console.log('Signup - submit button pressed');
  }

  onCancel() {
    console.log('Signup - submit button pressed');
    this.props.navigation.goBack();
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#F5FCFF',
  },
  text: {
    fontSize: 20,
    textAlign: 'center',
    margin: 10,
  },
});


Not much has changed on this code from our previous samples. 


home.js

import React from 'react';
import {
  StyleSheet,
  View,
  Text,
  Button,
  TouchableOpacity,
  Image,
} from 'react-native';

export default class Home extends React.Component {
  static navigationOptions = {
    title: 'Home',
  };

  render() {
    return (
      <View style={styles.container}>
        <Text style={styles.text}>Home - Post Login</Text>
      </View>);
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#F5FCFF',
  },
  text: {
    fontSize: 20,
    textAlign: 'center',
    margin: 10,
  },
});


This is the landing page post login. If take a step back look into home_drawer.js, we have contained home.js in a seperate stacknavigator in stead of just referencing it as a screen. You can take it as a placeholder to add more in the coming days as well as to cover issues with DrawerNavigator. As I alleged earlier, the most complicated piece in React Native is Navigation. With nested navigators, complication adds up. I'd encourage you to take this code, play around with navigation and experience it for yourself.


submodule_11.js

import React from 'react';
import {
  StyleSheet,
  View,
  Text,
  Button,
} from 'react-native';

export default class SubModule11 extends React.Component {
  static navigationOptions = {
    title: 'Sub Module 11',
  };
  render() {
    return (
      <View style={styles.container}>
        <Text style={styles.text}>Module 1 - Submodule 1</Text>
        <View>
          <Button title='Next' onPress={() => this.onNext() }/>
          <Button title='Home' onPress={() => this.onHome() }/>
        </View>
      </View>);
  }

  onNext() {
    console.log('SubModule11: onNext touched');
    this.props.navigation.navigate('SubModule2')
  }

  onHome() {
    console.log('SubModule11: onHome touched');
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#F5FCFF',
  },
  text: {
    fontSize: 20,
    textAlign: 'center',
    margin: 10,
  },
});

There is no change to this code from our previous examples. submodule_21 & submodule_22 are the same as submodule_11 & submodule_12


submodule_12.js

import React from 'react';
import {
  StyleSheet,
  View,
  Text,
  Button,
  Alert,
} from 'react-native';
import { NavigationActions } from 'react-navigation';

export default class SubModule12 extends React.Component {
  static navigationOptions = {
    title: 'Sub Module 12',
  };
  render() {
    return (
      <View style={styles.container}>
        <Text style={styles.text}>Module 1 - Submodule 2</Text>
        <View>
          <Button title='Next' onPress={() => this.onNext() }/>
          <Button title='Home' onPress={() => this.onHome() }/>
        </View>
      </View>);
  }

  onNext() {
    Alert.alert('Yeah from navigation', "Sorry, can't navigate. Does not exist for the moment", [
        {text: 'Ask me later', onPress: () => console.log('Ask me later pressed')},
        {text: 'Cancel', onPress: () => console.log('Cancel Pressed'), style: 'cancel'},
        {text: 'OK', onPress: () => console.log('OK Pressed')},
      ],
      { cancelable: true });
    console.log('SubModule12: onNext touched');
  }

  onHome() {
    console.dir(this.props);
    this.props.navigation.navigate('Home');
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#F5FCFF',
  },
  text: {
    fontSize: 20,
    textAlign: 'center',
    margin: 10,
  },
});



home_drawer.js

import React from 'react';
import { StackNavigator, DrawerNavigator } from 'react-navigation';

import SubModule11 from './module_1/submodule_11';
import SubModule12 from './module_1/submodule_12'
import SubModule21 from './module_2/submodule_21';
import SubModule22 from './module_2/submodule_22'
import Home from './home';

const Module1Stack = StackNavigator({
  SubModule1: { screen: SubModule11 },
  SubModule2: { screen: SubModule12 }
}, {
  headerMode: 'screen'
});

const Module2Stack = StackNavigator({
  SubModule1: { screen: SubModule21 },
  SubModule2: { screen: SubModule22 }
}, {
  headerMode: 'screen'
});

const HomeStack = StackNavigator({
  Home: { screen: Home },
}, {
  headerMode: 'screen',
});

export const PostAuthDrawer = DrawerNavigator({
  Home: { screen: ({ navigation }) => <HomeStack screenProps={{ rootNavigation: navigation, headerMode: 'none', }} /> },
  Module1: {
    screen: Module1Stack,
  },
  Module2: {
    screen: Module2Stack,
  },
});

This code is very much open. Both 'Module1', 'Module2' & 'HomeStack' are simple 'StackNavigator'. All of this navigators are contained in 'DrawerNavigator'. The code for 'Home' is slightly different from the rest. This is one of the bug that I mentioned about React-Native Navigators (v 0.48). Try removing this code and play around to see why this code is there.

In subsequent posts we'll take a look at customization of 'DrawerNavigator'







No comments:

Post a Comment