DEV Community

Cover image for React Native Plant App UI #2 : Implementing Custom Components
kris
kris

Posted on • Updated on • Originally published at kriss.io

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

Top comments (0)