Categories
React

How to Add Form Validation to Your React App with Redux Form

We look at basic uses of Redux form.

Form validation is a frequently needed feature in web apps. React does not come with its own form validation since it’s supposed to be a view library that provides developers with code structure. Fortunately, developers have come up with many form validation solutions. One of them is Redux Form, located at https://redux-form.com/8.2.2/.

As its name suggests, Redux Form uses Redux for storing the form data. It requires its own reducer for storing a form’s data. The form component that you write connects to the built-in reducer to store the data. If you want to get initial data, you load the data in your Redux store, then Redux Form can retrieve it.

Form value checks and form validations errors are provided by you. You can get form values from the store when you need it. The form input components can be customized easily by passing our custom component into the Field component provided by Redux Form.

In this article, we will write an address book app to illustrate the use of Redux Form for form validation. We need will have a Redux store for storing the form data, the selected contact data when editing, and a list of contacts. To start, we will run Create React App by running npx create-react-app address-book to create the app.

After that is run, we add our own libraries. We need Axios for making HTTP requests, React Bootstrap for styling, React Redux and Redux for state management, React Router for routing, and of course, Redux Form for form validation.

We install them by running:

npm i axios react-bootstrap react-redux react-router-fom redux redux-form

With our libraries installed, we can start writing our app. We create all files in the src folder unless otherwise specified. To start, we create actionCreators.js in the src folder and add:

import { SET_CONTACTS, LOAD } from "./actions";

const setContacts = contacts => {
  return {
    type: SET_CONTACTS,
    payload: contacts
  };
};

const setCurrentContact = contact => {
  return {
    type: LOAD,
    data: contact
  };
};

export { setContacts, setCurrentContact };

These are the actions and payload that we dispatch to our Redux store. setContacts is for setting a list of contacts and setCurrentContact is for loading the contact for editing.

Next, we create actions.js and add:

const SET_CONTACTS = "SET_CONTACTS";
const LOAD = "LOAD";

export { SET_CONTACTS, LOAD };

These are the constants for the action types of our store dispatch actions. Next in App.js , we replace the existing code with:

import React from "react";
import { Router, Route, Link } from "react-router-dom";
import HomePage from "./HomePage";
import { createBrowserHistory as createHistory } from "history";
import Navbar from "react-bootstrap/Navbar";
import Nav from "react-bootstrap/Nav";
import "./App.css";
const history = createHistory();

function App() {
  return (
    <div className="App">
      <Router history={history}>
        <Navbar bg="primary" expand="lg" variant="dark">
          <Navbar.Brand href="#home">Address Book App</Navbar.Brand>
          <Navbar.Toggle aria-controls="basic-navbar-nav" />
          <Navbar.Collapse id="basic-navbar-nav">
            <Nav className="mr-auto">
              <Nav.Link href="/">Home</Nav.Link>
            </Nav>
          </Navbar.Collapse>
        </Navbar>
        <Route path="/" exact component={HomePage} />
      </Router>
    </div>
  );
}

export default App;

to add the React Bootstrap Navbar component to the top of our page. We also add the React Router route for the home page that we’ll create.

Next, we create ContactForm.js and add:

import React from "react";
import Form from "react-bootstrap/Form";
import Button from "react-bootstrap/Button";
import PropTypes from "prop-types";
import { addContact, editContact, getContacts } from "./requests";
import { connect } from "react-redux";
import { setContacts, setCurrentContact } from "./actionCreators";
import { Field, reduxForm } from "redux-form";
import { renderInputField } from "./RenderInputField";
import { renderCountryField } from "./RenderCountryField";
import { getFormValues, isInvalid } from "redux-form";

const validate = values => {
  const errors = {};
  if (!values.firstName) {
    errors.firstName = "Required";
  }
  if (!values.lastName) {
    errors.lastName = "Required";
  }
  if (!values.city) {
    errors.city = "Required";
  }
  if (!values.address) {
    errors.address = "Required";
  }
  if (!values.region) {
    errors.region = "Required";
  }
  if (!values.postalCode) {
    errors.postalCode = "Required";
  } else {
    if (
      (values.country == "United States" &&
        !/^[0-9]{5}(?:-[0-9]{4})?$/.test(values.postalCode)) ||
      (values.country == "Canada" &&
        !/^[A-Za-z]d[A-Za-z][ -]?d[A-Za-z]d$/.test(values.postalCode))
    ) {
      errors.postalCode = "Invalid postal code";
    }
  }

if (!values.phone) {
    errors.phone = "Required";
  } else {
    if (
      (values.country == "United States" || values.country == "Canada") &&
      !/^[2-9]d{2}[2-9]d{2}d{4}$/.test(values.phone)
    ) {
      errors.phone = "Invalid phone";
    }
  }

if (!/[^@]+@[^.]+..+/.test(values.email)) {
    errors.email = "Invalid email";
  }
  if (Number.isNaN(+values.age) || values.age < 0 || values.age > 200) {
    errors.age = "Age must be between 0 and 200";
  }
  if (!values.country) {
    errors.country = "Required";
  }
  return errors;
};

function ContactForm({
  edit,
  onSave,
  setContacts,
  contact,
  onCancelAdd,
  onCancelEdit,
  invalid,
  values,
  currentContact,
  ...props
}) {
  const handleSubmit = async event => {
    if (invalid) {
      return;
    }
    if (!edit) {
      await addContact(values);
    } else {
      await editContact(values);
    }
    const response = await getContacts();
    setContacts(response.data);
    onSave();
  };

return (
    <div className="form">
      <Form noValidate onSubmit={props.handleSubmit(handleSubmit.bind(this))}>
        <Field
          name="firstName"
          type="text"
          component={renderInputField}
          label="First Name"
        />

        <Field
          name="lastName"
          type="text"
          component={renderInputField}
          label="Last Name"
        />

        <Field
          name="address"
          type="text"
          component={renderInputField}
          label="Address"
        />

        <Field
          name="city"
          type="text"
          component={renderInputField}
          label="City"
        />

        <Field
          name="region"
          type="text"
          component={renderInputField}
          label="Region"
        />

        <Field name="country" component={renderCountryField} label="Country" />

        <Field
          name="postalCode"
          type="text"
          component={renderInputField}
          label="Postal Code"
        />

        <Field
          name="phone"
          type="text"
          component={renderInputField}
          label="Phone"
        />

        <Field
          name="email"
          type="email"
          component={renderInputField}
          label="Email"
        />

        <Field
          name="age"
          type="text"
          component={renderInputField}
          label="Age"
        />

        <Button type="submit" style={{ marginRight: "10px" }}>
          Save
        </Button>
        <Button type="button" onClick={edit ? onCancelEdit : onCancelAdd}>
          Cancel
        </Button>
      </Form>
    </div>
  );
}

ContactForm.propTypes = {
  edit: PropTypes.bool,
  onSave: PropTypes.func,
  onCancelAdd: PropTypes.func,
  onCancelEdit: PropTypes.func,
  contact: PropTypes.object
};

const mapStateToProps = state => {
  return {
    contacts: state.contacts
  };
};

const mapDispatchToProps = dispatch => ({
  setContacts: contacts => dispatch(setContacts(contacts))
});

ContactForm = connect(state => ({
  values: getFormValues("syncValidation")(state),
  invalid: isInvalid("syncValidation")(state)
}))(ContactForm);

ContactForm = connect(
  mapStateToProps,
  mapDispatchToProps
)(ContactForm);

ContactForm = reduxForm({
  form: "syncValidation",
  validate,
  enableReinitialize: true
})(ContactForm);

ContactForm = connect(state => {
  return {
    initialValues: state.currentContact.data
  };
})(ContactForm);

export default ContactForm;

This is the form for editing and adding our contact. We use Redux Form extensively here. The Form component is provided by React Bootstrap. The Field component is from Redux Form. We pass in our custom input field components into the component prop of the field, along with the name , type , and label of our inputs. We will use them in our custom field components, renderInputField and renderCountryField .

In the onSubmit prop of the Form component, we wrap our own handleSubmit function with Redux Form’s handleSubmit function provided in the props so that we can use our own form submit handler instead of Redux Form’s handleSubmit function for handling form submissions.

In our handleSubmit function, we check for the form’s validity by getting the invalid prop from the props. The validation rules are in the validate function at the top of the code, which we passing into the reduxForm function at the bottom of our code. In the validate function, we get the form input values in the values parameter and set the errors object with the error messages of each field. We can check validation that depends on other fields easily in this function.

Once the form is checked to be valid, whereinvalid is false , then we call addContact or editContact by passing the value prop we get from the connect functions from Redux Form depending on if we are adding or not and save the data. Then we call getContacts and setContacts from mapDispatchToProps to update our Redux store with the latest contact entries from back end. Then call onSave , which is passed in from HomePage.js that we will create, to close the modal.

The invalid , and values props are generated by running:

ContactForm = connect(state => ({
  values: getFormValues("syncValidation")(state),
  invalid: isInvalid("syncValidation")(state)
}))(ContactForm);

This block:

ContactForm = reduxForm({
  form: "syncValidation",
  validate,
  enableReinitialize: true
})(ContactForm);

provides us with form validation capabilities of Redux Form in this component, and the initial values of the edit form is retrieved from our store by running:

ContactForm = connect(state => {
  return {
    initialValues: state.currentContact.data
  };
})(ContactForm);

Next, create exports.js and add:

export const COUNTRIES = [
  "Afghanistan",
  "Albania",
  "Algeria",
  "Andorra",
  "Angola",
  "Anguilla",
  "Antigua &amp; Barbuda",
  "Argentina",
  "Armenia",
  "Aruba",
  "Australia",
  "Austria",
  "Azerbaijan",
  "Bahamas",
  "Bahrain",
  "Bangladesh",
  "Barbados",
  "Belarus",
  "Belgium",
  "Belize",
  "Benin",
  "Bermuda",
  "Bhutan",
  "Bolivia",
  "Bosnia &amp; Herzegovina",
  "Botswana",
  "Brazil",
  "British Virgin Islands",
  "Brunei",
  "Bulgaria",
  "Burkina Faso",
  "Burundi",
  "Cambodia",
  "Cameroon",
  "Canada",
  "Cape Verde",
  "Cayman Islands",
  "Chad",
  "Chile",
  "China",
  "Colombia",
  "Congo",
  "Cook Islands",
  "Costa Rica",
  "Cote D Ivoire",
  "Croatia",
  "Cruise Ship",
  "Cuba",
  "Cyprus",
  "Czech Republic",
  "Denmark",
  "Djibouti",
  "Dominica",
  "Dominican Republic",
  "Ecuador",
  "Egypt",
  "El Salvador",
  "Equatorial Guinea",
  "Estonia",
  "Ethiopia",
  "Falkland Islands",
  "Faroe Islands",
  "Fiji",
  "Finland",
  "France",
  "French Polynesia",
  "French West Indies",
  "Gabon",
  "Gambia",
  "Georgia",
  "Germany",
  "Ghana",
  "Gibraltar",
  "Greece",
  "Greenland",
  "Grenada",
  "Guam",
  "Guatemala",
  "Guernsey",
  "Guinea",
  "Guinea Bissau",
  "Guyana",
  "Haiti",
  "Honduras",
  "Hong Kong",
  "Hungary",
  "Iceland",
  "India",
  "Indonesia",
  "Iran",
  "Iraq",
  "Ireland",
  "Isle of Man",
  "Israel",
  "Italy",
  "Jamaica",
  "Japan",
  "Jersey",
  "Jordan",
  "Kazakhstan",
  "Kenya",
  "Kuwait",
  "Kyrgyz Republic",
  "Laos",
  "Latvia",
  "Lebanon",
  "Lesotho",
  "Liberia",
  "Libya",
  "Liechtenstein",
  "Lithuania",
  "Luxembourg",
  "Macau",
  "Macedonia",
  "Madagascar",
  "Malawi",
  "Malaysia",
  "Maldives",
  "Mali",
  "Malta",
  "Mauritania",
  "Mauritius",
  "Mexico",
  "Moldova",
  "Monaco",
  "Mongolia",
  "Montenegro",
  "Montserrat",
  "Morocco",
  "Mozambique",
  "Namibia",
  "Nepal",
  "Netherlands",
  "Netherlands Antilles",
  "New Caledonia",
  "New Zealand",
  "Nicaragua",
  "Niger",
  "Nigeria",
  "Norway",
  "Oman",
  "Pakistan",
  "Palestine",
  "Panama",
  "Papua New Guinea",
  "Paraguay",
  "Peru",
  "Philippines",
  "Poland",
  "Portugal",
  "Puerto Rico",
  "Qatar",
  "Reunion",
  "Romania",
  "Russia",
  "Rwanda",
  "Saint Pierre &amp; Miquelon",
  "Samoa",
  "San Marino",
  "Satellite",
  "Saudi Arabia",
  "Senegal",
  "Serbia",
  "Seychelles",
  "Sierra Leone",
  "Singapore",
  "Slovakia",
  "Slovenia",
  "South Africa",
  "South Korea",
  "Spain",
  "Sri Lanka",
  "St Kitts &amp; Nevis",
  "St Lucia",
  "St Vincent",
  "St. Lucia",
  "Sudan",
  "Suriname",
  "Swaziland",
  "Sweden",
  "Switzerland",
  "Syria",
  "Taiwan",
  "Tajikistan",
  "Tanzania",
  "Thailand",
  "Timor L'Este",
  "Togo",
  "Tonga",
  "Trinidad &amp; Tobago",
  "Tunisia",
  "Turkey",
  "Turkmenistan",
  "Turks &amp; Caicos",
  "Uganda",
  "Ukraine",
  "United Arab Emirates",
  "United Kingdom",
  "United States",
  "United States Minor Outlying Islands",
  "Uruguay",
  "Uzbekistan",
  "Venezuela",
  "Vietnam",
  "Virgin Islands (US)",
  "Yemen",
  "Zambia",
  "Zimbabwe"
];

so we get an array of countries for our country field drop down.

Next in HomePage.css , we add:

.home-page {
    padding: 20px;
}

to get our page some padding.

Then we create HomePage.js and add:

import React from "react";
import { useState, useEffect } from "react";
import Table from "react-bootstrap/Table";
import ButtonToolbar from "react-bootstrap/ButtonToolbar";
import Button from "react-bootstrap/Button";
import Modal from "react-bootstrap/Modal";
import ContactForm from "./ContactForm";
import "./HomePage.css";
import { connect } from "react-redux";
import { getContacts, deleteContact } from "./requests";
import { setCurrentContact } from "./actionCreators";

function HomePage({ setCurrentContact }) {
  const [openAddModal, setOpenAddModal] = useState(false);
  const [openEditModal, setOpenEditModal] = useState(false);
  const [initialized, setInitialized] = useState(false);
  const [selectedContact, setSelectedContact] = useState({});
  const [contacts, setContacts] = useState([]);

  const openModal = () => {
    setOpenAddModal(true);
  };

  const closeModal = () => {
    setOpenAddModal(false);
    setOpenEditModal(false);
    getData();
  };

  const cancelAddModal = () => {
    setOpenAddModal(false);
  };

  const editContact = contact => {
    setSelectedContact(contact);
    setCurrentContact(contact);
    setOpenEditModal(true);
  };

  const cancelEditModal = () => {
    setOpenEditModal(false);
  };

  const getData = async () => {
    const response = await getContacts();
    setContacts(response.data);
    setInitialized(true);
  };

  const deleteSelectedContact = async id => {
    await deleteContact(id);
    getData();
  };

  useEffect(() => {
    if (!initialized) {
      getData();
    }
  });

  return (
    <div className="home-page">
      <h1>Contacts</h1>
      <Modal show={openAddModal} onHide={closeModal}>
        <Modal.Header closeButton>
          <Modal.Title>Add Contact</Modal.Title>
        </Modal.Header>
        <Modal.Body>
          <ContactForm
            edit={false}
            onSave={closeModal.bind(this)}
            onCancelAdd={cancelAddModal}
          />
        </Modal.Body>
      </Modal>

      <Modal show={openEditModal} onHide={closeModal}>
        <Modal.Header closeButton>
          <Modal.Title>Edit Contact</Modal.Title>
        </Modal.Header>
        <Modal.Body>
          <ContactForm
            edit={true}
            onSave={closeModal.bind(this)}
            contact={selectedContact}
            onCancelEdit={cancelEditModal}
          />
        </Modal.Body>
      </Modal>
      <ButtonToolbar onClick={openModal}>
        <Button variant="outline-primary">Add Contact</Button>
      </ButtonToolbar>
      <br />
      <div className="table-responsive">
        <Table striped bordered hover>
          <thead>
            <tr>
              <th>First Name</th>
              <th>Last Name</th>
              <th>Address</th>
              <th>City</th>
              <th>Country</th>
              <th>Postal Code</th>
              <th>Phone</th>
              <th>Email</th>
              <th>Age</th>
              <th>Edit</th>
              <th>Delete</th>
            </tr>
          </thead>
          <tbody>
            {contacts.map(c => (
              <tr key={c.id}>
                <td>{c.firstName}</td>
                <td>{c.lastName}</td>
                <td>{c.address}</td>
                <td>{c.city}</td>
                <td>{c.country}</td>
                <td>{c.postalCode}</td>
                <td>{c.phone}</td>
                <td>{c.email}</td>
                <td>{c.age}</td>
                <td>
                  <Button
                    variant="outline-primary"
                    onClick={editContact.bind(this, c)}
                  >
                    Edit
                  </Button>
                </td>
                <td>
                  <Button
                    variant="outline-primary"
                    onClick={deleteSelectedContact.bind(this, c.id)}
                  >
                    Delete
                  </Button>
                </td>
              </tr>
            ))}
          </tbody>
        </Table>
      </div>
    </div>
  );
}

const mapStateToProps = state => {
  return {
    contacts: state.contacts
  };
};

const mapDispatchToProps = dispatch => ({
  setCurrentContact: contact => dispatch(setCurrentContact(contact))
});

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(HomePage);

This is the home page of our app. We have a button to open the modal and a table to display the address book entries. Also, we have functions to open and close the modal with the openModal , closeModal , cancelAddModal , and cancelEditModal functions. We have a modal for add and another for editing contacts, they open the same form. When the edit modal is open with the editContact function, setCurrentContact provided by the mapDispatchToProps function via the props is run, setting the current contact being edited so that it can be retrieved for the initialValues of our ContactForm component by calling the connect function like we did. We have Edit and Delete buttons to call editContact and deleteSelectedContact respectively.

In index.js , replace the existing code with:

import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import * as serviceWorker from "./serviceWorker";
import { createStore, combineReducers } from "redux";
import { reducer as formReducer } from "redux-form";
import { Provider } from "react-redux";
import { contactsReducer, currentContactReducer } from "./reducers";

const rootReducer = combineReducers({
  form: formReducer,
  contacts: contactsReducer,
  currentContact: currentContactReducer
});

const store = createStore(rootReducer);

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById("root")
);

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: [https://bit.ly/CRA-PWA](https://bit.ly/CRA-PWA)
serviceWorker.unregister();

to inject our Redux store so that it’s available throughout our app.

Then create reducers.js and add:

import { SET_CONTACTS, LOAD } from "./actions";

function contactsReducer(state = {}, action) {
  switch (action.type) {
    case SET_CONTACTS:
      state = JSON.parse(JSON.stringify(action.payload));
      return state;
    default:
      return state;
  }
}

function currentContactReducer(state = {}, action) {
  switch (action.type) {
    case LOAD:
      return {
        data: action.data
      };
    default:
      return state;
  }
}

export { contactsReducer, currentContactReducer };

to create the 2 reducers that we mentioned before.

Next, create RenderCountryField.js and add:

import React from "react";
import Form from "react-bootstrap/Form";
import Col from "react-bootstrap/Col";
import { COUNTRIES } from "./exports";

export const renderCountryField = ({
  input,
  label,
  type,
  meta: { touched, error, warning }
}) => (
  <Form.Row>
    <Form.Group as={Col} md="12" controlId="country">
      <Form.Label>{label}</Form.Label>
      <Form.Control
        as="select"
        type={type}
        placeholder={label}
        {...input}
        isInvalid={touched && error}
      >
        {COUNTRIES.map(c => (
          <option key={c} value={c}>
            {c}
          </option>
        ))}
      </Form.Control>
      <Form.Control.Feedback type="invalid">
        {touched && error}
      </Form.Control.Feedback>
    </Form.Group>
  </Form.Row>
);

to create our country drop-down with form validation, which we pass into the component prop of the Field component in ContactForm.js .

Next create RenderInputField.js and add:

import React from "react";
import Form from "react-bootstrap/Form";
import Col from "react-bootstrap/Col";

export const renderInputField = ({
  input,
  label,
  type,
  meta: { touched, error, warning }
}) => (
  <Form.Row>
    <Form.Group as={Col} md="12">
      <Form.Label>{label}</Form.Label>
      <Form.Control
        type={type}
        placeholder={label}
        {...input}
        isInvalid={touched && error}
      />
      <Form.Control.Feedback type="invalid">
        {touched &&
          ((error && <span>{error}</span>) ||
            (warning && <span>{warning}</span>))}
      </Form.Control.Feedback>
    </Form.Group>
  </Form.Row>
);

to create the text input for our contact form.

Both inputs use the Form component from React Bootstrap.

Next, create requests.js and add:

const APIURL = 'http://localhost:3000';
const axios = require('axios');
export const getContacts = () => axios.get(`${APIURL}/contacts`);

export const addContact = (data) => axios.post(`${APIURL}/contacts`, data);

export const editContact = (data) => axios.put(`${APIURL}/contacts/${data.id}`, data);

export const deleteContact = (id) => axios.delete(`${APIURL}/contacts/${id}`);

to create the functions for making the HTTP requests to our back end to persist our contact data.

Finally, in index.html , replace the existing code with:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta
      name="description"
      content="Web site created using create-react-app"
    />
    <link rel="apple-touch-icon" href="logo192.png" />
    <!--
      manifest.json provides metadata used when your web app is installed on a
      user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
    -->
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
    <!--
      Notice the use of %PUBLIC_URL% in the tags above.
      It will be replaced with the URL of the `public` folder during the build.
      Only files inside the `public` folder can be referenced from the HTML.

      Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
      work correctly both with client-side routing and a non-root public URL.
      Learn how to configure a non-root public URL by running `npm run build`.
    -->
    <title>React Address Book App</title>
    <link
      rel="stylesheet"
      href="https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
      integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"
      crossorigin="anonymous"
    />
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
    <!--
      This HTML file is a template.
      If you open it directly in the browser, you will see an empty page.

      You can add webfonts, meta tags, or analytics to this file.
      The build step will place the bundled scripts into the <body> tag.

      To begin the development, run `npm start` or `yarn start`.
      To create a production bundle, use `npm run build` or `yarn build`.
    -->
  </body>
</html>

to change our app title and add our Bootstrap CSS.

Now we can run the app by running set PORT=3001 && react-scripts start on Windows or PORT=3006 react-scripts start on Linux.

To start the back end, we first install the json-server package by running npm i json-server. Then, go to our project folder and run:

json-server --watch db.json

In db.json, change the text to:

{
  "contacts": [
  ]
}

So we have the contacts endpoints defined in the requests.js available.

Categories
JavaScript JavaScript Basics

Cloning Arrays in JavaScript

There are a few ways to clone an array in JavaScript,

Object.assign

Object.assign allows us to make a shallow copy of any kind of object including arrays.

For example:

const a = [1,2,3];
const b = Object.assign([], a); // [1,2,3]

Array.slice

The Array.slice function returns a copy of the original array.

For example:

const a = [1,2,3];
const b = a.slice(0); // [1,2,3]

Array.from

The Array.slice function returns a copy of the original array. It takes array like objects like Set and it also takes an array as an argument.

const a = [1,2,3];
const b = Array.from(a); // [1,2,3]

Spread Operator

The fastest way to copy an array, which is available with ES6 or later, is the spread operator.

const a = [1,2,3];
const b = [...a]; // [1,2,3]

JSON.parse and JSON.stringify

This allows for deep copy of an array and only works if the objects in the array are plain objects. It can be used like this:

const a = [1,2,3];
const b = JSON.parse(JSON.stringify(a)); // [1,2,3]

Categories
CSS

Simple Introduction to CSS Grid

CSS grid lets us create layouts with CSS easily.

It’s also good for creating responsive layouts because we can group different items together.

In this article, we’ll look at how to define a grid layout also make it responsive.

Defining a Grid Layout

We can define a grid layout with some HTML and CSS.

If we want to define 4 div’s with one header, one left and right div with an empty space in between it, and one footer, we can do it as follows.

First, we add the HTML:

<div class='container'>  
  <div class='item-a'>  
    A  
  </div>  
  <div class='item-b'>  
    B  
  </div>  
  <div class='item-c'>  
    C  
  </div>  
  <div class='item-d'>  
    D  
  </div>  
</div>

Then we add the CSS:

.item-a {  
  grid-area: header;  
  background-color: lightyellow;  
}

.item-b {  
  grid-area: main;  
  background-color: lightgreen;  
}

.item-c {  
  grid-area: sidebar;  
  background-color: lightblue;  
}

.item-d {  
  grid-area: footer;  
  background-color: pink;  
}

.container {  
  display: grid;  
  grid-template-columns: 24vw 25vw 25vw 24vw;  
  grid-template-rows: auto;  
  grid-template-areas:  
    "header header header header"  
    "main main . sidebar"  
    "footer footer footer footer";  
}

Then we get the following:

The CSS has the following parts.

First, we look at the container class. This is the most important part of the grid.

We have display: grid; which designates that the div with the container class has a grid layout.

Then we define the grid columns with the grid-template-columns , which we set the value to 24vw 25vw 25vw 24vw .

This means we have 4 columns with the leftmost and rightmost columns having 24vw and the one in between having 25vw .

Then we have grid-template-rows: auto; . We leave it as auto since we aren’t concerned with the height of the rows.

To define how the grid is shared between the components inside the div with the container class, we write:

grid-template-areas:  
    "header header header header"  
    "main main . sidebar"  
    "footer footer footer footer";

We have the first row all fille with the header area in the first row.

Then in the new row, we have the main area fill the 2 leftmost columns. A dot for an empty space, and the sidebar area for the rightmost column.

In the bottom row, we have the footer area fill the whole row.

Then we can designate the areas with the item classes as follows:

.item-a {  
  grid-area: header;  
  background-color: lightyellow;  
}

.item-b {  
  grid-area: main;  
  background-color: lightgreen;  
}

.item-c {  
  grid-area: sidebar;  
  background-color: lightblue;  
}

.item-d {  
  grid-area: footer;  
  background-color: pink;  
}

In the code above, we designated whatever having the item-a class as the header with the grid-area and set the background color.

This means that whatever has the class item-a will be the header that fills the top row.

Then we do the same with the other 3 classes, so whatever has the class item-b will fill the leftmost 2 columns in the second row. The element with the class item-c fills the rightmost column of the second row. The element with the class item-d fills all of the bottom rows.

This is how we define a layout with a grid.

Responsive Layout

We can define a responsive layout with CSS selectors and adding a new layout for narrow screens.

We can define a narrow screen layout that only shows the header, main and footer areas as follows:

.item-a {  
  grid-area: header;  
  background-color: lightyellow;  
}

.item-b {  
  grid-area: main;  
  background-color: lightgreen;  
}

.item-c {  
  grid-area: sidebar;  
  background-color: lightblue;  
}

.item-d {  
  grid-area: footer;  
  background-color: pink;  
}

.container {  
  display: grid;  
  grid-template-rows: auto;  
}

@media only screen and (min-width: 751px) {  
  .container {  
    grid-template-columns: 24vw 25vw 25vw 24vw;  
    grid-template-areas:  
      "header header header header"  
      "main main . sidebar"  
      "footer footer footer footer";  
  }  
}

@media only screen and (max-width: 750px) {  
  .item-c {  
    display: none;  
  }

  .container {  
    grid-template-columns: 90vw;  
    grid-template-rows: auto;  
    grid-template-areas:  
      "header"  
      "main"  
      "footer";  
  }  
}

All we did is hide anything with class item-c when the screen width is 750px or less. Otherwise, we keep the same layout as before.

Otherwise, we just move the code around a bit to prevent duplication.

Then when our screen is narrow, we get

Conclusion

Using the CSS grid, making layouts is easier than ever.

First, we set the container div to display: grid to make it a container for a grid.

We then can define layouts with the grid-template-columns to define the columns, grid-template-rows to define the rows. This will form a grid.

Then we set the grid-template-areas attribute to define our layout one row at a time.

We can extend this to be responsive by using CSS media queries and then defining alternative layouts for different screen sizes.

Categories
JavaScript Vue

Generate Static Websites with Nuxt

Static websites are getting popular again nowadays. Informational and brochure sites no longer need to use content management systems like WordPress to be updated dynamically.

With static site generators, you can get your content from dynamic sources like headless CMS’s, APIs, and also from files like Markdown files.

Nuxt is a great static site generator based on Vue.js that is easy to use to build static websites. With Nuxt, all we have to do to build static websites from dynamic content is that we create the templates for showing the content dynamically from the dynamic sources like APIs and Markdown files. Then in the Nuxt configuration file, we define the routes statically so that it can go through the same routes to generate the content into static files.

In this article, we will build a news website using Nuxt and will use the News API, located at https://newsapi.org/, for the content. You have to know Vue.js before you can build a website using Nuxt since Nuxt is a framework based on Vue.js.

To start, first we register for an API key at the News API website. It is free if we only want the headlines. We start building the website by using the Nuxt CLI. We run the create-nuxt-app command by typing in:

npx create-nuxt-app news-website

This will create the initial project files in the news-website folder. When the wizard is run, we select none for server side frameworks, none for UI framework, none for test framework, Universal for the Nuxt mode, and choose to include Axios, linting and prettifying choices are up to you.

Next we need to install some packages. We need the @nuxtjs/dotenv package for reading the environment variables locally and the country-list library for getting a list of countries in our website. To install them we run:

npm i @nuxtjs/dotenv country-list

Now we can start building our website. In the default.vue file, we replace the existing code with:

<template>  
  <div>  
    <nav class="navbar navbar-expand-lg navbar-light bg-light">  
      <nuxt-link class="navbar-brand" to="/">News Website</nuxt-link>  
      <button  
        class="navbar-toggler"  
        type="button"  
        data-toggle="collapse"  
        data-target="#navbarSupportedContent"  
        aria-controls="navbarSupportedContent"  
        aria-expanded="false"  
        aria-label="Toggle navigation"  
      >  
        <span class="navbar-toggler-icon"></span>  
      </button> <div class="collapse navbar-collapse" id="navbarSupportedContent">  
        <ul class="navbar-nav mr-auto">  
          <li class="nav-item active">  
            <nuxt-link class="nav-link" to="/">Home</nuxt-link>  
          </li>  
          <li class="nav-item dropdown">  
            <a  
              class="nav-link dropdown-toggle"  
              href="#"  
              id="navbarDropdown"  
              role="button"  
              data-toggle="dropdown"  
              aria-haspopup="true"  
              aria-expanded="false"  
            >Headliny by Country</a>  
            <div class="dropdown-menu" aria-labelledby="navbarDropdown">  
              <nuxt-link  
                class="dropdown-item"  
                :to="`/headlines/${c.code}`"  
                v-for="(c, i) of countries"  
                :key="i"  
              >{{c.name}}</nuxt-link>  
            </div>  
          </li>  
        </ul>  
      </div>  
    </nav>  
    <nuxt />  
  </div>  
</template>

<script>  
import { requestsMixin } from "~/mixins/requestsMixin";  
const { getData } = require("country-list");

export default {  
  mixins: [requestsMixin],  
  data() {  
    return {  
      countries: getData()  
    };  
  }  
};  
</script>

<style>  
.bg-light {  
  background-color: lightcoral !important;  
}  
</style>

This is the file for defining the layout of our website. We added the Bootstrap navigation bar here. The bar has links for the home page and a drop-down for the list of countries. The nuxt-link components are all links to pages for getting the headlines for the country when the static files are generated. The countries are obtained from the country-list package in the script section by calling the getData function. In the style section, we changed the background color of our navigation bar by overriding the default color of the .bg-light class. The nuxt component in the bottom of the template section is where our content will be displayed.

Next we create amixins folder and create a file called requestsMixin.js file. In there, we add:

const APIURL = "https://newsapi.org/v2";  
const axios = require("axios");

export const requestsMixin = {  
  methods: {  
    getHeadlines(country) {  
      return axios.get(  
        `${APIURL}/top-headlines?country=${country}&apiKey=${process.env.VUE_APP_APIKEY}`  
      );  
    }, 

    getEverything(keyword) {  
      return axios.get(  
        `${APIURL}/everything?q=${keyword}&apiKey=${process.env.VUE_APP_APIKEY}`  
      );  
    }  
  }  
};

This file has the code to get the headlines by country and keyword from the News API.

Then in the pages folder, we create the headlines folder and in the headlines folder, create a _countryCode.vue file. In the file, we add:

<template>  
  <div class="container">  
    <h1 class="text-center">Headlines in {{getCountryName()}}</h1>  
    <div v-if="headlines.length > 0">  
      <div class="card" v-for="(h, i) of headlines" :key="i">  
        <div class="card-body">  
          <h5 class="card-title">{{h.title}}</h5>  
          <p class="card-text">{{h.content}}</p>  
          <button class="btn btn-primary" :href="h.url" target="_blank" variant="primary">Read</button>  
        </div>  
        <img :src="h.urlToImage" class="card-img-bottom" />  
      </div>  
    </div>  
    <div v-else>  
      <h2 class="text-center">No headlines found.</h2>  
    </div>  
  </div>  
</template><script>  
import { requestsMixin } from "~/mixins/requestsMixin";  
const { getData } = require("country-list");

export default {  
  mixins: [requestsMixin],  
  data() {  
    return {  
      headlines: [],  
      countries: getData()  
    };  
  },  
  beforeMount() {  
    this.getHeadlinesByCountry();  
  },  
  methods: {  
    async getHeadlinesByCountry() {  
      this.country = this.$route.params.countryCode;  
      const { data } = await this.getHeadlines(this.country);  
      this.headlines = data.articles;  
    }, 

    getCountryName() {  
      const country = this.countries.find(  
        c => c.code == this.$route.params.countryCode  
      );  
      return country ? country.name : "";  
    }  
  }  
};  
</script>

In the file, we accept the route parameter countryCode and from there, we call the this.getHeadlines function from the requestsMixin that we made earlier and included in this component to get the headlines from the News API. Then the results are displayed in Bootstrap cards in the template section. In the template, we get the country name by finding it from the country-list data. We display a message if there are no headlines found. In general, if we want to make a page that accepts URL parameters, we have to make a file with an underscore as the first character and the variable name of the URL parameter that we want. So _countryCode.vue will let us get the countryCode parameter by using this.$route.params.countryCode in this example.

Next in index.vue in the pages folder, we replace the exist code with:

<template>  
  <div class="container">  
    <h1 class="text-center">Home</h1>  
    <div class="card" v-for="(h, i) of headlines" :key="i">  
      <div class="card-body">  
        <h5 class="card-title">{{h.title}}</h5>  
        <p class="card-text">{{h.content}}</p>  
        <button class="btn btn-primary" :href="h.url" target="_blank" variant="primary">Read</button>  
      </div>  
      <img :src="h.urlToImage" class="card-img-bottom" />  
    </div>  
  </div>  
</template>

<script>  
import { requestsMixin } from "~/mixins/requestsMixin";  
const { getData } = require("country-list");

export default {  
  mixins: [requestsMixin],  
  data() {  
    return {  
      headlines: []  
    };  
  },  
  beforeMount() {  
    this.getHeadlinesByCountry();  
  },  
  methods: {  
    async getHeadlinesByCountry() {  
      const { data } = await this.getHeadlines("us");  
      this.headlines = data.articles;  
    }  
  }  
};  
</script>

<style>  
</style>

This lets us display the headlines for the US in the home page. It works similarly to the _countryCode.vue page except that we only get the US headlines instead of accepting a URL parameter to get headlines from different countries depending on the URL.

Next we create an create-env.js in the project’s root folder and add the following:

const fs = require('fs')  
fs.writeFileSync('./.env', `API_KEY=${process.env.API_KEY}`)

This allows us to deploy to Netlify because we need to create the .env file on the fly there from the entered environment variables. Also, we create the .env file manually and put the API_KEY as the key and the News API API key as the value.

Next in nuxt.config.js , we replace the existing code with:

require("dotenv").config();  
const { getData } = require("country-list");

export default {  
  mode: "universal",  
  /*  
   ** Headers of the page  
   */  
  head: {  
    title: "News Website",  
    meta: [  
      { charset: "utf-8" },  
      { name: "viewport", content: "width=device-width, initial-scale=1" },  
      {  
        hid: "description",  
        name: "description",  
        content: process.env.npm_package_description || ""  
      }  
    ],  
    link: [  
      { rel: "icon", type: "image/x-icon", href: "/favicon.ico" },  
      {  
        rel: "stylesheet",  
        href:  
         "https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"  
      }  
    ],  
    script: [  
      { src: "https://code.jquery.com/jquery-3.3.1.slim.min.js" },  
      {  
        src:  
          "https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js"  
      },  
      {  
        src:  
          "https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"  
      }  
    ]  
  },  
  /*  
   ** Customize the progress-bar color  
   */  
  loading: { color: "#fff" },  
  /*  
   ** Global CSS  
   */  
  css: [],  
  /*  
   ** Plugins to load before mounting the App  
   */  
  plugins: [],  
  /*  
   ** Nuxt.js dev-modules  
   */  
  buildModules: [],  
  /*  
   ** Nuxt.js modules  
   */  
  modules: [  
    // Doc: https://axios.nuxtjs.org/usage    
    "@nuxtjs/axios",  
    "@nuxtjs/dotenv"  
  ],  
  /*  
   ** Axios module configuration  
   ** See https://axios.nuxtjs.org/options
   */  
  axios: {},  
  /*  
   ** Build configuration  
   */  
  build: {  
    /*  
     ** You can extend webpack config here  
     */  
    extend(config, ctx) {}  
  },  
  env: {  
    apiKey: process.env.API_KEY || ""  
  },  
  router: {  
    routes: [  
      {  
        name: "index",  
        path: "/",  
        component: "pages/index.vue"  
      },  
      {  
        name: "headlines-id",  
        path: "/headlines/:countryCode?",  
        component: "pages/headlines/_countryCode.vue"  
      }  
    ]  
  },  
  generate: {  
    routes() {  
      return getData().map(d => `headlines/${d.code}`);  
    }  
  }  
};

In the head object, we changed the title so that we display the title we want instead of the default title. In the link section, we add the Bootstrap CSS, and in the script section, we add the Bootstrap JavaScript files and jQuery, which is a dependency of Bootstrap. Since we want to build a static site, we cannot use BootstrapVue because it is dynamic. We do not want any dynamic JavaScript in the generated output, so we have to use plain Bootstrap. In the modules section, we added ”@nuxtjs/dotenv” to read the environment variables from the .env file that we created into our Nuxt app. We also added require(“dotenv”).config(); so that we get the process.env.API_KEY which can be added to this configuration file. We have to do this so that we don’t have to check in our .env file. In the env section, we have the apiKey: process.env.API_KEY || “”, which is what we get by reading the API KEY in the .env file with dotenv.

In the router section, we define the dynamic routes so that they can be viewed when users click on links with the given URLs or click on a link with such URLs. Nuxt also uses these routes to generate static files. In the generate section, we define the routes that Nuxt will traverse to generate the static files for the static website. In this case, the array of routes consist of routes for the headlines page that we created earlier. It will loop through them to get the data for them, then render them and generate the file from the rendered results. The folder structure will correspond to the routes. So since our path is /headlines/:countryCode , the generated artifact will have the headlines folder along withe all the country code as names of subfolders, and inside each folder there will be a index.html with the rendered content.

Now we are ready to deploy our website to Netlify. Create a Netlify account by going to https://www.netlify.com/. The free plan will work for our needs. Then commit your code to a Git repository hosted on GitHub, Gitlab or Bitbucket. Then when you log in to Netlify, click on New site from Git. From there, you can add your Git repository that’s hosted in one of those services. Then when you’re asked to enter the Build Command, enter node ./create-env.js && npm run generate, and the Publish directory would be dist .

After that, enter the API Key in the .env file into the Environment variables section of the website settings, which you can go to by clicking on the Environment link on the Build & deploy menu. Enter API_KEY as the key and your News API API key as the value. Then click the save button.

Once you commit and push everything in a Git repository hosted by GitHub, Gitlab or Bitbucket, Netlify will build and deploy automatically.

Categories
JavaScript JavaScript Basics

How to Check if a Variable is a Number

We can check if a variable is a number in multiple ways.

isNaN

We can check by calling isNaN with the variable as the argument. It also detects if a string’s content is a number. For example:

isNaN(1) // false  
isNaN('1') // false  
isNaN('abc') // true

Note: isNaN(null) is true .

typeof Operator

We can use the typeof operator before a variable to check if it’s a number, like so:

typeof 1 == 'number' // true  
typeof '1' == 'number' // false