React Native Plant App UI #2 : Implementing Custom Components


This tutorial is the second part of our React Native Plant App tutorial series. In the previous part, we successfully set up the overall project structure, navigations, constants and also implemented image caching. This tutorial is the continuation of the same tutorial from where we left off in the last part. So, it is recommended to go through the previous part in order to establish the basis and get insight into the overall project.


As mentioned in the previous part, the motivation to implement this UI series came from the React Native App Templates that accommodates a wide variety of mobile application templates written in React Native and powered by universal features and design. These app templates allow us to implement our own apps and even start our own startups. And, this second part is also the continuation of coding implementations and designs from the Youtube video tutorial by React UI Kit for the Plant App. The video tutorial delivers the coding implementation of the overall app very thoroughly. This tutorial series is the implementation of the same coding style and designs in the form of the article. Thus, the learners can go through each step and take their time understanding the implementations.

Overview

In this second part of this tutorial series, we are going to implement all the components that we are going to use in developing this Plant app. We are going to set up all the component files in the ‘./components/’ folder. We consider these files as predefined custom components that we are going to use in our upcoming tutorials. So, we are just going to copy the coding implementation for the necessary component into its respective component file.

So, let us begin!!

Implementing Different component files

Here, we are going to implement all the components that are required in order to develop this project. All the components are predefined. So, we are just going to copy the coding implementation of a specific component into its component files. We might remember from the previous tutorial that we have already set up the component files in the ‘./components’ folder. Now, all we need to do is to add the required code in all the components so that we can import these components in the different screens and implement them.

Here, all the components are implemented with required prop configurations. By setting up these components now, we can use them easily in our upcoming tutorials. We can simply import the ‘index.js’ file in the ‘./components’ folder in which all our components are already imported and thus exported as well. Now, we are going to copy the codes given in the code snippet below into the respective components files:

In the Block.js file of ‘./components’ folder

Here, we are going to implement the Block component. The overall coding implementation for the Block component is provided in the code snippet below:

    import React, { Component } from 'react'
    import { StyleSheet, View, Animated } from 'react-native'
    import { theme } from '../constants';
    export default class Block extends Component {
      handleMargins() {
        const { margin } = this.props;
        if (typeof margin === 'number') {
          return {
            marginTop: margin,
            marginRight: margin,
            marginBottom: margin,
            marginLeft: margin,
          }
        }
        if (typeof margin === 'object') {
          const marginSize = Object.keys(margin).length;
          switch (marginSize) {
            case 1:
              return {
                marginTop: margin[0],
                marginRight: margin[0],
                marginBottom: margin[0],
                marginLeft: margin[0],
              }
            case 2:
              return {
                marginTop: margin[0],
                marginRight: margin[1],
                marginBottom: margin[0],
                marginLeft: margin[1],
              }
            case 3:
              return {
                marginTop: margin[0],
                marginRight: margin[1],
                marginBottom: margin[2],
                marginLeft: margin[1],
              }
            default:
              return {
                marginTop: margin[0],
                marginRight: margin[1],
                marginBottom: margin[2],
                marginLeft: margin[3],
              }
          }
        }
      }
      handlePaddings() {
        const { padding } = this.props;
        if (typeof padding === 'number') {
          return {
            paddingTop: padding,
            paddingRight: padding,
            paddingBottom: padding,
            paddingLeft: padding,
          }
        }
        if (typeof padding === 'object') {
          const paddingSize = Object.keys(padding).length;
          switch (paddingSize) {
            case 1:
              return {
                paddingTop: padding[0],
                paddingRight: padding[0],
                paddingBottom: padding[0],
                paddingLeft: padding[0],
              }
            case 2:
              return {
                paddingTop: padding[0],
                paddingRight: padding[1],
                paddingBottom: padding[0],
                paddingLeft: padding[1],
              }
            case 3:
              return {
                paddingTop: padding[0],
                paddingRight: padding[1],
                paddingBottom: padding[2],
                paddingLeft: padding[1],
              }
            default:
              return {
                paddingTop: padding[0],
                paddingRight: padding[1],
                paddingBottom: padding[2],
                paddingLeft: padding[3],
              }
          }
        }
      }
      render() {
        const {
          flex,
          row,
          column,
          center,
          middle,
          left,
          right,
          top,
          bottom,
          card,
          shadow,
          color,
          space,
          padding,
          margin,
          animated,
          wrap,
          style,
          children,
          ...props
        } = this.props;
        const blockStyles = [
          styles.block,
          flex && { flex },
          flex === false && { flex: 0 }, // reset / disable flex
          row && styles.row,
          column && styles.column,
          center && styles.center,
          middle && styles.middle,
          left && styles.left,
          right && styles.right,
          top && styles.top,
          bottom && styles.bottom,
          margin && { ...this.handleMargins() },
          padding && { ...this.handlePaddings() },
          card && styles.card,
          shadow && styles.shadow,
          space && { justifyContent: `space-${space}` },
          wrap && { flexWrap: 'wrap' },
          color && styles[color], // predefined styles colors for backgroundColor
          color && !styles[color] && { backgroundColor: color }, // custom backgroundColor
          style, // rewrite predefined styles
        ];
        if (animated) {
          return (
            <Animated.View style={blockStyles} {...props}>
              {children}
            </Animated.View>
          )
        }
        return (
          <View style={blockStyles} {...props}>
            {children}
          </View>
        )
      }
    }
    export const styles = StyleSheet.create({
      block: {
        flex: 1,
      },
      row: {
        flexDirection: 'row',
      },
      column: {
        flexDirection: 'column',
      },
      card: {
        borderRadius: theme.sizes.radius,
      },
      center: {
        alignItems: 'center',
      },
      middle: {
        justifyContent: 'center',
      },
      left: {
        justifyContent: 'flex-start',
      },
      right: {
        justifyContent: 'flex-end',
      },
      top: {
        justifyContent: 'flex-start',
      },
      bottom: {
        justifyContent: 'flex-end',
      },
      shadow: {
        shadowColor: theme.colors.black,
        shadowOffset: { width: 0, height: 2 },
        shadowOpacity: 0.1,
        shadowRadius: 13,
        elevation: 2,
      },
      accent: { backgroundColor: theme.colors.accent, },
      primary: { backgroundColor: theme.colors.primary, },
      secondary: { backgroundColor: theme.colors.secondary, },
      tertiary: { backgroundColor: theme.colors.tertiary, },
      black: { backgroundColor: theme.colors.black, },
      white: { backgroundColor: theme.colors.white, },
      gray: { backgroundColor: theme.colors.gray, },
      gray2: { backgroundColor: theme.colors.gray2, },
    })

Here, all the required configuration along with styles are already predefined and implemented. We can use this component to create a block in the screen layout. It also allows different props as well.

In the Badge.js file of ./components folder

Here, we are going to implement the Badge component. The overall coding implementation for the Badge component is provided in the code snippet below:

    import React, { Component } from 'react'
    import { StyleSheet } from 'react-native'
    import Block from './Block';
    import { theme } from '../constants';
    export default class Badge extends Component {
      render() {
        const { children, style, size, color, ...props } = this.props;
        const badgeStyles = StyleSheet.flatten([
          styles.badge,
          size && {
            height: size,
            width: size,
            borderRadius: size,
          },
          style,
        ]);
        return (
          <Block flex={false} middle center color={color} style={badgeStyles} {...props}>
            {children}
          </Block>
        )
      }
    }
    const styles = StyleSheet.create({
      badge: {
        height: theme.sizes.base,
        width: theme.sizes.base,
        borderRadius: theme.sizes.border,
      }
    })

This Badge component allows us to add a badge to our screens.

In the Button.js file of ‘./components’ folder

Here, we are going to implement the Button component. The overall coding implementation for the Button component is provided in the code snippet below:

    import React, { Component } from 'react';
    import { StyleSheet, TouchableOpacity } from 'react-native';
    import { LinearGradient } from 'expo';
    import { theme } from '../constants';
    class Button extends Component {
      render() {
        const {
          style,
          opacity,
          gradient,
          color,
          startColor,
          endColor,
          end,
          start,
          locations,
          shadow,
          children,
          ...props
        } = this.props;
        const buttonStyles = [
          styles.button,
          shadow && styles.shadow,
          color && styles[color], // predefined styles colors for backgroundColor
          color && !styles[color] && { backgroundColor: color }, // custom backgroundColor
          style,
        ];
        if (gradient) {
          return (
            <TouchableOpacity
              style={buttonStyles}
              activeOpacity={opacity}
              {...props}
            >
              <LinearGradient
                start={start}
                end={end}
                locations={locations}
                style={buttonStyles}
                colors={[startColor, endColor]}
              >
                {children}
              </LinearGradient>
            </TouchableOpacity>
          )
        }
        return (
          <TouchableOpacity
            style={buttonStyles}
            activeOpacity={opacity || 0.8}
            {...props}
          >
            {children}
          </TouchableOpacity>
        )
      }
    }
    Button.defaultProps = {
      startColor: theme.colors.primary,
      endColor: theme.colors.secondary,
      start: { x: 0, y: 0 },
      end: { x: 1, y: 1 },
      locations: [0.1, 0.9],
      opacity: 0.8,
      color: theme.colors.white,
    }
    export default Button;
    const styles = StyleSheet.create({
      button: {
        borderRadius: theme.sizes.radius,
        height: theme.sizes.base * 3,
        justifyContent: 'center',
        marginVertical: theme.sizes.padding / 3,
      },
      shadow: {
        shadowColor: theme.colors.black,
        shadowOffset: { width: 0, height: 2 },
        shadowOpacity: 0.1,
        shadowRadius: 10,
      },
      accent: { backgroundColor: theme.colors.accent, },
      primary: { backgroundColor: theme.colors.primary, },
      secondary: { backgroundColor: theme.colors.secondary, },
      tertiary: { backgroundColor: theme.colors.tertiary, },
      black: { backgroundColor: theme.colors.black, },
      white: { backgroundColor: theme.colors.white, },
      gray: { backgroundColor: theme.colors.gray, },
      gray2: { backgroundColor: theme.colors.gray2, },
      gray3: { backgroundColor: theme.colors.gray3, },
      gray4: { backgroundColor: theme.colors.gray4, },
    });

This Button component enables us to add buttons to our screen with different style and prop configurations.

In the Card.js file of ‘./components’ folder

Here, we are going to implement the Card component. The overall coding implementation for the Card component is provided in the code snippet below:

    import React, { Component } from 'react';
    import { StyleSheet } from 'react-native';
    import Block from './Block';
    import { theme } from '../constants';
    export default class Card extends Component {
      render() {
        const { color, style, children, ...props } = this.props;
        const cardStyles = [
          styles.card,
          style,
        ];
        return (
          <Block color={color || theme.colors.white} style={cardStyles} {...props}>
            {children}
          </Block>
        )
      }
    }
    export const styles = StyleSheet.create({
      card: {
        borderRadius: theme.sizes.radius,
        padding: theme.sizes.base + 4,
        marginBottom: theme.sizes.base,
      },
    })

This Card component enables us to add cards to our screen with different prop configurations and size style properties.

In the Divider.js file of ‘./components’ folder

Here, we are going to implement the Divider component. The overall coding implementation for the Divider component is provided in the code snippet below:

    import React, { Component } from 'react';
    import { StyleSheet } from 'react-native';
    import Block from './Block';
    import { theme } from '../constants';
    export default class Divider extends Component {
      render() {
        const { color, style, ...props } = this.props;
        const dividerStyles = [
          styles.divider,
          style,
        ];
        return (
          <Block
            color={color || theme.colors.gray2}
            style={dividerStyles}
            {...props}
          />
        )
      }
    }
    export const styles = StyleSheet.create({
      divider: {
        height: 0,
        margin: theme.sizes.base * 2,
        borderBottomColor: theme.colors.gray2,
        borderBottomWidth: StyleSheet.hairlineWidth,
      }
    })

This Divider component enables us to add a horizontal divider with prop style configurations.

In the Input.js file of ‘./components’ folder

Here, we are going to implement the Input component. The overall coding implementation for the Input component is provided in the code snippet below:

    import React, { Component } from 'react'
    import { StyleSheet, TextInput } from 'react-native'
    import { Icon } from 'expo';
    import Text from './Text';
    import Block from './Block';
    import Button from './Button';
    import { theme } from '../constants';
    export default class Input extends Component {
      state = {
        toggleSecure: false,
      }
      renderLabel() {
        const { label, error } = this.props;
        return (
          <Block flex={false}>
            {label ? <Text gray2={!error} accent={error}>{label}</Text> : null}
          </Block>
        )
      }
      renderToggle() {
        const { secure, rightLabel } = this.props;
        const { toggleSecure } = this.state;
        if (!secure) return null;
        return (
          <Button
            style={styles.toggle}
            onPress={() => this.setState({ toggleSecure: !toggleSecure })}
          >
            {
              rightLabel ? rightLabel :
                <Icon.Ionicons
                  color={theme.colors.gray}
                  size={theme.sizes.font * 1.35}
                  name={!toggleSecure ? "md-eye" : "md-eye-off"}
              />
            }
          </Button>
        );
      }
      renderRight() {
        const { rightLabel, rightStyle, onRightPress } = this.props;
        if (!rightLabel) return null;
        return (
          <Button
            style={[styles.toggle, rightStyle]}
            onPress={() => onRightPress && onRightPress()}
          >
            {rightLabel}
          </Button>
        );
      }
      render() {
        const {
          email,
          phone,
          number,
          secure,
          error,
          style,
          ...props
        } = this.props;
        const { toggleSecure } = this.state;
        const isSecure = toggleSecure ? false : secure;
        const inputStyles = [
          styles.input,
          error && { borderColor: theme.colors.accent },
          style,
        ];
        const inputType = email
          ? 'email-address' : number
          ? 'numeric' : phone
          ? 'phone-pad' : 'default';
        return (
          <Block flex={false} margin={[theme.sizes.base, 0]}>
            {this.renderLabel()}
            <TextInput
              style={inputStyles}
              secureTextEntry={isSecure}
              autoComplete="off"
              autoCapitalize="none"
              autoCorrect={false}
              keyboardType={inputType}
              {...props}
            />
            {this.renderToggle()}
            {this.renderRight()}
          </Block>
        )
      }
    }
    const styles = StyleSheet.create({
      input: {
        borderWidth: StyleSheet.hairlineWidth,
        borderColor: theme.colors.black,
        borderRadius: theme.sizes.radius,
        fontSize: theme.sizes.font,
        fontWeight: '500',
        color: theme.colors.black,
        height: theme.sizes.base * 3,
      },
      toggle: {
        position: 'absolute',
        alignItems: 'flex-end',
        width: theme.sizes.base * 2,
        height: theme.sizes.base * 2,
        top: theme.sizes.base,
        right: 0,
      }
    });

This Input component is similar to InputText component provided by react-native. But, this Input component provides more features and easy configuration of props.

In the Progress.js file of ‘./components’ folder

Here, we are going to implement the Progress component. The overall coding implementation for the Progress component is provided in the code snippet below:

    import React, { Component } from 'react'
    import { StyleSheet } from 'react-native'
    import { LinearGradient } from 'expo';
    import Block from './Block';
    class Progress extends Component {
      render() {
        const { startColor, endColor, value, opacity, style, ...props } = this.props;
        return (
          <Block row center color="gray3" style={[styles.background, styles]} {...props}>
            <LinearGradient
              end={{ x: 1, y: 0 }}
              style={[styles.overlay, { flex: value }]}
              colors={[startColor, endColor]}
            >
              <LinearGradient
                end={{ x: 1, y: 0 }}
                colors={[startColor, endColor]}
                style={[styles.active, { flex: value }]}
              />
            </LinearGradient>
          </Block>
        )
      }
    }
    Progress.defaultProps = {
      startColor: '#4F8DFD',
      endColor: '#3FE4D4',
      value: 0.75,
      opacity: 0.2,
    }
    export default Progress;
    const styles = StyleSheet.create({
      background: {
        height: 6,
        marginVertical: 8,
        borderRadius: 8
      },
      overlay: {
        height: 14,
        maxHeight: 14,
        borderRadius: 7,
        paddingHorizontal: 4,
      },
      active: {
        marginTop: 4,
        height: 6,
        maxHeight: 6,
        borderRadius: 7,
      }
    })

This Progress component allows us to add a progress bar with gradient configurations to our screen.

In the Switch.js file of ‘./components’ folder

Here, we are going to implement the Switch component. The overall coding implementation for the Switch component is provided in the code snippet below:

    import React from 'react';
    import { Switch, Platform } from 'react-native';
    import { theme } from '../constants';
    const GRAY_COLOR = 'rgba(168, 182, 200, 0.30)';
    export default class SwitchInput extends React.PureComponent {
      render() {
        const { value, ...props } = this.props;
        let thumbColor = null;
        if (Platform.OS === 'android') {
          thumbColor = GRAY_COLOR;
          if (props.value) thumbColor = theme.colors.secondary;
        }
        return (
          <Switch
            thumbColor={thumbColor}
            ios_backgroundColor={GRAY_COLOR}
            trackColor={{
              // false: GRAY_COLOR,
              true: theme.colors.secondary
            }}
            value={value}
            {...props}
          />
        );
      }
    }

This Switch component allows us to add switch buttons to our screens.

In the Text.js file of ‘./components’ folder

Here, we are going to implement the Text component. The overall coding implementation for the Text component is provided in the code snippet below:

    // just copy this code from the driving repo :)
    import React, { Component } from "react";
    import { Text, StyleSheet } from "react-native";
    import { theme } from "../constants";
    export default class Typography extends Component {
      render() {
        const {
          h1,
          h2,
          h3,
          title,
          body,
          caption,
          small,
          size,
          transform,
          align,
          // styling
          regular,
          bold,
          semibold,
          medium,
          weight,
          light,
          center,
          right,
          spacing, // letter-spacing
          height, // line-height
          // colors
          color,
          accent,
          primary,
          secondary,
          tertiary,
          black,
          white,
          gray,
          gray2,
          style,
          children,
          ...props
        } = this.props;
        const textStyles = [
          styles.text,
          h1 && styles.h1,
          h2 && styles.h2,
          h3 && styles.h3,
          title && styles.title,
          body && styles.body,
          caption && styles.caption,
          small && styles.small,
          size && { fontSize: size },
          transform && { textTransform: transform },
          align && { textAlign: align },
          height && { lineHeight: height },
          spacing && { letterSpacing: spacing },
          weight && { fontWeight: weight },
          regular && styles.regular,
          bold && styles.bold,
          semibold && styles.semibold,
          medium && styles.medium,
          light && styles.light,
          center && styles.center,
          right && styles.right,
          color && styles[color],
          color && !styles[color] && { color },
          // color shortcuts
          accent && styles.accent,
          primary && styles.primary,
          secondary && styles.secondary,
          tertiary && styles.tertiary,
          black && styles.black,
          white && styles.white,
          gray && styles.gray,
          gray2 && styles.gray2,
          style // rewrite predefined styles
        ];
        return (
          <Text style={textStyles} {...props}>
            {children}
          </Text>
        );
      }
    }
    const styles = StyleSheet.create({
      // default style
      text: {
        fontSize: theme.sizes.font,
        color: theme.colors.black
      },
      // variations
      regular: {
        fontWeight: "normal",
      },
      bold: {
        fontWeight: "bold",
      },
      semibold: {
        fontWeight: "500",
      },
      medium: {
        fontWeight: "500",
      },
      light: {
        fontWeight: "200",
      },
      // position
      center: { textAlign: "center" },
      right: { textAlign: "right" },
      // colors
      accent: { color: theme.colors.accent },
      primary: { color: theme.colors.primary },
      secondary: { color: theme.colors.secondary },
      tertiary: { color: theme.colors.tertiary },
      black: { color: theme.colors.black },
      white: { color: theme.colors.white },
      gray: { color: theme.colors.gray },
      gray2: { color: theme.colors.gray2 },
      // fonts
      h1: theme.fonts.h1,
      h2: theme.fonts.h2,
      h3: theme.fonts.h3,
      title: theme.fonts.title,
      body: theme.fonts.body,
      caption: theme.fonts.caption,
      small: theme.fonts.small
    });

This Text component is similar to that of the Text component provided by the react-native package. This Text component provides an additional range of props to configure the text on our screen.

Using Some of these Components

Here, we are going to use the components that we implemented earlier in the App.js as well as the Welcome screen.

Using in App.js file

In the App.js file, we are going to use the Block component in order to display every screen in our app as a block.

First, we need to import the Block component as shown in the code snippet below:

    import { Block } from './components';

Now, in the `render()` function of the App.js file:

    return (
          <Block white>
            <Navigation />
          </Block>
        );

Here, we have used the Block component with white prop configuration. The white prop configuration automatically sets the block color property to white.

Using in Welcome.js file

In the Welcome.js file, we are going to use the Block and Text components with some prop configurations. The overall coding implementation in the Welcome.js file is shown in the code snippet below:

    import { StyleSheet } from 'react-native';
    import { Button, Block, Text } from '../components';
    export default class Welcome extends React.Component {
      static navigationOptions = {
        header : null
      }
      render(){
        return (
          <Block center middle>
            <Text>Welcome</Text>
          </Block>
        );
      }
      
    }

Here, we have imported the Button, Block and Text component from the index.js file of the './components' folder. We have also set the header config to null using navigationOptions. Then, we have returned the Block parent component wrapping the Text component. Both the components are our predefined components not from any other package. We have also used the center and middle prop in Block component which sets the content inside the Block to the center of the screen.

Hence, we will get the following result in our emulator screen:

As we can see, we have got the text at the center of the screen just by using some props in the Block component. We have not used any custom style properties. With this, we have come to the end of this part of the tutorial.

Finally, We have successfully added all the components necessary to implement this React Native Plant App.

Conclusion

This tutorial is the second part of the React Native Plant App tutorial series. In this part, we continued from where we left off in the first part of this tutorial series. In this part of the tutorial, we implemented all the components in our ‘./components/’ folder. All the components can be considered as predefined custom components that we are going to use to implement different UI sections in the App. After setting up all the components, we also learned how to make use of these components in the Welcome screen along with prop configurations.

In the next part of this tutorial series, we are going to start implementing some of the UI sections of our Welcome screen.

So, Stay Tuned folks!!!