Categories
HTML

How to Toggle an HTML Radio Button by Clicking its Label?

Sometimes, we want to toggle an HTML radio button by clicking its label.

In this article, we’ll look at how to toggle an HTML radio button by clicking its label.

Toggle an HTML Radio Button by Clicking its Label with JavaScript

To toggle an HTML radio button by clicking its label with JavaScript, we should put the radio button and the radio button text in the label element.

For instance, we can write:

<label>  
  <input type="radio" name="mode" value="create">  
  <i>create table</i>  
</label>

We put the input and i element with the label text so that when we click on the text, the radio button will be selected.

Conclusion

To toggle an HTML radio button by clicking its label with JavaScript, we should put the radio button and the radio button text in the label element.

Categories
Bootstrap React

How to Build a New York Times App with React and Bootstrap

React is a great framework for building interactive web apps. It comes with a bare bones set of features. It can render your page when you update your data and provides a convenient syntax for you to write your code easily. We can easily use it to build apps that uses public APIs, like the New York Times API.

In this story, we will build an app that uses the New York Times API with multilingual capability. You can view the static text in the app in English or French. The app can get the news from different sections and also do searches. It supports cross domain HTTP requests so that we can write client side apps that uses the API.

Before building the app, you need to register for an API key at https://developer.nytimes.com/

To start building the app, we use the Create React App command line utility to generate the scaffolding code. To use it, we run npx create-react-app nyt-app to create the code in the nyt-app folder. After that we need to install some libraries. We need the Axios HTTP client, a library to convert objects into query strings, Bootstrap library to make everything look better, React Router for routing and create forms easily with Formik and Yup. For translation and localization, we use the React-i18next library to allow us to translate our text into English and French. To install the libraries, we run npm i axios bootstrap formik i18next i18next-browser-languagedetector i18next-xhr-backend querystring react-bootstrap react-i18next react-router-dom yup .

Now that we have all the libraries installed, we can start writing code. For simplicity’s sake, we put everything in the src folder. We start by modifying 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 TopBar from "./TopBar";
import { createBrowserHistory as createHistory } from "history";
import "./App.css";
import SearchPage from "./SearchPage";
import { useTranslation } from "react-i18next";
import { useState, useEffect } from "react";
const history = createHistory();

function App() {
  const { t, i18n } = useTranslation();
  const [initialized, setInitialized] = useState(false);
  const changeLanguage = lng => {
    i18n.changeLanguage(lng);
  };

useEffect(() => {
    if (!initialized) {
      changeLanguage(localStorage.getItem("language") || "en");
      setInitialized(true);
    }
  });

return (
    <div className="App">
      <Router history={history}>
        <TopBar />
        <Route path="/" exact component={HomePage} />
        <Route path="/search" exact component={SearchPage} />
      </Router>
    </div>
  );
}

export default App;

This is the root component of our app and is the component that is loaded when the app first loads. We use the useTranslation function from the react-i18next library returns an object with the t property and the i18n property,. Here, we destructured the returned object’s properties into its own variables. we will use the t , which takes a translation key, to get the English or French text depending on the language set. In this file, we use the i18n function to set the language with the provided i18n.changeLanguage function. We also set the language from local storage when provided so that the chosen language will be persisted after refresh.

We also add the routes for our pages here used by the React router.

In App.css ,we put:

.center {
  text-align: center;
}

to center some text.

Next we make the home page. we createHomePage.js and in the file, we put:

import React from "react";
import { useState, useEffect } from "react";
import Form from "react-bootstrap/Form";
import ListGroup from "react-bootstrap/ListGroup";
import Card from "react-bootstrap/Card";
import Button from "react-bootstrap/Button";
import { getArticles } from "./requests";
import { useTranslation } from "react-i18next";
import "./HomePage.css";

const sections = `arts, automobiles, books, business, fashion, food, health,
home, insider, magazine, movies, national, nyregion, obituaries,
opinion, politics, realestate, science, sports, sundayreview,
technology, theater, tmagazine, travel, upshot, world`
  .replace(/ /g, "")
  .split(",");

function HomePage() {
  const [selectedSection, setSelectedSection] = useState("arts");
  const [articles, setArticles] = useState([]);
  const [initialized, setInitialized] = useState(false);
  const { t, i18n } = useTranslation();

const load = async section => {
    setSelectedSection(section);
    const response = await getArticles(section);
    setArticles(response.data.results || []);
  };

const loadArticles = async e => {
    if (!e || !e.target) {
      return;
    }
    setSelectedSection(e.target.value);
    load(e.target.value);
  };

const initializeArticles = () => {
    load(selectedSection);
    setInitialized(true);
  };

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

return (
    <div className="HomePage">
      <div className="col-12">
        <div className="row">
          <div className="col-md-3 d-none d-md-block d-lg-block d-xl-block">
            <ListGroup className="sections">
              {sections.map(s => (
                <ListGroup.Item
                  key={s}
                  className="list-group-item"
                  active={s == selectedSection}
                >
                  <a
                    className="link"
                    onClick={() => {
                      load(s);
                    }}
                  >
                    {t(s)}
                  </a>
                </ListGroup.Item>
              ))}
            </ListGroup>
          </div>
          <div className="col right">
            <Form className="d-sm-block d-md-none d-lg-none d-xl-none">
              <Form.Group controlId="section">
                <Form.Label>{t("Section")}</Form.Label>
                <Form.Control
                  as="select"
                  onChange={loadArticles}
                  value={selectedSection}
                >
                  {sections.map(s => (
                    <option key={s} value={s}>{t(s)}</option>
                  ))}
                </Form.Control>
              </Form.Group>
            </Form>
            <h1>{t(selectedSection)}</h1>
            {articles.map((a, i) => (
              <Card key={i}>
                <Card.Body>
                  <Card.Title>{a.title}</Card.Title>
                  <Card.Img
                    variant="top"
                    className="image"
                    src={
                      Array.isArray(a.multimedia) &&
                      a.multimedia[a.multimedia.length - 1]
                        ? a.multimedia[a.multimedia.length - 1].url
                        : null
                    }
                  />
                  <Card.Text>{a.abstract}</Card.Text>
                  <Button
                    variant="primary"
                    onClick={() => (window.location.href = a.url)}
                  >
                    {t("Go")}
                  </Button>
                </Card.Body>
              </Card>
            ))}
          </div>
        </div>
      </div>
    </div>
  );
}

export default HomePage;

In this file, we display a responsive layout where there is a left bar is the screen is wide and the a drop down on the right pane if it’s not. We display the items in the chosen section we choose from the left pane or the drop down. To display the items, we use the Card widget from React Bootstrap. We also use the t function provided by react-i18next to load our text from our translation file, which we will create. To load the initial article entries, we run a function in the callback of the useEffect function to show load the items once from the New York Times API. We need the initialized flag so that the function in the callback won’t load on every re-render. In the drop down, we added code to load articles whenever the selection changes.

The we createHomePage.css , and add:

.link {
  cursor: pointer;
}

.right {
  padding: 20px;
}

.image {
  max-width: 400px;
  text-align: center;
}

.sections {
    margin-top: 20px;
}

We change the cursor style for the Go button and add some padding to the right pane.

Next we create a file for loading the translations and setting the default language. Create a file called i18n.js and add:

import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import { resources } from "./translations";
import Backend from "i18next-xhr-backend";
import LanguageDetector from "i18next-browser-languagedetector";

i18n
  .use(Backend)
  .use(LanguageDetector)
  .use(initReactI18next)
  .init({
    resources,
    lng: "en",
    fallbackLng: "en",
    debug: true,

interpolation: {
      escapeValue: false,
    },
  });

export default i18n;

In this file, we load the translations from a file, and set the default language to English. Since react-i18next escapes everything, we can set escapeValue to false for interpolation since it’s redundant.

We need a file to put the code for making HTTP requests. To do so, we create a file called requests.js and add:

const APIURL = "https://api.nytimes.com/svc";
const axios = require("axios");
const querystring = require("querystring");

export const search = data => {
  Object.keys(data).forEach(key => {
    data["api-key"] = process.env.REACT_APP_APIKEY;
    if (!data[key]) {
      delete data[key];
    }
  });
  return axios.get(
    `${APIURL}/search/v2/articlesearch.json?${querystring.encode(data)}`
  );
};
export const getArticles = section =>
  axios.get(
    `${APIURL}/topstories/v2/${section}.json?api-key=${process.env.REACT_APP_APIKEY}`
  );

We load the API key from the process.env.REACT_APP_APIKEY variable, which is provided by an environment variable in the .env file located at the root folder. You have to create it yourself, and in there, put:

REACT_APP_APIKEY='you New York Times API key'

Replace the value on the right side with the API key you got after registering in the New York Times API website.

Next we create the search page. Create a file called SearchPage.js and add:

import React from "react";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import "./SearchPage.css";
import * as yup from "yup";
import { Formik } from "formik";
import Form from "react-bootstrap/Form";
import Col from "react-bootstrap/Col";
import Button from "react-bootstrap/Button";
import { Trans } from "react-i18next";
import { search } from "./requests";
import Card from "react-bootstrap/Card";

const schema = yup.object({
  keyword: yup.string().required("Keyword is required"),
});

function SearchPage() {
  const { t } = useTranslation();
  const [articles, setArticles] = useState([]);
  const [count, setCount] = useState(0);

const handleSubmit = async e => {
    const response = await search({ q: e.keyword });
    setArticles(response.data.response.docs || []);
  };

return (
    <div className="SearchPage">
      <h1 className="center">{t("Search")}</h1>
      <Formik validationSchema={schema} onSubmit={handleSubmit}>
        {({
          handleSubmit,
          handleChange,
          handleBlur,
          values,
          touched,
          isInvalid,
          errors,
        }) => (
          <Form noValidate onSubmit={handleSubmit} className="form">
            <Form.Row>
              <Form.Group as={Col} md="12" controlId="keyword">
                <Form.Label>{t("Keyword")}</Form.Label>
                <Form.Control
                  type="text"
                  name="keyword"
                  placeholder={t("Keyword")}
                  value={values.keyword || ""}
                  onChange={handleChange}
                  isInvalid={touched.keyword && errors.keyword}
                />
                <Form.Control.Feedback type="invalid">
                  {errors.keyword}
                </Form.Control.Feedback>
              </Form.Group>
            </Form.Row>
            <Button type="submit" style={{ marginRight: "10px" }}>
              {t("Search")}
            </Button>
          </Form>
        )}
      </Formik>
      <h3 className="form">
        <Trans i18nKey="numResults" count={articles.length}>
          There are <strong>{{ count }}</strong> results.
        </Trans>
      </h3>
      {articles.map((a, i) => (
        <Card key={i}>
          <Card.Body>
            <Card.Title>{a.headline.main}</Card.Title>
            <Card.Text>{a.abstract}</Card.Text>
            <Button
              variant="primary"
              onClick={() => (window.location.href = a.web_url)}
            >
              {t("Go")}
            </Button>
          </Card.Body>
        </Card>
      ))}
    </div>
  );
}

export default SearchPage;

This is where we create a search form with the keyword field used for searching the API. When the use clicks Search, then it will search the New York Times API for articles using the keyword. We use Formik to handle the form value changes and make the values available in the e object in the handleSubmit parameter for us to use. We use React Bootstrap for the buttons, form elements, and the cards. After clicking Search, the articles variable get set and load the cards for the articles.

We use the Trans component provided by react-i18next for translating text that have some dynamic components, like in the example above. We have a variable in the text for the number of results. Whenever you have something like, you wrap it in the Trans component and then pass in the variables like in the example above by passing in the variables as props. Then you will show the variable in the text between the Trans tags. We we will also make interpolation available in the translations by putting “There are <1>{{count}}</1> results.” in the English translation and “Il y a <1>{{count}}</1> résultats.” in French translation. The 1 tag corresponds to the strong tag. The number in this case is arbitrary. As long as the pattern consistent with the component’s pattern, it will work, so strong tag in this case should always be 1 in the translation string.

To add the translations mentioned above along with the rest of the translations, create a file called translations.js and add:

const resources = {
  en: {
    translation: {
      "New York Times App": "New York Times App",
      arts: "Arts",
      automobiles: "Automobiles",
      books: "Books",
      business: "Business",
      fashion: "Fashion",
      food: "Food",
      health: "Health",
      home: "Home",
      insider: "Inside",
      magazine: "Magazine",
      movies: "Movies",
      national: "National",
      nyregion: "New York Region",
      obituaries: "Obituaries",
      opinion: "Opinion",
      politics: "Politics",
      realestate: "Real Estate",
      science: "Science",
      sports: "Sports",
      sundayreview: "Sunday Review",
      technology: "Technology",
      theater: "Theater",
      tmagazine: "T Magazine",
      travel: "Travel",
      upshot: "Upshot",
      world: "World",
      Search: "Search",
      numResults: "There are <1>{{count}}</1> results.",
      Home: "Home",
      Search: "Search",
      Language: "Language",
      English: "English",
      French: "French",
      Keyword: "Keyword",
      Go: "Go",
      Section: "Section",
    },
  },
  fr: {
    translation: {
      "New York Times App": "App New York Times",
      arts: "Arts",
      automobiles: "Les automobiles",
      books: "Livres",
      business: "Entreprise",
      fashion: "Mode",
      food: "Aliments",
      health: "Santé",
      home: "Maison",
      insider: "Initiée",
      magazine: "Magazine",
      movies: "Films",
      national: "Nationale",
      nyregion: "La région de new york",
      obituaries: "Notices nécrologiques",
      opinion: "Opinion",
      politics: "Politique",
      realestate: "Immobilier",
      science: "Science",
      sports: "Des sports",
      sundayreview: "Avis dimanche",
      technology: "La technologie",
      theater: "Théâtre",
      tmagazine: "Magazine T",
      travel: "Voyage",
      upshot: "Résultat",
      world: "Monde",
      Search: "Search",
      numResults: "Il y a <1>{{count}}</1> résultats.",
      Home: "Page d'accueil",
      Search: "Chercher",
      Language: "La langue",
      English: "Anglais",
      French: "Français",
      Keyword: "Mot-clé",
      Go: "Aller",
      Section: "Section",
    },
  },
};

export { resources };

We have the static text translations, and the interpolated text we mentioned above in this file.

Finally, we create the top bar by creating 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";
import { useTranslation } from "react-i18next";

function TopBar({ location }) {
  const { pathname } = location;
  const { t, i18n } = useTranslation();
  const changeLanguage = lng => {
    localStorage.setItem("language", lng);
    i18n.changeLanguage(lng);
  };

return (
    <Navbar bg="primary" expand="lg" variant="dark">
      <Navbar.Brand href="#home">{t("New York Times 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 == "/"}>
            {t("Home")}
          </Nav.Link>
          <Nav.Link href="/search" active={pathname.includes("/search")}>
            {t("Search")}
          </Nav.Link>
          <NavDropdown title={t("Language")} id="basic-nav-dropdown">
            <NavDropdown.Item onClick={() => changeLanguage("en")}>
              {t("English")}
            </NavDropdown.Item>
            <NavDropdown.Item onClick={() => changeLanguage("fr")}>
              {t("French")}
            </NavDropdown.Item>
          </NavDropdown>
        </Nav>
      </Navbar.Collapse>
    </Navbar>
  );
}

export default withRouter(TopBar);

We use the NavBar component provided by React Boostrap and we add a drop down for users to select a language and when they click those items, they can set the language. Notice that we wrapped the TopBar component with the withRouter function so that we get the current route’s value with the location prop, and use it to set which link is active by setting the active prop in the Nav.Link components.

Finally, we replace the existing code in index.html 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 New York Times 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 add the Bootstrap CSS and change the title of the app.

After all the work is done, when we run npm start , we get:

Categories
Web Components

Creating Web Components — Lifecycle Callbacks

As web apps get more complex, we need some way to divide the code into manageable chunks. To do this, we can use Web Components to create reusable components that we can use in multiple places.

In this article, we’ll look at the lifecycle hooks of Web Components and how we use them.

Lifecycle Hooks

Web Components have their own lifecycle. The following events happen in a Web Component’s lifecycle:

  • element is inserted into the DOM
  • updates when UI event is being triggered
  • element deleted from the DOM

A Web Component has lifecycle hooks, which are callback functions, to capture these lifecycle events and let us handle them accordingly.

They let us handle these events without creating our own system to do so. Most JavaScript frameworks provide the same functionality, but Web Components is a standard so we don’t need to load extra code to be able to use them.

The following lifecycle hooks are in a web component:

  • constructor()
  • connectedCallback()
  • disconnectedCallback()
  • attributeChangedCallback(name, oldValue, newValue)
  • adoptedCallback()

constructor()

The constructor() is called when the Web Component is created. It’s called when we create the shadow DOM and it’s used for setting up listeners and initialize a component’s state.

However, it’s not recommended that we run things like rendering and fetching resources here. The connectedCallback is better for these kinds of tasks.

Defining a constructor is optional for ES6 classes, but an empty one will be created when it’s undefined.

When creating the constructor, we’ve to call super() to call the class that the Web Component class extends.

We can have return statements in there and we can’t use document.write() or document.open() in there.

Also, we can’t gain attributes or children in the constructor method.

connectedCallback()

connectedCallback() method is called when an element is added to the DOM. We can be sure that the element is available to the DOM when this method is called.

This means that we can safely set attributes, fetch resources, run set up code or render templates.

disconnectedCallback()

This is called when the element is removed from the DOM. Therefore, it’s an ideal place to add cleanup logic and to free up resources. We can also use this callback to:

  • notify another part of an application that the element is removed from the DOM
  • free resources that won’t be garbage collected automatically like unsubscribing from DOM events, stop interval timers, or unregister all registered callbacks

This hook is never called when the user closes the tab and it can be trigger more than once during its lifetime.

attributeChangedCallback(attrName, oldVal, newVal)

We can pass attributes with values to a Web Component like any other attribute:

<custom-element
  foo="foo"
  bar="bar"
  baz="baz">
</custom-element>

In this callback, we can get the value of the attributes as they’re assigned in the code.

We can add a static get observedAttributes() hook to define what attribute values we observe. For example, we can write:

class CustomElement extends HTMLElement {
  constructor() {
    super();
    const shadow = this.attachShadow({
      mode: 'open'
    });
  }

  static get observedAttributes() {
    return ['foo', 'bar', 'baz'];
  }

  attributeChangedCallback(name, oldValue, newValue) {
    console.log(`${name}'s value has been changed from ${oldValue} to ${newValue}`);
  }
}

customElements.define('custom-element', CustomElement);

Then we should the following from the console.log :

foo's value has been changed from null to foo
bar's value has been changed from null to bar
baz's value has been changed from null to baz

given that we have the following HTML:

<custom-element foo="foo" bar="bar" baz="baz">
</custom-element>

We get this because we assigned the values to the foo , bar and baz attributes in the HTML with the values of the same name.

adoptedCallback()

The adoptedCallback is called when we call document.adoptNode with the element passed in. It only occurs when we deal with iframes.

The adoptNode method is used to transfer a node from one document to another. An iframe has another document, so it’s possible to call this with iframe’s document object.

Example

We can use it to create an element with text that blinks as follows:

class BlinkElement extends HTMLElement {
  constructor() {
    super();
  }

  connectedCallback() {
    const shadow = this.attachShadow({
      mode: 'open'
    });
    this.span = document.createElement('span');
    this.span.textContent = this.getAttribute('text');
    const style = document.createElement('style');
    style.textContent = 'span { color: black }';
    this.intervalTimer = setInterval(() => {
      let styleText = this.style.textContent;
      if (style.textContent.includes('red')) {
        style.textContent = 'span { color: black }';
      } else {
        style.textContent = 'span { color: red }';
      }

}, 1000)
    shadow.appendChild(style);
    shadow.appendChild(this.span);
  }

  disconnectedCallback() {
    clearInterval(this.intervalTimer);
  }

  static get observedAttributes() {
    return ['text'];
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (name === 'text') {
      if (this.span) {
        this.span.textContent = newValue;
      }

    }
  }
}

customElements.define('blink-element', BlinkElement);

In the connectedCallback , we have all the code for initializing the element. It includes adding a span for the text that we passed into the text attribute as its value, and the styles for blinking the text. The style starts with the creation of the style element and then we added the setInterval code to blink the text by changing the color of the span from red to black and vice versa.

Then we attached the nodes to the shadow DOM which we created at the beginning.

We have the observedAttributes static method to set the attributes to watch for. This is used by the attributeChangedCallback to watch for the value of the text attribute and set it accordingly by getting the newValue .

To see the attributeChangedCallback in action, we can create the element dynamically and set its attribute as follows:

const blink2 = document.createElement('blink-element');
document.body.appendChild(blink2);
blink2.setAttribute('text', 'bar');
blink2.setAttribute('text', 'baz');

Then we should bar and baz if we log newValue in the attributeChangedCallback hook.

Finally, we have the disconnectedCallback which is run when the component is removed. For example, when we remove an element with removeChild as in the following code:

const blink2 = document.createElement('blink-element');
document.body.appendChild(blink2);
blink2.setAttribute('text', 'bar');
document.body.removeChild(blink2);

The lifecycle hooks allow us to handle various DOM events inside and outside the shadow DOM. Web Components have hooks for initializing, removing, and attribute changes. We can select which attributes to watch for changes.

There’re also hooks for adopting elements in another document.

Categories
Web Components

Creating Web Components — Templates and Slots

As web apps get more complex, we need some way to divide the code into manageable chunks. To do this, we can use Web Components to create reusable components that we can use in multiple places.

In this article, we’ll look at how to use templates and slots with Web Components.

Templates

We can use the template element to add content that’s not rendered in the DOM. This lets us incorporate them into any other element by writing the following HTML:

<template>
  <p>Foo</p>
</template>

Then we can write the following JavaScript to incorporate it into the page:

const template = document.querySelector('template');
const templateContent = template.content;
document.body.appendChild(templateContent);

We should see ‘Foo’ on the page when we load the page. The p element should be showing when we inspect the code for ‘Foo’. This confirms that the markup isn’t affected by the template element.

Using Templates with Web Components

We can also use them with Web Components. To use it, we can get the template element in the class for the Web Component just like we do outside the Web Component.

For example, we can write:

customElements.define('foo-paragraph',
  class extends HTMLElement {
    constructor() {
      super();
      let template = document.querySelector('template');
      let templateContent = template.content;
      const shadowRoot = this.attachShadow({
          mode: 'open'
        })
        .appendChild(templateContent.cloneNode(true));
    }
  })

In the code above, we get the template element and the call cloneNode on templateContent which has template.content as the value to get the template’s content.

Then we call cloneNode to clone the template.content node’s children in addition to the node itself. This is indicated by the true argument of the cloneNode function call.

Finally, we attached the cloned templateContent into the shadow root of the Web Component with the attachShadow and appendChild methods.

We can also include styling inside a template element, so we can reuse it as we do with the HTML markup.

For instance, we can change the template element to:

<template>
  <style>
    p {
      color: white;
      background-color: gray;
      padding: 5px;
    }

  </style>
  <p>Foo</p>
</template>

Then we should see:

Slots

To make our templates more flexible, we can add slots to them to display content as we wish instead of static content. With slots, we can pass in content between our Web Component opening and closing tags.

It has more limited support than templates. Slots can be used with Chrome 53 or later, Opera since version 40, Firefox since version 59, and not supported in Edge.

For example, we can add some slots by writing:

<template>
  <slot name="foo">Default text for foo</slot>
  <slot name="bar">Default text for bar</slot>
  <slot name="baz">Default text for baz</slot>
</template>

Then given that we have the same code as before for defining the foo-paragraph element in JavaScript, we can use it as follows:

<foo-paragraph>
  <p slot='foo'>foo</p>
  <p slot='bar'>bar</p>
  <p slot='baz'>baz</p>
</foo-paragraph>

The output then will be:

foo

bar

baz

If we omit the elements inside the foo-paragraph tags, we get the default text:

Default text for foo Default text for bar Default text for baz

In the code above, we name the slots with the name attribute in the template element, then in our Web Component, we fill in the slots that we defined by using the slot attribute.

We can style them like any other template element. To style them, we select the elements to be styled by using the selector for the elements that we’ll fill into the slots.

For example, we can write:

<template>
  <style>
    ::slotted(p) {
      padding: 5px;
      background-color: black;
      color: white;
    }

    ::slotted(p[slot='foo']) {
      background-color: gray;
    }
  </style>
  <slot name="foo">Default text for foo</slot>
  <slot name="bar">Default text for bar</slot>
  <slot name="baz">Default text for baz</slot>
</template>

<foo-paragraph>
  <p slot='foo'>foo</p>
  <p slot='bar'>bar</p>
  <p slot='baz'>baz</p>
</foo-paragraph>

The ::slotted pseudo-element represents elements that has been placed into a slot inside the HTML template. Therefore, we can use it to select the slot elements that we want to style.

In the argument of ::slotted , we can pass in the element that we want to style if they were passed into the slot. This means that only p elements that are passed in will have styles applied to them.

::slotted(p[slot=’foo’]) means that a p element with slot attribute values foo will have styling applied to it.

Then we should get the following after applying the styles:

Templates and slots lets us create reusable components that we can use in Web Components. template element do not show up during render so we can put them anywhere and use them as we wish in any Web Component.

With slots, we have more flexibility when creating Web Components since we can create components that we can insert other elements into to fill the slots.

In any case, we can apply styles to the elements as we wish. We have the ::slotted pseudoelement to apply styles to specific elements in slots.

Categories
HTML JavaScript

What’s new in Lighthouse 6

Lighthouse is a tool made by Google to let us measure the performance metrics of our browser apps.

Version 6 is the latest version of the software.

As with other versions, it’s available as part of Chrome or as an NPM package to let us run it automatically to check those performance metrics when the software is being built for continuous integration.

In this article, we’ll look at the latest features of Lighthouse 6.

New Metrics

Lighthouse 6 added some new metrics for measuring the performance of our browser apps.

Largest Contentful Paint (LCP)

One of them is the Largest Contentful Paint (LCP). It’s a measurement of perceived loading experience.

It captures how long it takes to load all the parts of our page.

Like with First Contentful Paint (FCP), we don’t want this to take too long.

If it’s more than 2.5 seconds, then it’s too long and we’ve to make our web app speedier.

Cumulative Layout Shift (CLS)

Another new metric is the Cumulative Layout Shift or CLS.

This is a measurement of visual stability.

Users don’t want a web page to have content that moves around too much.

Jumpy pages make it easy for users to do something they don’t want to do accidentally.

Therefore, any unexpected shift is annoying.

It’s measured by the following formula:

layout shift score = impact fraction * distance fraction

Impact fraction is the measurement of how unstable some element is.

It’s a percentage change of the distance from the original position to the new position.

Distance fraction is the distance that an element moved relative to the viewport.

It’s the great distance of any unstable element that has moved in the frame.

A score below 0.1 is considered good.

These are welcome changes since slowing loading and jumpy pages are always frustrating for users.

Total Blocking Time (TBT)

Total Blocking Time is another measurement of responsiveness.

It’s the total duration of when the main thread is blocked long enough to prevent input responsiveness.

It’s the total time between FCP and Time to Interactive.

Time to Interactive is the time to take to load the page until when we can interact with the page.

Performance Score Update

In Lighthouse 6, the performance score is calculated from the weighted blend of multiple performance metrics to summarize page speed.

The metrics are weighted as follows according to https://web.dev/lighthouse-whats-new-6.0/#new-metrics:

Phase Metric Name Metric Weight
Early (15%) First Contentful Paint (FCP) 15%
Mid (40%) Speed Index (SI) 15%
Largest Contentful Paint (LCP) 25%
Late (15%) Time To Interactive (TTI) 15%
Main Thread (25%) Total Blocking Time (TBT) 25%
Predictability (5%) Cumulative Layout Shift (CLS) 5%

The weights are changed in all the categories to takes into account the new metrics.

In version 5, the weights are as follows:

Phase Metric Name Metric Weight
Early (23%) First Contentful Paint (FCP) 23%
Mid (34%) Speed Index (SI) 27%
First Meaningful Paint (FMP) 7%
Finished (46%) Time to Interactive (TTI) 33%
First CPU Idle (FCI) 13%
Main Thread Max Potential FID 0%

TTI’s weight decreased according to user feedback. It’s very variable so reducing its weight will decrease its variability.

FCP is also decreased in weight since it doesn’t give us the full picture of loading.

The first CPU Idle metric is deprecated since it isn’t as direct as other metrics for interactivity.

With Lighthouse 6, we can use the Lighthouse Scoring Calculator to make our calculations.

New Audits Tools

Lighthouse 6 can audit unused JavaScript.

It’s included since 2017, but it’s disabled by default to keep Lighthouse fast.

Now that the tool is more efficient, it’s turned on by default.

It also has some new audit features to audit for some accessibility attributes.

They include:

  • aria-hidden-body
  • aria-hidden-focus
  • aria-input-field-name
  • aria-toggle-field-name
  • form-field-multiple-labels
  • heading-order
  • duplicate-id-active
  • duplicate-id-aria

aria-hidden-body makes sure aria-hidden=’true’ is in the body element

aria-hidden-focus checks for aria-hidden in focusable elements.

aria-input-field-name checks for aria attributes in input field names.

aria-toggle-field-name checks for aria attributes in a toggle field.

Form fields shouldn’t have multiple labels.

Headings should be sequentially descending order.

And there shouldn’t be any duplicate aria IDs.

Lighthouse 6 check for all those so that we won’t confuse users that are impaired.

Maskable icons are a new icon format that supports progressive web apps.

It’ll check for them so that we get good looking icons everywhere.

It also checks for the meta charset element to our HTML.

We also should add a Content-Type response header which it also checks for.

It specifies the character encoding explicitly so browsers won’t be confused.

With this declaration, browsers can’t render our page wrong.

Otherwise, it can render our page wrong and make the user experience poor.

Source Location Links

Lighthouse 6 provides us with the lines of code in our code that are causing audits to fail.

It has links to look through them in the Sources panel.

Experimental Features

Experimental features in Lighthouse 6 that are worth looking at include the ability to detect duplicate modules in JavaScript bundles.

It can also detect extra polyfills that we don’t need in modern browsers.

And it can detect unused JavaScript and report them by modules.

There’s also a visualization of all things in a module that require changes to improve the audit score.

We can enable them with the --preset experimental switch.

For example, we can run:

lighthouse https://example.com --view --preset experimental

to run Lighthouse in the command line with the experimental features on.

Lighthouse CI

Lighthouse CI is a Node CLI program and a server that lets us run Lighthouse audits for every commit.

It can easily be integrated into the continuous integration pipeline.

It supports many CI providers like Travis, Circle, GitLab, and Github Actions.

Docker images are provided to let us make the work of setting up the build pipeline with Lighthouse easy.

We can also install it manually.

To do that, we run:

npm install -g lighthouse

Then we can run it by running:

lighthouse <url>

Where url is the URL of our page.

Then we can change some options like logging and other configuration.

Logging can be set with --verbose or --quiet.

Other configuration options include changing the port, host name, emulate desktop, mobile, etc.

The full list of options are at https://www.npmjs.com/package/lighthouse#using-lighthouse-in-chrome-devtools

For instance, we can run Lighthouse and output to JSON by running:

lighthouse --output json

Renamed Chrome DevTools Panel

The Audits panel has been renamed to the Lighthouse panel in chrome.

This makes it easier to find.

Mobile Emulation

We can test mobile performance with emulation of slow network and CPU conditions.

Device screen emulation can also be done.

Lighthouse 6 has changed the reference device to the Moto G4.

Browser Extension

There’s a Chrome extension for Lighthouse.

This lets us run it locally in addition to what’s in Chrome itself.

It uses the PageSpeed Insights API.

The API has many limitations.

It can’t audit non-public websites since it runs from a remote server and not locally.

It’s also not guaranteed to have the latest Lighthouse release.

In Chrome, we should use the Lighthouse section of the dev tools to do our audits.

Or we can use the Node package version.

Conclusion

Lighthouse 6 is an even better version of Lighthouse.

It has new metrics to audit the performance of an app from the user’s perspective.

The weights to calculate the final performance score is changed from user feedback.

Also, it can look through our code to find where the problems originate.

This is very helpful when it comes to fixing problems easily.

With the Node package, we can automate our auditing easily.

Then the problems that needed to be fixed will be apparent to us all the time.