Categories
Nodejs

How to Scrape Websites with Node.js

Web scraping is a technique where you extract data from a website by parsing the HTML. This is done by loading the website’s content into a DOM tree, parsing the elements, extracting data from the desired elements, and storing the results in a database.

In this article, we will make a web scraper that fetches listings from a deals website called Red Flag Deals and stores the data in a database. We will have a backend that manages a queue of scrape requests that we will call “jobs”. We provide the ability add, view, and delete jobs. Then we create a frontend to let us view the data we scraped. The web scraper will be a Node.js script. The backend will be an Express app with Sequelize as the ORM. We will use SQLite as our database to keep things simple for this demonstration app. The frontend will be written with React.

Web Scraper Backend

We will start with the backend. Create a project folder and within it, create a backend folder for our backend code. Next, we run Express Generator by executing npx express-generator. This will create the files we need for the base Express app. Now we install the packages for our backend app by running npm install.

Once the packages are installed, we will add Babel to the app so that we can use features of JavaScript that aren’t available in Node.js such as import.

Run npm i @babel/cli @babel/core @babel/node @babel/preset-env. Next create a file called .babelrc in the backend folder and add the following:

{
    "presets": [
        "@babel/preset-env"
    ]
}

In package.json, we add the following to the scripts section:

"start": "nodemon --exec npm run babel-node --  ./bin/www",
"babel-node": "babel-node"

We start our app with babel-node runtime instead of the normal Node runtime so that we can use Babel. Note that we also need nodemon so that the app will restart when files are changed. To install it, run npm i -g nodemon.

Next we need to install our own libraries. We will use the Bull package for creating a job queue for scraping data, CORS for allowing requests from the frontend, Request and Request Promise for fetching the website text, Sequelize for the ORM, and SQLite3 for storing data. We install the packages by running npm i cors bull cheerio request request-promise sequelize sqlite3.

We also need Redis for our Bull job queue to work. The latest version only runs on Linux so we have to use it. In Ubuntu, we run:

sudo apt-get update
sudo apt-get upgrade
sudo apt-get install redis-server
redis-server

This installs and runs Redis. Detailed instructions are at https://tecadmin.net/install-redis-ubuntu/.

Next we add the code for Sequelize to our backend. Run npx sequelize-cli init. In the config.json, replace the code with the following:

{
  "development": {
    "dialect": "sqlite",
    "storage": "development.db"
  },
  "test": {
    "dialect": "sqlite",
    "storage": "test.db"
  },
  "production": {
    "dialect": "sqlite",
    "storage": "production.db"
  }
}

Then we need to create migrations and models for our database to store our data. Run npx sequelize-cli model:create --name Deal --attributes name:string,description:string,url:string,content:text. Running this will create a migration and the associated model.

When you run npx sequelize-cli db:migrate you will get the Deals table in your SQLite database.

With the database code done, we can start writing the logic. Create a queues folder in the backend folder and add a file called scraperQueue.js and add the following:

const Queue = require("bull");
const cheerio = require("cheerio");
const rp = require("request-promise");
const models = require("../models");

const scraperQueue = new Queue("web scraping");

scraperQueue.process(async (job, done) => {
  const deal = job.data.deal;
  const response = await rp(deal.url);
  const $ = cheerio.load(response);
  const items = $(".list_item");
  let deals = [];
  Object.keys(items).forEach(key => {
    const dealer = $(items[key])
      .find(".offer_dealer")
      .text();
    const title = $(items[key])
      .find(".offer_title")
      .text();

    let link = $(items[key])
      .find(".offer_title a")
      .attr("href");
    link = `https://www.redflagdeals.com${link}`;
    deals.push({ dealer, title, link });
  });
  await models.Deal.update(
    {
      content: JSON.stringify(deals),
    },
    {
      where: {
        id: deal.id,
      },
    }
  );
  done();
});

export { scraperQueue };

This is the queue for our web scraping jobs. What we are doing in this file is loading the web page we want to scrape, which is the Red Flag Deals deals page. We open the response of the page with Cheerio by loading the HTML into the DOM tree, locate the items with the class list-item and then iterate through each element and extract the text from the offer_dealer and offer_title classes. For the link, we load the element into the DOM tree with Cheerio and then get the a element from the node with the class offer_title. Then get the href attribute and add the URL prefix and root URL to it since it’s a relative path.

This means if the page structure changes, for example, when the class names change, then you have to update the code to keep it working.

After getting the data, we save it with Sequelize to the job entry we created.

Next we create the API endpoints to add, get, and delete the data. Create a file called deals.js in the routes folder and add the following:

const express = require("express");
const models = require("../models");
const router = express.Router();
import { scraperQueue } from "../queues/scraperQueue";

router.post("/scrape", async (req, res, next) => {
  const { name, description, url } = req.body;
  const deal = await models.Deal.create({
    name,
    description,
    url,
  });
  scraperQueue.add({ deal });
  res.json(deal);
});

router.delete("/job/:id", async (req, res, next) => {
  const id = req.params.id;
  await models.Deal.destroy({ where: { id } });
  res.json({ id });
});

router.get("/jobs", async (req, res, next) => {
  const deals = await models.Deal.findAll();
  res.json(deals);
});

router.get("/job/:id", async (req, res, next) => {
  const id = req.params.id;
  const deals = await models.Deal.findAll({ where: { id } });
  const deal = deals[0];
  deal.content = JSON.parse(deal.content);
  res.json(deal);
});

module.exports = router;

We have the scrape route to add the job to the queue and save the data to the Deals table. The delete /job/:id route is used to delete data from the Deals table. The /jobs table is for getting the jobs, and the get/job/:id route is for returning the deals data to the user.

Finally we have to make some changes to app.js. We replace the existing code with the following:

const createError = require("http-errors");
const express = require("express");
const path = require("path");
const cookieParser = require("cookie-parser");
const logger = require("morgan");
const cors = require("cors");

const indexRouter = require("./routes/index");
const dealsRouter = require("./routes/deals");

const app = express();

// view engine setup
app.set("views", path.join(__dirname, "views"));
app.set("view engine", "jade");

app.use(logger("dev"));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, "public")));
app.use(cors());

app.use("/", indexRouter);
app.use("/deals", dealsRouter);

// catch 404 and forward to error handler
app.use(function(req, res, next) {
  next(createError(404));
});

// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get("env") === "development" ? err : {};

// render the error page
  res.status(err.status || 500);
  res.render("error");
});

module.exports = app;

We add the cors add-on to let the app talk to front end, and we added const dealsRouter = require(“./routes/deals”); and app.use(“/deals”, dealsRouter); to expose the deals routes.

This finishes the backend of our web scraping app. Now we need the frontend to add or remove jobs and display the results of the web scraping jobs.

Web Scraper Frontend

We start by creating a React project with Create React App. Create the app by going to the project’s root folder and run npx create-react-app frontend.

Next we need to install some packages. Run npm i axios bootstrap formik mobx mobx-react react-bootstrap react-router-dom yup. Axios is our HTTP client for making requests to our backend. Bootstrap and React Bootstrap are used for styling, MobX and MobX React are for state management, and Formik and Yup are for handling form value changes and form validation respectively.

Now we add and modify files in the src folder unless otherwise mentioned.

First we replace the code in App.js with the following:

import React from "react";
import { Router, Route, Link } from "react-router-dom";
import HomePage from "./HomePage";
import ResultsPage from "./ResultsPage";
import TopBar from "./TopBar";
import { createBrowserHistory as createHistory } from "history";
import { JobsStore } from "./stores";
import "./App.css";
const history = createHistory();
const jobsStore = new JobsStore();

function App() {
  return (
    <div className="App">
      <Router history={history}>
        <TopBar />
        <Route
          path="/"
          exact
          component={props => <HomePage {...props} jobsStore={jobsStore} />}
        />
        <Route path="/results/:id" exact component={ResultsPage} />
      </Router>
    </div>
  );
}

export default App;

We have the TopBar component which displays our top bar which we will create soon. Below that we have the routes for our pages which we will create. Note that we pass in our store to the components in the route. This is the MobX store which will persist the list of jobs for easy access.

In App.css we replace the existing code with:

.center {
    text-align: center;
}

Next we will build the home page. Create a file called HomePage.js and add the following:

import React from "react";
import { useEffect, useState } from "react";
import { Formik } from "formik";
import Form from "react-bootstrap/Form";
import Col from "react-bootstrap/Col";
import Button from "react-bootstrap/Button";
import * as yup from "yup";
import Table from "react-bootstrap/Table";
import { Redirect } from "react-router";
import "./HomePage.css";
import { observer } from "mobx-react";
import { scrape, removeJob, getAllJobs } from "./requests";

const schema = yup.object({
  name: yup.string().required("Name is required"),
  description: yup.string().required("Description is required"),
  url: yup.string().required("URL is required"),
});

function HomePage({ jobsStore }) {
  const [redirect, setRedirect] = useState(false);
  const [jobId, setJobId] = useState(0);
  const [initialized, setInitialized] = useState(false);
  const handleSubmit = async evt => {
    const isValid = await schema.validate(evt);
    if (!isValid) {
      return;
    }
    jobsStore.jobs.push(evt);
    jobsStore.setJobs(jobsStore.jobs);
    await scrape(evt);
    getJobs();
  };

  const selectJob = id => {
    setJobId(id);
    setRedirect(true);
  };

  const getJobs = async () => {
    const response = await getAllJobs();
    jobsStore.setJobs(response.data);
  };

  const deleteJob = id => {
    removeJob(id);
    getJobs();
  };

  useEffect(() => {
    if (!initialized) {
      getJobs();
      setInitialized(true);
    }
  });

  if (redirect) {
    return <Redirect to={`/results/${jobId}`} />;
  }

  return (
    <div className="home-page">
      <h1>Web Scraping Jobs</h1>
      <Formik
        validationSchema={schema}
        onSubmit={handleSubmit}
      >
        {({
          handleSubmit,
          handleChange,
          handleBlur,
          values,
          touched,
          isInvalid,
          errors,
        }) => (
          <Form noValidate onSubmit={handleSubmit}>
            <Form.Row>
              <Form.Group as={Col} md="12" controlId="name">
                <Form.Label>Name</Form.Label>
                <Form.Control
                  type="text"
                  name="name"
                  placeholder="Name"
                  value={values.name || ""}
                  onChange={handleChange}
                  isInvalid={touched.name && errors.name}
                />
                <Form.Control.Feedback type="invalid">
                  {errors.name}
                </Form.Control.Feedback>
              </Form.Group>
              <Form.Group as={Col} md="12" controlId="chatRoomName">
                <Form.Label>Description</Form.Label>
                <Form.Control
                  type="text"
                  name="description"
                  placeholder="Description"
                  value={values.description || ""}
                  onChange={handleChange}
                  isInvalid={touched.description && errors.description}
                />

                <Form.Control.Feedback type="invalid">
                  {errors.description}
                </Form.Control.Feedback>
              </Form.Group>

              <Form.Group controlId="url" as={Col} md="12">
                <Form.Label>Website</Form.Label>
                <Form.Control
                  as="select"
                  name="url"
                  value={values.url || ""}
                  onChange={handleChange}
                  isInvalid={touched.url && errors.url}
                >
                  <option value="">Select</option>
                  <option value="https://www.redflagdeals.com/in/toronto/deals">
                    Red Flag Deals
                  </option>
                </Form.Control>
                <Form.Control.Feedback type="invalid">
                  {errors.url}
                </Form.Control.Feedback>
              </Form.Group>
            </Form.Row>
            <Button type="submit" style={{ marginRight: "10px" }}>
              Add
            </Button>
          </Form>
        )}
      </Formik>
      <br />
      <Table striped bordered>
        <thead>
          <tr>
            <th>Name</th>
            <th>Description</th>
            <th>URL</th>
            <th></th>
            <th></th>
          </tr>
        </thead>
        <tbody>
          {jobsStore.jobs.map((j, i) => {
            return (
              <tr key={i}>
                <td>{j.name}</td>
                <td>{j.description}</td>
                <td>{j.url}</td>
                <td>
                  <Button onClick={selectJob.bind(this, j.id)}>Results</Button>
                </td>
                <td>
                  <Button onClick={deleteJob.bind(this, j.id)}>Delete</Button>
                </td>
              </tr>
            );
          })}
        </tbody>
      </Table>
    </div>
  );
}

export default observer(HomePage);

This page has the form for adding the jobs. We validate that all fields are required with Yup, which we use to create the schema object. We nest our Form component, which is provided by React Bootstrap with the Formik component so that it will handle all the form value changes and return the entered values in the handleSubmit function. We display the form validation errors with the Form.Control.Feedback components. When the user clicks “Add”, the handleSubmit function is executed.

In the handleSubmit function will validate the form values by running the schema.validate function, which returns a promise. Once that is executed and the validation passes, then we call thescrape function in requests.js, which we will create to add the jobs to the queue. After that, it will get the latest jobs by calling getJobs in our component, put the returned entries into our jobsStore and display the items in the table at the bottom.

We need the useEffect callback to call getJobs during the first load of the component so that we can populate the table when the page loads. We do this by checking the initialized flag. If it’s false, the getJobs runs, the initialized will be set to true by calling setInitialized(true) .

We have a button to let us view the jobs with the Result button, and we have a Delete button where we call the removeJob function from requests.js to make the API request to delete the job.

For styling the HomePage component, create HomePage.css and add:

.home-page {
  padding: 20px;
}

Next we writethe code for the requests. Create requests.js and add:

const APIURL = "http://localhost:3000";
const axios = require("axios");

export const getAllDeals = id => axios.get(`${APIURL}/deals/job/${id}`);

export const getAllJobs = () => axios.get(`${APIURL}/deals/jobs`);

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

export const removeJob = id => axios.delete(`${APIURL}/deals/job/${id}`);

Next we create a page to display the results. Create a file called ResultsPage.js and add:

import React, { useState, useEffect } from "react";
import { withRouter } from "react-router-dom";
import { getAllDeals } from "./requests";
import Card from "react-bootstrap/Card";
import "./ResultsPage.css";

function ResultsPage({ match: { params } }) {
  const [deals, setDeals] = useState([]);
  const [name, setName] = useState("");
  const [initialized, setInitialized] = useState(false);

  const getDeals = async () => {
    const id = params.id;
    const response = await getAllDeals(id);
    setDeals(response.data.content);
    setName(response.data.name);
  };

  useEffect(() => {
    if (!initialized) {
      getDeals();
      setInitialized(true);
    }
  });

  return (
    <div className="results-page">
      <h1 className="center">Deals Results: {name}</h1>
      {deals.map((d, i) => {
        return (
          <Card key={i}>
            <Card.Title className="title">{d.dealer}</Card.Title>
            <Card.Body>
              {d.title}
              <br />
              <a className="btn btn-primary" href={d.link}>
                Go
              </a>
            </Card.Body>
          </Card>
        );
      })}
    </div>
  );
}

export default withRouter(ResultsPage);

We get the web scrape job entry by ID and display the results in a list. We get the ID by checking for the ID parameter in the route. When we wrap ResultsPage with the withRouter function, we get the query parameter in the props.

For styling, we create a file called ResultsPage.css and add:

.results-page {
  padding: 20px;
}

.title {
  margin: 0 20px;
}

Now we need to create the MobX store to store the jobs data. Create a file stores.js and add:

import { observable, action, decorate } from "mobx";

class JobsStore {
  jobs = [];

setJobs(jobs) {
    this.jobs = jobs;
  }
}

JobsStore = decorate(JobsStore, {
  jobs: observable,
  setJobs: action,
});

export { JobsStore };

The setJobs is used in HomePage.js to set the jobs, and we accessed the list of jobs stored in the jobs field by using jobsStore.jobs since we passed in an instance of this as a prop to HomePage .

Finally, we create the top bar for our page to display the title and link for the app. Create TopBar.js and add:

import React from "react";
import Navbar from "react-bootstrap/Navbar";
import Nav from "react-bootstrap/Nav";
import NavDropdown from "react-bootstrap/NavDropdown";
import "./TopBar.css";
import { withRouter } from "react-router-dom";

function TopBar({ location }) {
  const { pathname } = location;

  return (
    <Navbar expand="lg" variant="dark">
      <Navbar.Brand href="#home">Web Scraper App</Navbar.Brand>
      <Navbar.Toggle aria-controls="basic-navbar-nav" />
      <Navbar.Collapse id="basic-navbar-nav">
        <Nav className="mr-auto">
          <Nav.Link href="/" active={pathname == "/"}>
            Home
          </Nav.Link>
        </Nav>
      </Navbar.Collapse>
    </Navbar>
  );
}

export default withRouter(TopBar);

This just displays the Navbar provided by React Bootstrap with the link to the home page. If we are on the home page, then the link will be highlighted. This is the purpose of the active property.

Next we create TopBar.css and add:

nav {
    background-color: green!important;
}

Finally, in index.html in the public folder, 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>Web Scraper 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>

We added the Bootstrap styles and change the title.

Now that the hard work is done. We can run the app.

First run the backend app by running npm start from the backend folder. Next go to the frontend folder and run npm start. When it asks you if you want it to run it from a different port, type yes.

Categories
JavaScript

Common Mistakes Developers Made While Writing JavaScript Apps

Every developer make mistake some time when writing apps. This is a normal part of the development process. There are some mistakes that are made more often than others. It is good to be aware of them so that they can be corrected before it’s made. Once we can do that then we can develop apps a lot easier and become a proficient JavaScript developer since we can develop apps that makes less bugs. We will go through some of the more common bugs that are made so we can avoid them here

Confusing the Equals

In JavaScript, single equal, double equal, and tripe equal all have their own meanings. Single equal sign is the assignment operator which assigns the value on the right to the variable to the left. Double equal sign is the equality check operator which checks an object’s content for equality without checking its type. The triple equals operator checks an object’s content and the type for equality.

We use the single equal assignment operator like the following:

let a = 1

This is used for setting variable values, so it’s we use single equal in a place that shouldn’t be used like in:

if (a = 1){ ... }

then we would be assigning 1 to a which is always true . This means that the code inside the if statement always runs. That’s definitely not what we want.

Double equal sign shouldn’t be too too much since it doesn’t check the type of the object when checking for equality, so something like:

1 == '1'

if true . This creates problems because if we are only checking for number 1 and we get string with 1 in it, it’s still true. We want to convert them to the same type and then compare them with === to check for equality to avoid ambiguity. So if we want to compare if all forms of 1 are equal to each other we convert them to number first by using:

Number(1) === Number('1')

1 wouldn’t be equal to '1' is we use === since they are different types. This would make things a lot more clear. If we want to make sure something aren’t equal to each other then we can use !== to check if they don’t have the same content or the same type, so:

1 !== '1'

would be true .

Mismatching Brackets

Nested statements and functions in each other means that there’re multiple levels of brackets in each file. Usually, apps are pretty complex so the levels can add up. This means that mismatching brackets is easy if you use a text editor that doesn’t support syntax highlight or check for mismatched brackets. This can easily be avoided with modern text editors such as Visual Studio Code, Atom, and Sublime. If we want to use simpler text editors, then use linters and code formatting tools like ESLint and Prettier to detect these issues. They also let us format code automatically and detect common style issues that may occur like quote style inconsistencies, number of characters in a line, functions that can be shortened, etc.

Mismatching Quotes

JavaScript lets us use single quotes, double quotes, and backticks for strings. They are equivalent. However, we should open and close with the same character. So if we open a string with single quote then use single quote to close a string, and if we started with double quotes or backticks, then use those to close the string respectively. Also, special characters like quotation marks have to escape to be included in a string in some cases. If you open a string with single quotes, then if you use single quotes in a string, then you have to escape that to include it in the string. This also applies to double quotes and backticks. If you use double quotes in a double quoted string then you have to escape it. And if you use a backtick in a template string, then you have to escape the backtick character.

Parentheses

In if statements, parentheses always have to wrap around the whole condition. For example, something like:

if (x > y) && (y < 10) {...}

won’t work. The correct way to write that statement is to write is:

if ((x > y) && (y < 10)) {...}

if we want to check that both conditions are true.

Semicolon

Semicolons are optional at the end of a JavaScript line. However, to make things clear for other people, we should include it so that people know where a line ends. We can use a code linter or formatter like ESLint or Prettier to do this automatically. Lots of text editors also have add-ons to fix it automatically for you.

Capitalization

JavaScript code is case-sensitive, so things that have different capitalization will be considered different even if they have the same spelling. Anything that references them also have to be capitalized correctly. For example, getElementsByTagName should always be spelled this way with exact same capitalization. The general convention in JavaScript is that we spell variables and function names with camel case, so we should stick with that for consistency if we define our own variables.

Referencing Code That’s not Yet Loaded

It’s important that we reference code that have been defined and loaded before hand in order to use the code. If we are using script tag then before we reference the code, we have to load the code first. So the script tag that references the script with the variable we want to use should come before the script tag that references the script that has the code that we want to reference.

Fortunately, JavaScript modules are much more common nowadays, so that we just have to remember to export the code that you want to use in another module and the import the exported variable in the module that you want to use the variable before referencing it. JavaScript modules solves a lot problems with script tags. Global variables do not have to used much anymore to let different scripts reference the same variable, and also we can make variables private without the use of closures since we only need to export the ones that are needed elsewhere.

Variable Names

In JavaScript, there are many reserved words that cannot be used as variable names. They shouldn’t be used as variable names since they collide the name of the reserved words, creating confusion with the JavaScript interpreter. To avoid this we should use names that are descriptive like firstName , lastName , and things like that.

Scope

We shouldn’t use global variables whenever possible. This means that all variables we declare should use the let or const keywords for variables and constants respectively. This avoids issues with global variable scope and overwriting global variable data. It also let us avoid errors from assigning to entities that should be constant or assigning things accidentally that are global.

Missing or Unexpected Parameters

When functions are called with expected parameters missing, then unexpected results may be returned because you passed in unexpected input to a function.We have to make sure that we pass in the right parameters to a function so that we get the right results. It also a good idea to check for unexpected parameters if the function is used in lots of places like functions that are in libraries so that we can deal with unexpected parameters gracefully like ending the execution of the function when parameters passed in are of an invalid type or null or undefined .

Index Errors

JavaScript arrays starts with index 0, so the last index of an array will always be 1 less than the length of the array. When you pass in an index that is beyond the length or it’s invalid like a negative number, we get undefined , so we will get ReferenceErrors if we try to get the invalid entry and do stuff with it.

Null and Undefined

null and undefined are things that are often the values of variables. This means that we have to check for these when we are getting something by traversing the nested properties of objects or looping through an array. When we have null or undefined we want to handle them gracefully. This means that we should always check for them. So we if have an object called foo and we want to access deeply nested properties within it, then we should check for null or undefined in each level, like so:

if (foo && foo.bar && foo.bar.baz){ ... }

This way we know that all the properties are defined so that we can get access the baz property. For arrays if we want to access index i , we should check if it’s defined by writing:

if (arr[i]){...}

so that we won’t get unexpected undefined errors.

The optional chaining operator (?. ) is being worked on, so hopefully it will be finalized soon and we can use it to access deeply nested properties in an object and we don’t have to deal with properties being undefined as much anymore. Also, Lodash has a get function, which can try to access deeply properties of arrays and objects and get the value if it exists or return null if there’s anything undefined in the middle of the object’s hierarchy. This is much better than a long chain of undefined or null checks.

Categories
JavaScript Answers

How to Remove Only the Parent Element and not its Child Elements in JavaScript?

To remove only the parent element and not its child elements in JavaScript, we can create a new fragment that has the element itself removed but with the children kept.

Then we can replace the element with the fragment which has the child elements.

For instance, if we have:

<div>  
  pre text  
  <div class="remove-just-this">  
    <p>child foo</p>  
    <p>child bar</p>  
    nested text  
  </div>  
  post text  
</div>

and we want to remove the div with class remove-just-this , then we can write:

const fragment = document.createDocumentFragment();  
const element = document.querySelector('.remove-just-this')  
while (element.firstChild) {  
  fragment.appendChild(element.firstChild);  
}  
element.parentNode.replaceChild(fragment, element);

We call document.createDocumentFragment to create a fragment.

Then we get the div with class remove-just-this with document.querySelector .

Then we get the children of the div and append them to the fragment with:

while (element.firstChild) {  
  fragment.appendChild(element.firstChild);  
}

And finally, we call element.parentNode.replaceChild to replace the element with the fragment .

Now we get:

<div>  
  pre text  
    
    <p>child foo</p>  
    <p>child bar</p>  
    nested text  
    
  post text  
</div>

as a result.

Categories
JavaScript Answers

How to Detect When a YouTube Video Finishes Playing with JavaScript?

To detect when a YouTube video finishes playing with JavaScript, we can set the events.onReady and events.onStateChange methods in the object we pass into the YT.Player constructor.

For instance, we can write the following HTML:

<div id="player"></div>

Then we can add a video into the div with the YouTube API by writing:

const tag = document.createElement("script");
tag.id = "iframe-demo";
tag.src = "https://www.youtube.com/iframe_api";
const [firstScriptTag] = document.getElementsByTagName("script");
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
let player;

window.onPlayerReady = (event) => {
  event.target.playVideo();
};

window.onPlayerStateChange = (event) => {
  if (event.data === 0) {
    console.log("done");
  }
};

window.onYouTubeIframeAPIReady = () => {
  player = new window.YT.Player("player", {
    width: 200,
    height: 100,
    videoId: "M7lc1UVf-VE",
    events: {
      onReady: window.onPlayerReady,
      onStateChange: window.onPlayerStateChange
    }
  });
};

We add the YouTube script with:

const tag = document.createElement("script");
tag.id = "iframe-demo";
tag.src = "https://www.youtube.com/iframe_api";
const [firstScriptTag] = document.getElementsByTagName("script");
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);

Then we add the onPlayerReady method with:

window.onPlayerReady = (event) => {
  event.target.playVideo();
};

to play the video when the player is ready.

Then we have the onPlayerStateChange method that checks if event.data is 0.

If it is, then we know the video is done playing.

And finally, we have the onYouTubeIframeAPIReady method to create a YouTube player with the YT.Player constructor.

'player' is the ID of the div that houses the video.

width and height are the width and height.

videoId is the ID of the YouTube video to play.

And the events property have the event listeners for when the video player is ready and when the video player state changes, which are onReady and onStateChange respectively.

Categories
JavaScript Answers

How to Pick a Random Property from a JavaScript Object?

To pick a random property from a JavaScript object, we can use the Object.keys method to get the keys of the object in an array.

Then we can use the Math.random and Math.floor methods to get a random index in the keys array.

For instance, we can write:

const animals = {
  'cat': 'meow',
  'dog': 'woof',
  'cow': 'moo',
  'sheep': 'baaah',
  'bird': 'tweet'
};

const keys = Object.keys(animals)
const prop = keys[Math.floor(Math.random() * keys.length)]
console.log(prop);

We have the animals object that we want to get a random key from.

Then we get the keys with the Object.keys method.

Next, we get a random array index of in keys with:

Math.floor(Math.random() * keys.length)

And we pass that into the square brackets to get the key from animals .

If we want to get the values, we just replace Object.keys with Object.values by writing:

const animals = {
  'cat': 'meow',
  'dog': 'woof',
  'cow': 'moo',
  'sheep': 'baaah',
  'bird': 'tweet'
};

const values = Object.values(animals)
const prop = values[Math.floor(Math.random() * values.length)]
console.log(prop);