Categories
React Answers

How to Play an MP3 Clip on Click in React?

Sometimes, we want to play an mp3 clip on click in React.

In this article, we’ll look at how to play an mp3 clip on click in React.

Play an MP3 Clip on Click in React

To play an mp3 clip on click in React, we can use the Audio constructor to create an audio element with the MP3 file URL.

Then we call play on the created object to play the audio clip.

For instance, we write:

import React from "react";

export default function App() {
  const audio = new Audio(
    "https://file-examples-com.github.io/uploads/2017/11/file_example_MP3_700KB.mp3"
  );

  const start = () => {
    audio.play();
  };

  return (
    <div>
      <button onClick={start}>Play</button>
    </div>
  );
}

We create the audio object with the Audio constructor with the MP3 file URL as its argument.

Then we call audio.play in the start function to play the audio clip when we click Play since we set start as the value of the onClick prop as the button.

Conclusion

To play an mp3 clip on click in React, we can use the Audio constructor to create an audio element with the MP3 file URL.

Then we call play on the created object to play the audio clip.

Categories
React Answers

How to Embed a YouTube Video into a React App?

To embed a YouTube video into a React app, we can add an iframe into a React component with the embed video URL as the value of the src prop.

For instance, we write:

import React from "react";

export default function App() {
  return (
    <div>
      <iframe
        src="https://www.youtube.com/embed/C0DPdy98e4c"
        frameborder="0"
        allow="autoplay; encrypted-media"
        allowfullscreen
        title="video"
      />{" "}
    </div>
  );
}

We set src to the URL of the video we want to display.

frameborder is set to 0 to remove the iframe’s border.

allowfullscreen lets the user make the video full screen.

Categories
React

How to Add Copy to Clipboard Feature to Your React App

Copy to clipboard feature is a popular convenience feature for web apps like password managers, where it is inconvenient for people to highlight text and then copy it. It is an easy feature to add to your own web app.

In this article, we will build a password manager that lets you enter, edit and delete password to the websites the user goes to and let them copy their username and password to the clip to use them anywhere they like. We will use React to build the app.

To start, we will run Create React App to create the app. Run:

npx create-react-app password-manager

to create the app. Next, we add our own libraries, we will use Axios for making HTTP requests to our back end, Formik and Yup for form value handling and form validation respectively, MobX for state management, React Bootstrap for styling, React-Copy-To-Clipboard for letting us copy data to the clipboard, and React Router for routing.

We install them by running:

npm i axios formik mobx mobx-react react-bootstrap react-copy-to-clipboard react-router-dom yup

With all the libraries installed, we can start building our app. We create all the files in the src folder unless otherwise specified.

First, we replace the existing code in App.css with:

.bg-primary {
  background-color: #09d3ac !important;
}

to change the top bar’s background color. Next in App.js , replace the current code with:

import React from "react";
import { Router, Route } 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({ passwordsStore }) {
  return (
    <div className="App">
      <Router history={history}>
        <Navbar bg="primary" expand="lg" variant="dark">
          <Navbar.Brand href="#home">Password Manager</Navbar.Brand>
          <Navbar.Toggle aria-controls="basic-navbar-nav" />
          <Navbar.Collapse id="basic-navbar-nav">
            <Nav className="mr-auto">
              <Nav.Link href="/" active>Home</Nav.Link>
            </Nav>
          </Navbar.Collapse>
        </Navbar>
        <Route
          path="/"
          exact
          component={props => (
            <HomePage {...props} passwordsStore={passwordsStore} />
          )}
        />
      </Router>
    </div>
  );
}

export default App;

to add our React Bootstrap top bar and our route to the home page. passwordStore is our MobX store for storing our password list in the front end.

Next, create HomePage.css and add:

.home-page {
  padding: 20px;
}

to add some padding to our page.

Then 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 PasswordForm from "./PasswordForm";
import "./HomePage.css";
import { deletePassword, getPasswords } from "./requests";
import { observer } from "mobx-react";
import { CopyToClipboard } from "react-copy-to-clipboard";

function HomePage({ passwordsStore }) {
  const [openAddModal, setOpenAddModal] = useState(false);
  const [openEditModal, setOpenEditModal] = useState(false);
  const [initialized, setInitialized] = useState(false);
  const [selectedPassword, setSelectedPassword] = useState({});

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

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

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

  const editPassword = contact => {
    setSelectedPassword(contact);
    setOpenEditModal(true);
  };

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

  const getData = async () => {
    const response = await getPasswords();
    passwordsStore.setPasswords(response.data);
    setInitialized(true);
  };

  const deleteSelectedPassword = async id => {
    await deletePassword(id);
    getData();
  };

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

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

      <Modal show={openEditModal} onHide={closeModal}>
        <Modal.Header closeButton>
          <Modal.Title>Edit Password</Modal.Title>
        </Modal.Header>
        <Modal.Body>
          <PasswordForm
            edit={true}
            onSave={closeModal.bind(this)}
            contact={selectedPassword}
            onCancelEdit={cancelEditModal}
            passwordsStore={passwordsStore}
          />
        </Modal.Body>
      </Modal>
      <ButtonToolbar onClick={openModal}>
        <Button variant="outline-primary">Add Password</Button>
      </ButtonToolbar>
      <br />
      <div className="table-responsive">
        <Table striped bordered hover>
          <thead>
            <tr>
              <th>Name</th>
              <th>URL</th>
              <th>Username</th>
              <th>Password</th>
              <th></th>
              <th></th>
              <th></th>
              <th></th>
            </tr>
          </thead>
          <tbody>
            {passwordsStore.passwords.map(c => (
              <tr key={c.id}>
                <td>{c.name}</td>
                <td>{c.url}</td>
                <td>{c.username}</td>
                <td>******</td>
                <td>
                  <CopyToClipboard text={c.username}>
                    <Button variant="outline-primary">
                      Copy Username to Clipboard
                    </Button>
                  </CopyToClipboard>
                </td>
                <td>
                  <CopyToClipboard text={c.password}>
                    <Button variant="outline-primary">
                      Copy Password to Clipboard
                    </Button>
                  </CopyToClipboard>
                </td>
                <td>
                  <Button
                    variant="outline-primary"
                    onClick={editPassword.bind(this, c)}
                  >
                    Edit
                  </Button>
                </td>
                <td>
                  <Button
                    variant="outline-primary"
                    onClick={deleteSelectedPassword.bind(this, c.id)}
                  >
                    Delete
                  </Button>
                </td>
              </tr>
            ))}
          </tbody>
        </Table>
      </div>
    </div>
  );
}
export default observer(HomePage);

This component is the home page of our app. We have a table to display the list of passwords, a button to add a login and password entry, and buttons in each row of the table to copy username and password, and edit and delete each entry. We have the name, URL, username and password columns. The CopyToClipboard component allows us to copy the data we copy to the text prop of the component. Any component can be inside this component. We have one React Bootstrap modal for add a password and another one for edit. PasswordForm is our form for adding the password entries, which we will create later.

We have the openModal , closeModal , cancelAddModal , and cancelEditModal functions to open and close the modals. In the editPassword function, we call the setSelectedPassword function to set the password entry to be edited.

The observer we wrap around the HomePage component is for letting us watch the latest values from passwordsStore .

Next, we modify index.js to have:

import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import * as serviceWorker from "./serviceWorker";
import { PasswordsStore } from "./store";
const passwordsStore = new PasswordsStore();

ReactDOM.render(
  <App passwordsStore={passwordsStore} />,
  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();

We pass in our PasswordStore MobX store here, which will pass it to all the other components.

Next, we create PasswordForm.js and add:

import React 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 PropTypes from "prop-types";
import { addPassword, getPasswords, editPassword } from "./requests";

const schema = yup.object({
  name: yup.string().required("Name is required"),
  url: yup
    .string()
    .url()
    .required("URL is required"),
  username: yup.string().required("Username is required"),
  password: yup.string().required("Password is required")
});

function PasswordForm({
  edit,
  onSave,
  contact,
  onCancelAdd,
  onCancelEdit,
  passwordsStore
}) {
  const handleSubmit = async evt => {
    const isValid = await schema.validate(evt);
    if (!isValid) {
      return;
    }
    if (!edit) {
      await addPassword(evt);
    } else {
      await editPassword(evt);
    }
    const response = await getPasswords();
    passwordsStore.setPasswords(response.data);
    onSave();
  };

  return (
    <>
      <Formik
        validationSchema={schema}
        onSubmit={handleSubmit}
        initialValues={contact || {}}
      >
        {({
          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="url">
                <Form.Label>URL</Form.Label>
                <Form.Control
                  type="text"
                  name="url"
                  placeholder="URL"
                  value={values.url || ""}
                  onChange={handleChange}
                  isInvalid={touched.url && errors.url}
                />
                <Form.Control.Feedback type="invalid">
                  {errors.url}
                </Form.Control.Feedback>
              </Form.Group>

              <Form.Group as={Col} md="12" controlId="username">
                <Form.Label>Username</Form.Label>
                <Form.Control
                  type="text"
                  name="username"
                  placeholder="Username"
                  value={values.username || ""}
                  onChange={handleChange}
                  isInvalid={touched.username && errors.username}
                />
                <Form.Control.Feedback type="invalid">
                  {errors.username}
                </Form.Control.Feedback>
              </Form.Group>

              <Form.Group as={Col} md="12" controlId="password">
                <Form.Label>Password</Form.Label>
                <Form.Control
                  type="password"
                  name="password"
                  placeholder="Password"
                  value={values.password || ""}
                  onChange={handleChange}
                  isInvalid={touched.password && errors.password}
                />
                <Form.Control.Feedback type="invalid">
                  {errors.password}
                </Form.Control.Feedback>
              </Form.Group>
            </Form.Row>
            <Button type="submit" style={{ marginRight: "10px" }}>
              Save
            </Button>
            <Button type="button" onClick={edit ? onCancelEdit : onCancelAdd}>
              Cancel
            </Button>
          </Form>
        )}
      </Formik>
    </>
  );
}

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

export default PasswordForm;

Here, we add our form for letting users enter the username and password of their websites. We use the Yup schema object we created at the top of our code to make sure all fields are entered and check that the URL entered is actually a URL. We use the Formik component to handle the form of input changes and get the latest values.

Once the form is checked to be valid by schema.validate promise resolving to true , then addPassword or editPassword functions from requests.js , which we will create later will be called depending if the user is adding or editing an entry. Once that succeeds, then the getPasswords from the same file is called, and then setPasswords from passwordsStore is called to store the passwords in the store. Finally, onSave passed in from the props in HomePage component is called to close the modal.

Next create requests.js and add:

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

export const getPasswords = () => axios.get(`${APIURL}/passwords`);

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

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

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

to let us make the requests to our back end to save the password entries.

Then we create our MobX store by creating store.js and add:

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

class PasswordsStore {
  passwords = [];

  setPasswords(passwords) {
    this.passwords = passwords;
  }
}

PasswordsStore = decorate(PasswordsStore, {
  passwords: observable,
  setPasswords: action
});

export { PasswordsStore };

We have the passwords field which can be observed for the latest value if we wrap the observer function provided by MobX outside a component. The setPasswords is used to set the latest password entries in the store so that they can be propagated to the components.

Finally, in index.html , we 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>Password Manager</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 the title and add the 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:

{
  "passwords": [
  ]
}

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

Categories
Vue

How to Create Web Components with Vue.js

Component-based architecture is the main architecture for front end development today. The World Wide Web Consortium (W3C) has caught up to the present by creating the web components API. It lets developers build custom elements that can be embedded in web pages. The elements can be reused and nested anywhere, allowing for code reuse in any pages or apps.

The custom elements are nested in the shadow DOM, which is rendered separately from the main DOM of a document. This means that they are completely isolated from other parts of the page or app, eliminating the chance of conflict with other parts,

There are also template and slot elements that aren’t rendered on the page, allowing you to reused the things inside in any place.

To create web components without using any framework, you have to register your element by calling CustomElementRegistry.define() and pass in the name of the element you want to define. Then you have to attach the shadow DOM of your custom element by calling Element.attachShawdow() so that your element will be displayed on your page.

This doesn’t include writing the code that you want for your custom elements, which will involve manipulating the shadow DOM of your element. It is going to be frustrating and error-prone if you want to build a complex element.

Vue.js abstracts away the tough parts by letting you build your code into a web component. You write code by importing and including the components in your Vue components instead of globally, and then you can run commands to build your code into one or more web components and test it.

We build the code into a web component with Vue CLI by running:

npm run build -- --target wc --inline-vue --name custom-element-name

The --inline-vue flag includes a copy of view in the built code, --target wc builds the code into a web component, and --name is the name of your element.

In this article, we will build a weather widget web component that displays the weather from the OpenWeatherMap API. We will add a search to let users look up the current weather and forecast from the API.

We will use Vue.js to build the web component. To begin building it, we start with creating the project with Vue CLI. Run npx @vue/cli create weather-widget to create the project. In the wizard, select Babel, SCSS and Vuex.

The OpenWeatherMap API is available at https://openweathermap.org/api. You can register for an API key here. Once you got an API key, create an .env file in the root folder and add VUE_APP_APIKEY as the key and the API key as the value.

Next, we install some packages that we need for building the web component. We need Axios for making HTTP requests, BootstrapVue for styling, and Vee-Validate for form validation. To install them, we run npm i axios bootstrap-vue vee-validate to install them.

With all the packages installed we can start writing our code. Create CurrentWeather.vue in the components folder and add:

<template>
  <div>
    <br />
    <b-list-group v-if="weather.main">
      <b-list-group-item>Current Temparature: {{weather.main.temp - 273.15}} C</b-list-group-item>
      <b-list-group-item>High: {{weather.main.temp_max - 273.15}} C</b-list-group-item>
      <b-list-group-item>Low: {{weather.main.temp_min - 273.15}} C</b-list-group-item>
      <b-list-group-item>Pressure: {{weather.main.pressure }}mb</b-list-group-item>
      <b-list-group-item>Humidity: {{weather.main.humidity }}%</b-list-group-item>
    </b-list-group>
  </div>
</template>

<script>
import { requestsMixin } from "@/mixins/requestsMixin";
import store from "../store";
import { BListGroup, BListGroupItem } from "bootstrap-vue";
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'

export default {
  store,
  name: "CurrentWeather",
  mounted() {},
  mixins: [requestsMixin],
  components: {
    BListGroup,
    BListGroupItem
  },
  computed: {
    keyword() {
      return this.$store.state.keyword;
    }
  },
  data() {
    return {
      weather: {}
    };
  },
  watch: {
    async keyword(val) {
      const response = await this.searchWeather(val);
      this.weather = response.data;
    }
  }
};
</script>

<style scoped>
p {
  font-size: 20px;
}
</style>

This component displays the current weather from the OpenWeatherMap API as the keyword from the Vuex store is updated. We will create the Vuex store later. The this.searchWeather function is from the requestsMixin , which is a Vue mixin that we will create. The computed block gets the keyword from the store via this.$store.state.keyword and return the latest value.

Note that we’re importing all the BootstrapVue components individually here. This is because we aren’t building an app. main.js in our project will not be run, so we cannot register components globally by calling Vue.use . Also, we have to import the store here, so that we have access to the Vuex store in the component.

Next, create Forecast.vue in the same folder and add:

<template>
  <div>
    <br />
    <b-list-group v-for="(l, i) of forecast.list" :key="i">
      <b-list-group-item>
        <b>Date: {{l.dt_txt}}</b>
      </b-list-group-item>
      <b-list-group-item>Temperature: {{l.main.temp - 273.15}} C</b-list-group-item>
      <b-list-group-item>High: {{l.main.temp_max - 273.15}} C</b-list-group-item>
      <b-list-group-item>Low: {{l.main.temp_min }}mb</b-list-group-item>
      <b-list-group-item>Pressure: {{l.main.pressure }}mb</b-list-group-item>
    </b-list-group>
  </div>
</template>

<script>
import { requestsMixin } from "@/mixins/requestsMixin";
import store from "../store";
import { BListGroup, BListGroupItem } from "bootstrap-vue";
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'

export default {
  store,
  name: "Forecast",
  mixins: [requestsMixin],
  components: {
    BListGroup,
    BListGroupItem
  },
  computed: {
    keyword() {
      return this.$store.state.keyword;
    }
  },
  data() {
    return {
      forecast: []
    };
  },
  watch: {
    async keyword(val) {
      const response = await this.searchForecast(val);
      this.forecast = response.data;
    }
  }
};
</script>

<style scoped>
p {
  font-size: 20px;
}
</style>

It’s very similar to CurrentWeather.vue . The only difference is that we are getting the current weather instead of the weather forecast.

Next, we create a mixins folder in the src folder and add:

const APIURL = "[http://api.openweathermap.org](http://api.openweathermap.org)";
const axios = require("axios");

export const requestsMixin = {
  methods: {
    searchWeather(loc) {
      return axios.get(
        `${APIURL}/data/2.5/weather?q=${loc}&appid=${process.env.VUE_APP_APIKEY}`
      );
    },

searchForecast(loc) {
      return axios.get(
        `${APIURL}/data/2.5/forecast?q=${loc}&appid=${process.env.VUE_APP_APIKEY}`
      );
    }
  }
};

These functions are for getting the current weather and the forecast respectively from the OpenWeatherMap API. process.env.VUE_APP_APIKEY is obtained from our .env file that we created earlier.

Next in App.vue , we replace the existing code with:

<template>
  <div>
    <b-navbar toggleable="lg" type="dark" variant="info">
      <b-navbar-brand href="#">Weather App</b-navbar-brand>
    </b-navbar>
    <div class="page">
      <ValidationObserver ref="observer" v-slot="{ invalid }">
        <b-form @submit.prevent="onSubmit" novalidate>
          <b-form-group label="Keyword" label-for="keyword">
            <ValidationProvider name="keyword" rules="required" v-slot="{ errors }">
              <b-form-input
                :state="errors.length == 0"
                v-model="form.keyword"
                type="text"
                required
                placeholder="Keyword"
                name="keyword"
              ></b-form-input>
              <b-form-invalid-feedback :state="errors.length == 0">Keyword is required</b-form-invalid-feedback>
            </ValidationProvider>
          </b-form-group>

          <b-button type="submit" variant="primary">Search</b-button>
        </b-form>
      </ValidationObserver>

      <br />

      <b-tabs>
        <b-tab title="Current Weather">
          <CurrentWeather />
        </b-tab>
        <b-tab title="Forecast">
          <Forecast />
        </b-tab>
      </b-tabs>
    </div>
  </div>
</template>

<script>
import CurrentWeather from "@/components/CurrentWeather.vue";
import Forecast from "@/components/Forecast.vue";
import store from "./store";
import {
  BTabs,
  BTab,
  BButton,
  BForm,
  BFormGroup,
  BFormInvalidFeedback,
  BNavbar,
  BNavbarBrand,
  BFormInput
} from "bootstrap-vue";
import { ValidationProvider, extend, ValidationObserver } from "vee-validate";
import { required } from "vee-validate/dist/rules";
extend("required", required);

export default {
  store,
  name: "App",
  components: {
    CurrentWeather,
    Forecast,
    ValidationProvider,
    ValidationObserver,
    BTabs,
    BTab,
    BButton,
    BForm,
    BFormGroup,
    BFormInvalidFeedback,
    BNavbar,
    BNavbarBrand,
    BFormInput
  },
  data() {
    return {
      form: {}
    };
  },
  methods: {
    async onSubmit() {
      const isValid = await this.$refs.observer.validate();
      if (!isValid) {
        return;
      }
      localStorage.setItem("keyword", this.form.keyword);
      this.$store.commit("setKeyword", this.form.keyword);
    }
  },
  beforeMount() {
    this.form = { keyword: localStorage.getItem("keyword") || "" };
  },
  mounted() {
    this.$store.commit("setKeyword", this.form.keyword);
  }
};
</script>

<style lang="scss">
@import "./../node_modules/bootstrap/dist/css/bootstrap.css";
@import "./../node_modules/bootstrap-vue/dist/bootstrap-vue.css";
.page {
  padding: 20px;
}
</style>

We add the BootstrapVue b-navbar here to add a top bar to show the extension’s name. Below that, we added the form for searching the weather info. Form validation is done by wrapping the form in the ValidationObserver component and wrapping the input in the ValidationProvider component. We provide the rule for validation in the rules prop of ValidationProvider . The rules will be added in main.js later.

The error messages are displayed in the b-form-invalid-feedback component. We get the errors from the scoped slot in ValidationProvider . It’s where we get the errors object from.

When the user submits the number, the onSubmit function is called. This is where the ValidationObserver becomes useful as it provides us with the this.$refs.observer.validate() function to check for form validity.

If isValid resolves to true , then we set the keyword in local storage, and also in the Vuex store by running this.$store.commit(“setKeyword”, this.form.keyword); .

In the beforeMount hook, we set the keyword so that it will be populated when the extension first loads if a keyword was set in local storage. In the mounted hook, we set the keyword in the Vuex store so that the tabs will get the keyword to trigger the search for the weather data.

Like in the previous components, we import and register all the components and the Vuex store in this component, so that we can use the BootstrapVue components here. We also called Vee-Validate’s extend function so that we can use its required form validation rule for checking the input.

In style section of this file, we import the BootstrapVue styles, so that they can be accessed in this and the child components. We also add the page class so that we can add some padding to the page.

Then in store.js , we replace the existing code with:

import Vue from "vue";
import Vuex from "vuex";

Vue.use(Vuex);

export default new Vuex.Store({
  state: {
    keyword: ""
  },
  mutations: {
    setKeyword(state, payload) {
      state.keyword = payload;
    }
  },
  actions: {}
});

to add the Vuex store that we referenced in the components. We have the keyword state for storing the search keyword in the store, and the setKeyword mutation function so that we can set the keyword in our components.

Finally, in package.json , we add 2 scripts to the scripts section of the file:

"wc-build": "npm run build -- --target wc --inline-vue --name weather-widget",

"wc-test": "cd dist && live-server --port=8080 --entry-file=./demo.html"

The wc-build script builds our code into a web component as we described before, and the wc-test runs a local web server so that we can see what the web component looks like when it’s included in a web page. We use the live-server NPM package for serving the file. The --entry-file option specifies that we server demo.html as the home page, which we get when we run npm run wc-build .

If we run npm run wc-build and npm run wc-test , we get:

As you can see, we get the web component’s shadow DOM rendered in the browser and in the developer console.

We created a web component with less effort than using plain JavaScript, especially for something complex enough to have nesting and interactions.

Categories
JavaScript

Using a JavaScript Proxy Object to Control Object Operations

JavaScript object operations can be controlled using a special Proxy object

In JavaScript, a Proxy is an object that lets us control what happens when we do some operation. For example, we can use them to control the lookup, assignment, enumeration of properties or how functions are invoked.

The Proxy constructor takes 2 arguments. The first is the target, which is the object that you want to apply the controlling operations to and the second is the handler which is an object that actually controls how operations behave in the target object, also called traps.

The handler object is the object that contains traps for the Proxy. It has a number of methods to let us control the fundamental operations of the object. The object has a number of methods to trap various operations that are done by the methods in the Object constructor. They include:

  • handler.getPrototypeOf() — lets us control the behavior of the Object.getPrototypeOf() method for the target object
  • handler.setPrototypeOf()— lets us control the behavior of the Object.setPrototypeOf() method for the target object
  • handler.isExtensible()— lets us control the behavior of the Object.isExtensible() method for the target object
  • handler.preventExtensions()— lets us control the behavior of the Object.preventExtensions() method for the target object
  • handler.getOwnPropertyDescriptor()— lets us control the behavior of the Object.getOwnPropertyDescriptor() method for the target object
  • handler.defineProperty()— lets us control the behavior of the Object.defineProperty() method for the target object
  • handler.has()— lets us control the behavior of the Object.has() method for the target object
  • handler.get()— lets us control the behavior of the Object.get() method for the target object
  • handler.set()— lets us control the behavior of the Object.set() method for the target object
  • handler.deleteProperty()— lets us control the behavior of the Object.deleteProperty() method for the target object
  • handler.ownKeys()— lets us control the behavior of the Object.ownKeys() method for the target object
  • handler.apply()— lets us control the behavior of the Object.apply() method for the target object
  • handler.construct()— lets us control the behavior of the Object.construct() method for the target object

A basic example would be to return a default value for a property with the Proxy. For example, if we have the following code:

const handler = {
  get(obj, prop) {
    return prop === 'a' && obj[prop] ?
      obj[prop] :
      1;
  }
};

let p = new Proxy({}, handler);
console.log(p.a); // 1
p.a = 2;
console.log(p.a); // 2

Then the first console.log statement would output 1 and the second one would output 2. This is because in the handler object, we have a get function to modify how a property is retrieved. In the function, if the property name is a and obj[prop] is truthy, which means that obj['a'] is truthy, then we return it, otherwise, we return 1. This sets the default value of p.a where p is the Proxy object constructed by the Proxy constructor to 1. If we set a new value for p.a then the get function will return the new value since it’s truthy. Therefore, the second console.log statement of p.a outputs 2.

We can pass in an empty object for the handler argument. It would make all the default operations to the target object be forwarded as-is. For example, if we have:

let p = new Proxy({}, {});
console.log(p.a); // undefined
p.a = 2;
console.log(p.a); // 2

Then we get that the first console.log statement is undefined, but the second one is 2 because we didn’t modify the get function in the handler object to return anything if nothing is set.

Also, we can use proxies for validation of values that are assigned to an object’s properties. For example, we can use it to validate that a valid US phone number is assigned to a property of the Proxy object:

const handler = {
  set(obj, prop, value) {
    const validPhone = /^d{3}-d{3}-d{4}$/.test(value);
    if (prop === 'phoneNumber') {
      if (!validPhone) {
        throw new Error('Invalid phone number');
      }
    }

    obj[prop] = value;
    return validPhone;
  }
};

let person = new Proxy({}, handler);

person.phoneNumber = '555-555-5555'; // valid
console.log(person.phoneNumber);
person.phoneNumber = 'abc'; // throws an error

In the example above, we check that what’s being assigned is actually a valid US phone number by checking against the given regular expression. If the phoneNumber property is being assigned, then we check the regular expression against the value given in the parameters of the set function, and if the validPhone is false, then we throw an error. Otherwise, we set the value to the phoneNumber property of the object. In the end, we return the validation status of the given value. The first assignment:

person.phoneNumber = '555-555-5555';

This should work since it matches the regular expression in the set function. However, the second assignment would throw an error because it doesn’t match the regular expression given.

We can add the construct function to the handler object to extend the constructor of the target object. For example, we can extend a base object with the superclass by setting the base object’s prototype to the superclass and then create a new proxy with a handler object that has the construct and apply functions to control the behavior of the constructor and the apply functions of the base object. For example, we can write:

function extend(sup, base) {
  const descriptor = Object.getOwnPropertyDescriptor(
    base.prototype, 'constructor'
  );
  base.prototype = Object.create(sup.prototype);
  const handler = {
    construct(target, args) {
      const obj = Object.create(base.prototype);
      this.apply(target, obj, args);
      return obj;
    },
    apply(target, that, args) {
      sup.apply(that, args);
      base.apply(that, args);
    }
  };
  const proxy = new Proxy(base, handler);
  descriptor.value = proxy;
  Object.defineProperty(base.prototype, 'constructor', descriptor);
  return proxy;
}

let Person = function(name) {
  this.name = name;
};

let Boy = extend(Person, function(name, age, gender) {
  this.name = name;
  this.age = age;
  this.gender = gender;
});

let Joe = new Boy('Joe', 13, 'M');
console.log(Joe.gender);
console.log(Joe.name);
console.log(Joe.age);

This creates a proxy with the base object as a target. The handler has the constructor and the apply functions to modify the behavior of the constructor for the base object and the apply function respectively.

The construct function creates a new object obj by setting the prototype to the sup object which served as the superclass of the obj object, which in JavaScript is the same as the prototype. This is a template object which the base object inherits its members from. Then this.apply(target, obj, args); to run the constructor with the passed in arguments in the args object and then return the obj object. The apply function in the handler runs the constructor functions for both the sup and base objects to construct the base object.

Then at the end, the Proxy object is created and set as the constructor’s value with the descriptor.value = proxy; line. Then we set the base object’s prototype’s constructor by running Object.defineProperty(base.prototype, ‘constructor’, descriptor); and return the Proxy object to let us extend the constructor of the base object with a superclass object.

Below the extend function, we created a Person constructor to let us set the name property of instances of Person. Then we call the extend function with the Person object and pass in a new constructor function to with parameters for the name, age, and gender to set these properties. Then we get a new constructed object with:

let Joe = new Boy('Joe', 13, 'M');

Then when we log the properties we get ‘M’ for gender, ‘Joe’ for name, and 13 for age.

When we set one property of a Proxy object, we can simultaneously modify another property of the object. For example, if we have a room Proxy object which is constructed with a target object with the people property that has an array of names of people in the same room, and we want to push to the people array when the lastPerson property of the Proxy object is set. We do can this with the following code:

let room = new Proxy({
  people: ['Joe', 'Jane']
}, {
  get(obj, prop) {
    if (prop === 'lastPerson') {
      return obj.people[obj.people.length - 1];
    }
    return obj[prop];
  },
  set(obj, prop, value) {
    if (prop === 'lastPerson') {
      obj.people.push(value);
      obj[prop] = value;
      return true;
    }
    return true;
  }
});

console.log(room);
room.lastPerson = 'John';
console.log(room.people);
console.log(room.lastPerson);

room.lastPerson = 'Mary';
console.log(room.people);
console.log(room.lastPerson);

In the get function, we specify that the value of the lastPerson of the property will be the last element of the people array. Therefore, when we run console.log on room.lastPerson, we always get the last element of the room.people array. Otherwise, we set the object as is. In the set function, when the lastPerson property is being modified then we also push whatever value is being set into the people array in the room Proxy object. Therefore, when we run the console.log statements, we get:

["Joe", "Jane", "John"]
John

["Joe", "Jane", "John", "Mary"]
Mary

As we can see, when we set the lastPerson property of room we also get the same value pushed into the people array.

Below is a more comprehensive example of the traps we can set in the handler object to control the behavior of the proxy object’s operations:

const handler = {
  get(obj, prop) {
    return obj[prop];
  },
  set(obj, prop, value) {
    obj[prop] = value;
    return true;
  },
  deleteProperty(obj, prop) {
    delete obj[prop];
    return false;
  },
  ownKeys(obj) {
    return Reflect.ownKeys(obj);
  },
  has(obj, prop) {
    return prop in obj;
  },
  defineProperty(obj, prop, descriptor) {
    Object.defineProperty(obj, prop, descriptor)
    return true;
  },
  getOwnPropertyDescriptor(obj, prop) {
    return Object.getOwnPropertyDescriptor(obj, prop);
  },
}

let proxy = new Proxy({}, handler);
proxy.a = 1;
console.log(proxy.a);
console.log(Object.getOwnPropertyDescriptor(proxy, 'a'))
console.log(Object.defineProperty(proxy, 'b', {
  value: 1
}))
console.log('a' in proxy);
console.log(delete proxy.c);
console.log(Object.keys(proxy));

In the example above, the getOwnProperty function in the handler object controls the behavior of the Object.getOwnPropertyDescriptor() when it’s applied to the proxy object. The defineProperty function in the handler object controls how the Object.defineProperty() behaves when it’s called on the proxy object. The has function controls the behavior of the in operator, and the deleteProperty function controls the value that the delete operator returns when running with the proxy object as the operand. We returned false instead of the usual true when we use the delete operator on the proxy. The ownKeys function modifies the behavior of the Object.keys() method by enumerating the keys of an object with the Reflect object.

Wrapping up

JavaScript Proxies are a useful way to control the behavior of object operations. We can control how the object operators like the in, delete, and assignment operator behaves on the target object and its properties. This is very useful for validation during those operations and also handy for modifying the return values of operations that can return values like the in and delete operator.

We can modify the assignment operator with the set function where we can validate the value being assigned and also modify other properties at the same time. We can modify the behavior of the get function to return different values for properties in certain situations like when no value is set on a property.