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
Angular Bootstrap

How to Use Bootstrap in Your Angular App

Bootstrap is a popular library for styling web pages. We can use it to style Angular apps easily with ready made Bootstrap components for Angular. ngx-bootstrap is one of the most popular Bootstrap library for Angular. We can use it to style our apps easily.

ngx-bootstrap provides things that are harder to add with plain Bootstrap to an Angular app like modals. Common things like navigation and forms are not included, and so we use regular Bootstrap classes to style those.

In this article, we will build an address book app that stores contacts in a back end. We use MobX-Angular to share data between components. For styling, we use ngx-bootstrap library to style the app.

MobX is a simple state management library for state management. It works by allowing any code that references the MobX store to observe values that the store tracks. The store also has functions that you can use to set the values you want in the store.

You just import the store into the place where you want to use it, so there’s no boilerplate to add until NgRX/store. This means that the state management is much simpler than with NgRX/store. There is no need for dispatch functions and reducers with MobX.

Angular and MobX works great together because of the MobX-Angular library.

We start by installing the Angular CLI by running npm i -g @angular/cli . Next we run ng new address-book-app to create the files for our address book app. Be sure to choose to including routing and use SCSS for styling.

Next we install a few libraries that we need to use. We MobX and MobX-Angular for state management, Ng2-Validation for form validation of number and email fields, and Ngx-Bootstrap for styling.

After all the libraries are installed, we run a few commands to create more code files.

ng g component contactForm
ng g component homePage
ng g class contactStore
ng g service contacts
ng g class exports

This will create files for our home page, contact form, and a service for sending HTTP requests to our server,

Now in contact-form.component.ts , we put:

import { Component, OnInit, Input, Output, SimpleChanges } from '@angular/core';
import { COUNTRIES } from '../exports';
import { contactStore } from '../contact-store';
import { EventEmitter } from '@angular/core';
import { ContactsService } from '../contacts.service';
import { NgForm } from '@angular/forms';

@Component({
  selector: 'app-contact-form',
  templateUrl: './contact-form.component.html',
  styleUrls: ['./contact-form.component.scss']
})
export class ContactFormComponent implements OnInit {
  contactData: any = <any>{};
  countries = COUNTRIES;
  store = contactStore;
  @Input('edit') edit: boolean;
  @Input('contact') contact: any = <any>{};
  @Output('contactEdited') contactEdited = new EventEmitter();

  constructor(
    private contactsService: ContactsService
  ) { }

  ngOnInit() {
  }

  ngOnChanges(changes: SimpleChanges) {
    if (this.contact) {
      this.contactData = Object.assign({}, this.contact);
    }
  }

  getPostalCodeRegex() {
    if (this.contactData.country == "United States") {
      return /^[0-9]{5}(?:-[0-9]{4})?$/;
    } else if (this.contactData.country == "Canada") {
      return /^[A-Za-z]d[A-Za-z][ -]?d[A-Za-z]d$/;
    }
    return /./;
  }

  getPhoneRegex() {
    if (["United States", "Canada"].includes(this.contactData.country)) {
      return /^[2-9]d{2}[2-9]d{2}d{4}$/;
    }
    return /./;
  }

  saveContact(contactForm: NgForm) {
    if (contactForm.invalid) {
      return;
    }

    if (this.edit) {
      this.contactsService.editContact(this.contactData)
        .subscribe(res => {
          this.getContacts();
        })
    }
    else {
      this.contactsService.addContact(this.contactData)
        .subscribe(res => {
          this.getContacts();
        })
    }
  }

  getContacts() {
    this.contactsService.getContacts()
      .subscribe(res => {
        this.store.setContacts(res);
        this.contactEdited.emit();
      })
  }

}

This is the logic for the contact form for our contact form. We functions for getting the regular expression for phone and postal code depending on country, getPostalCodeRegex and getPhoneRegex respectively. Also we have a function for saving our contact and getting it after it’s saved, the saveContact and getContacts functions respectively. We need the ngOnChanges life cycle handler to get existing contact if it’s passed in.

Whenever we get contacts, we call this.store.setContacts , where this.store is the MobX store that we will create, to set the latest contacts array to the store.

In contact-form.component.html , we add:

<form #contactForm="ngForm" (ngSubmit)="saveContact(contactForm)">
  <div class="form-group">
    <label>First Name</label>
    <input
      type="text"
      class="form-control"
      placeholder="First Name"
      #firstName="ngModel"
      name="firstName"
      [(ngModel)]="contactData.firstName"
      required
    />
    <div *ngIf="firstName.invalid && (firstName.dirty || firstName.touched)">
      <div *ngIf="firstName.errors.required">
        First Name is required.
      </div>
    </div>
  </div>
  <div class="form-group">
    <label>Last Name</label>
    <input
      type="text"
      class="form-control"
      placeholder="Last Name"
      #lastName="ngModel"
      name="lastName"
      [(ngModel)]="contactData.lastName"
      required
    />
    <div *ngIf="lastName?.invalid && (lastName.dirty || lastName.touched)">
      <div *ngIf="lastName.errors.required">
        Last Name is required.
      </div>
    </div>
  </div>

  <div class="form-group">
    <label>Address Line 1</label>
    <input
      type="text"
      class="form-control"
      placeholder="Address Line 1"
      #addressLineOne="ngModel"
      name="addressLineOne"
      [(ngModel)]="contactData.addressLineOne"
      required
    />
    <div
      *ngIf="
        addressLineOne?.invalid &&
        (addressLineOne.dirty || addressLineOne.touched)
      "
    >
      <div *ngIf="addressLineOne.errors.required">
        Address line 1 is required.
      </div>
    </div>
  </div>

  <div class="form-group">
    <label>Address Line 2</label>
    <input
      type="text"
      class="form-control"
      placeholder="Address Line 2"
      #addressLineTwo="ngModel"
      name="addressLineTwo"
      [(ngModel)]="contactData.addressLineTwo"
    />
  </div>

  <div class="form-group">
    <label>City</label>
    <input
      type="text"
      class="form-control"
      placeholder="City"
      #city="ngModel"
      name="city"
      [(ngModel)]="contactData.city"
      required
    />
    <div *ngIf="city?.invalid && (city.dirty || city.touched)">
      <div *ngIf="city.errors.required">
        City is required.
      </div>
      <div *ngIf="city.invalid">
        City is invalid.
      </div>
    </div>
  </div>

<div class="form-group">
    <label>Country</label>
    <select
      class="form-control"
      #country="ngModel"
      name="country"
      [(ngModel)]="contactData.country"
      required
    >
      <option *ngFor="let c of countries" [value]="c.name">
        {{ c.name }}
      </option>
    </select>
    <div *ngIf="country?.invalid && (country.dirty || country.touched)">
      <div *ngIf="country.errors.required">
        Country is required.
      </div>
    </div>
  </div>

  <div class="form-group">
    <label>Postal Code</label>
    <input
      type="text"
      class="form-control"
      placeholder="Postal Code"
      #postalCode="ngModel"
      name="postalCode"
      [(ngModel)]="contactData.postalCode"
      required
      [pattern]="getPostalCodeRegex()"
    />
    <div
      *ngIf="postalCode?.invalid && (postalCode.dirty || postalCode.touched)"
    >
      <div *ngIf="postalCode.errors.required">
        Postal code is required.
      </div>
      <div *ngIf="postalCode.invalid">
        Postal code is invalid.
      </div>
    </div>
  </div>

  <div class="form-group">
    <label>Phone</label>
    <input
      type="text"
      class="form-control"
      placeholder="Phone"
      #phone="ngModel"
      name="phone"
      [(ngModel)]="contactData.phone"
      required
      [pattern]="getPhoneRegex()"
    />
    <div *ngIf="phone?.invalid && (phone.dirty || phone.touched)">
      <div *ngIf="phone.errors.required">
        Phone is required.
      </div>
    </div>
  </div>

  <div class="form-group">
    <label>Age</label>
    <input
      type="text"
      class="form-control"
      placeholder="Age"
      #age="ngModel"
      name="age"
      [(ngModel)]="contactData.age"
      required
      [range]="[0, 200]"
    />
    <div *ngIf="age?.invalid && (age.dirty || age.touched)">
      <div *ngIf="age.errors.required">
        Age is required.
      </div>
      <div *ngIf="age.invalid">
        Age must be between 0 and 200.
      </div>
    </div>
  </div>

  <div class="form-group">
    <label>Email</label>
    <input
      type="text"
      class="form-control"
      placeholder="Email"
      #email="ngModel"
      name="email"
      [(ngModel)]="contactData.email"
      required
      email
    />
    <div *ngIf="email?.invalid && (email.dirty || email.touched)">
      <div *ngIf="email.errors.required">
        Email is required.
      </div>
      <div *ngIf="email.invalid">
        Email is invalid.
      </div>
    </div>
  </div>
  <button type="submit" class="btn btn-primary">Submit</button>
</form>

This is the contact form itself. The email directive is provided by ng2-validation for validating email. Also the [range]=”[0, 200]” input is also provided by the same library for validation number range.

Next we work on the home page. In home-page.component.ts , we add:

import { Component, OnInit, TemplateRef } from '@angular/core';
import { BsModalService, BsModalRef } from 'ngx-bootstrap/modal';
import { ContactsService } from '../contacts.service';
import { contactStore } from '../contact-store';

@Component({
  selector: 'app-home-page',
  templateUrl: './home-page.component.html',
  styleUrls: ['./home-page.component.scss']
})
export class HomePageComponent implements OnInit {

modalRef: BsModalRef;
  store = contactStore;
  edit: boolean;
  contacts: any[] = [];
  selectedContact: any = <any>{};
  constructor(
    private modalService: BsModalService,
    private contactsService: ContactsService
  ) { }

  openModal(addTemplate: TemplateRef<any>) {
    this.modalRef = this.modalService.show(addTemplate);
  }

  openEditModal(editTemplate: TemplateRef<any>, contact) {
    this.selectedContact = contact;
    this.modalRef = this.modalService.show(editTemplate);
  }

  closeModal() {
    this.modalRef.hide();
    this.selectedContact = {};
  }

  ngOnInit() {
    this.contactsService.getContacts()
      .subscribe(res => {
        this.store.setContacts(res);
      })
  }

  deleteContact(id) {
    this.contactsService.deleteContact(id)
      .subscribe(res => {
        this.getContacts();
      })
  }

  getContacts() {
    this.contactsService.getContacts()
      .subscribe(res => {
        this.store.setContacts(res);
      })
  }
}

We have the getContacts function to get contacts, deleteContact function to delete a contact, and openModal , openEditModal functions to open the add contact and edit contact modals respectively. The closeModal function is used to close the modal with the contactEdited event is emitted from ContactFormComponent .

Whenever we get contacts, we call this.store.setContacts , where this.store is the MobX store that we will create, to set the latest contacts array to the store.

Next in home-page.component.html , we put:

<div class="home-page" *mobxAutorun>
  <h1 class="text-center">Address Book</h1>
  <button
    type="button"
    class="btn btn-primary"
    (click)="openModal(addTemplate)"
  >
    Add Contact
  </button>

  <div class="table-container">
    <table class="table">
      <thead>
        <tr>
          <th>First Name</th>
          <th>Last Name</th>
          <th>Address</th>
          <th>Phone</th>
          <th>Age</th>
          <th>Email</th>
          <th></th>
          <th></th>
        </tr>
      </thead>
      <tbody>
        <tr *ngFor="let c of store.contacts">
          <td>{{ c.firstName }}</td>
          <td>{{ c.lastName }}</td>
          <td>
            {{ c.addressLineOne }}, {{ c.city }},
            {{ c.country }}
          </td>
          <td>{{ c.phone }}</td>
          <td>{{ c.age }}</td>
          <td>{{ c.email }}</td>
          <td>
            <button
              type="button"
              class="btn btn-primary"
              (click)="openEditModal(editTemplate, c)"
            >
              Edit
            </button>
          </td>
          <td>
            <button
              type="button"
              class="btn btn-primary"
              (click)="deleteContact(c.id)"
            >
              Delete
            </button>
          </td>
        </tr>
      </tbody>
    </table>
  </div>
</div>

<ng-template #addTemplate>
  <div class="modal-header">
    <h4 class="modal-title pull-left">Add Contact</h4>
  </div>
  <div class="modal-body">
    <app-contact-form
      (contactEdited)="closeModal()"
      [edit]="false"
    ></app-contact-form>
  </div>
</ng-template>

<ng-template #editTemplate>
  <div class="modal-header">
    <h4 class="modal-title pull-left">Edit Contact</h4>
  </div>
  <div class="modal-body">
    <app-contact-form
      (contactEdited)="closeModal()"
      [edit]="true"
      [contact]='selectedContact'
    ></app-contact-form>
  </div>
</ng-template>

We have a table to display the list of contacts. *ngFor=”let c of store.contacts” directive is in the tr tag to loop through the contacts stored in our MobX store.

The ng-template components are modals provided by ngx-bootstrap . We have addTemplate for using the contact form to add contact, and editTemplate to display the same form for editing. The tables and buttons are provided by regular Bootstrap

In home-page.component.scss , we add:

.home-page {
    padding: 20px;
}

.table-container {
    margin-top: 20px;
}

to add some padding and margin.

In app-routing.module.ts , which we get by choosing to including routing when running the Angular CLI wizard, we put:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { HomePageComponent } from './home-page/home-page.component';

const routes: Routes = [
  { path: '', component: HomePageComponent },
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

to let users see the home page when people navigate to the page by typing in the URL or clicking on the link.

In app.component.html , we replace the existing code with:

<nav class="navbar navbar-expand-lg navbar-light bg-light">
  <a class="navbar-brand" href="#">Address Book</a>
  <button
    class="navbar-toggler"
    type="button"
    data-toggle="collapse"
    data-target="#navbarSupportedContent"
    aria-controls="navbarSupportedContent"
    aria-expanded="false"
    aria-label="Toggle navigation"
  >
    <span class="navbar-toggler-icon"></span>
  </button>

  <div class="collapse navbar-collapse" id="navbarSupportedContent">
    <ul class="navbar-nav mr-auto">
      <li class="nav-item active">
        <a class="nav-link" href="#">Home </a>
      </li>
    </ul>
  </div>
</nav>
<router-outlet></router-outlet>

Here we add the Bootstrap navigation bar for showing the app title and navigation. The nav element is provided by plain Bootstrap.

Next in app.module.ts , we replace the existing code with:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { HomePageComponent } from './home-page/home-page.component';
import { ContactFormComponent } from './contact-form/contact-form.component';
import { ModalModule } from 'ngx-bootstrap/modal';
import { CustomFormsModule } from 'ng2-validation'
import { MobxAngularModule } from 'mobx-angular';
import { ContactsService } from './contacts.service';

@NgModule({
  declarations: [
    AppComponent,
    HomePageComponent,
    ContactFormComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    FormsModule,
    HttpClientModule,
    CustomFormsModule,
    ModalModule.forRoot(),
    MobxAngularModule
  ],
  providers: [
    ContactsService
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

to include all the libraries we use for building the app along with the code we generated.

Then we create the MobX store. In contact-store.ts , we add:

import { observable, action } from 'mobx-angular';

class ContactStore {
    @observable contacts = [];
    @action setContacts(contacts) {
        this.contacts = contacts;
    }
}

export const contactStore = new ContactStore();

The store is very simple. It has the contacts array which we decorate with the observable decorator so that it will be always updated everywhere when we reference it in our code. The setContacts function let us set the value the contacts array which will be propagated to any code that references it since we have the observable decorator in front of it. The action decorator means that we designate the function to be able to update observable values in the store.

We export the instance of the store, which we used our components wherever we referenced contactStore .

Next in contact.service.ts , we add:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { environment } from 'src/environments/environment';

@Injectable({
  providedIn: 'root'
})
export class ContactsService {

  constructor(
    private http: HttpClient
  ) { }

  getContacts() {
    return this.http.get(`${environment.apiUrl}/contacts`);
  }

  addContact(data) {
    return this.http.post(`${environment.apiUrl}/contacts`, data);
  }

  editContact(data) {
    return this.http.put(`${environment.apiUrl}/contacts/${data.id}`, data);
  }

  deleteContact(id) {
    return this.http.delete(`${environment.apiUrl}/contacts/${id}`);
  }
}

to let us make HTTP requests to our back end for manipulating contacts.

Next in exports.ts , we add:

export const COUNTRIES = [
    { "name": "Afghanistan", "code": "AF" },
    { "name": "Aland Islands", "code": "AX" },
    { "name": "Albania", "code": "AL" },
    { "name": "Algeria", "code": "DZ" },
    { "name": "American Samoa", "code": "AS" },
    { "name": "AndorrA", "code": "AD" },
    { "name": "Angola", "code": "AO" },
    { "name": "Anguilla", "code": "AI" },
    { "name": "Antarctica", "code": "AQ" },
    { "name": "Antigua and Barbuda", "code": "AG" },
    { "name": "Argentina", "code": "AR" },
    { "name": "Armenia", "code": "AM" },
    { "name": "Aruba", "code": "AW" },
    { "name": "Australia", "code": "AU" },
    { "name": "Austria", "code": "AT" },
    { "name": "Azerbaijan", "code": "AZ" },
    { "name": "Bahamas", "code": "BS" },
    { "name": "Bahrain", "code": "BH" },
    { "name": "Bangladesh", "code": "BD" },
    { "name": "Barbados", "code": "BB" },
    { "name": "Belarus", "code": "BY" },
    { "name": "Belgium", "code": "BE" },
    { "name": "Belize", "code": "BZ" },
    { "name": "Benin", "code": "BJ" },
    { "name": "Bermuda", "code": "BM" },
    { "name": "Bhutan", "code": "BT" },
    { "name": "Bolivia", "code": "BO" },
    { "name": "Bosnia and Herzegovina", "code": "BA" },
    { "name": "Botswana", "code": "BW" },
    { "name": "Bouvet Island", "code": "BV" },
    { "name": "Brazil", "code": "BR" },
    { "name": "British Indian Ocean Territory", "code": "IO" },
    { "name": "Brunei Darussalam", "code": "BN" },
    { "name": "Bulgaria", "code": "BG" },
    { "name": "Burkina Faso", "code": "BF" },
    { "name": "Burundi", "code": "BI" },
    { "name": "Cambodia", "code": "KH" },
    { "name": "Cameroon", "code": "CM" },
    { "name": "Canada", "code": "CA" },
    { "name": "Cape Verde", "code": "CV" },
    { "name": "Cayman Islands", "code": "KY" },
    { "name": "Central African Republic", "code": "CF" },
    { "name": "Chad", "code": "TD" },
    { "name": "Chile", "code": "CL" },
    { "name": "China", "code": "CN" },
    { "name": "Christmas Island", "code": "CX" },
    { "name": "Cocos (Keeling) Islands", "code": "CC" },
    { "name": "Colombia", "code": "CO" },
    { "name": "Comoros", "code": "KM" },
    { "name": "Congo", "code": "CG" },
    { "name": "Congo, The Democratic Republic of the", "code": "CD" },
    { "name": "Cook Islands", "code": "CK" },
    { "name": "Costa Rica", "code": "CR" },
    {
        "name": "Cote D"Ivoire", "code": "CI"
    },
    { "name": "Croatia", "code": "HR" },
    { "name": "Cuba", "code": "CU" },
    { "name": "Cyprus", "code": "CY" },
    { "name": "Czech Republic", "code": "CZ" },
    { "name": "Denmark", "code": "DK" },
    { "name": "Djibouti", "code": "DJ" },
    { "name": "Dominica", "code": "DM" },
    { "name": "Dominican Republic", "code": "DO" },
    { "name": "Ecuador", "code": "EC" },
    { "name": "Egypt", "code": "EG" },
    { "name": "El Salvador", "code": "SV" },
    { "name": "Equatorial Guinea", "code": "GQ" },
    { "name": "Eritrea", "code": "ER" },
    { "name": "Estonia", "code": "EE" },
    { "name": "Ethiopia", "code": "ET" },
    { "name": "Falkland Islands (Malvinas)", "code": "FK" },
    { "name": "Faroe Islands", "code": "FO" },
    { "name": "Fiji", "code": "FJ" },
    { "name": "Finland", "code": "FI" },
    { "name": "France", "code": "FR" },
    { "name": "French Guiana", "code": "GF" },
    { "name": "French Polynesia", "code": "PF" },
    { "name": "French Southern Territories", "code": "TF" },
    { "name": "Gabon", "code": "GA" },
    { "name": "Gambia", "code": "GM" },
    { "name": "Georgia", "code": "GE" },
    { "name": "Germany", "code": "DE" },
    { "name": "Ghana", "code": "GH" },
    { "name": "Gibraltar", "code": "GI" },
    { "name": "Greece", "code": "GR" },
    { "name": "Greenland", "code": "GL" },
    { "name": "Grenada", "code": "GD" },
    { "name": "Guadeloupe", "code": "GP" },
    { "name": "Guam", "code": "GU" },
    { "name": "Guatemala", "code": "GT" },
    { "name": "Guernsey", "code": "GG" },
    { "name": "Guinea", "code": "GN" },
    { "name": "Guinea-Bissau", "code": "GW" },
    { "name": "Guyana", "code": "GY" },
    { "name": "Haiti", "code": "HT" },
    { "name": "Heard Island and Mcdonald Islands", "code": "HM" },
    { "name": "Holy See (Vatican City State)", "code": "VA" },
    { "name": "Honduras", "code": "HN" },
    { "name": "Hong Kong", "code": "HK" },
    { "name": "Hungary", "code": "HU" },
    { "name": "Iceland", "code": "IS" },
    { "name": "India", "code": "IN" },
    { "name": "Indonesia", "code": "ID" },
    { "name": "Iran, Islamic Republic Of", "code": "IR" },
    { "name": "Iraq", "code": "IQ" },
    { "name": "Ireland", "code": "IE" },
    { "name": "Isle of Man", "code": "IM" },
    { "name": "Israel", "code": "IL" },
    { "name": "Italy", "code": "IT" },
    { "name": "Jamaica", "code": "JM" },
    { "name": "Japan", "code": "JP" },
    { "name": "Jersey", "code": "JE" },
    { "name": "Jordan", "code": "JO" },
    { "name": "Kazakhstan", "code": "KZ" },
    { "name": "Kenya", "code": "KE" },
    { "name": "Kiribati", "code": "KI" },
    {
        "name": "Korea, Democratic People"S Republic of", "code": "KP"
    },
    { "name": "Korea, Republic of", "code": "KR" },
    { "name": "Kuwait", "code": "KW" },
    { "name": "Kyrgyzstan", "code": "KG" },
    {
        "name": "Lao People"S Democratic Republic", "code": "LA"
    },
    { "name": "Latvia", "code": "LV" },
    { "name": "Lebanon", "code": "LB" },
    { "name": "Lesotho", "code": "LS" },
    { "name": "Liberia", "code": "LR" },
    { "name": "Libyan Arab Jamahiriya", "code": "LY" },
    { "name": "Liechtenstein", "code": "LI" },
    { "name": "Lithuania", "code": "LT" },
    { "name": "Luxembourg", "code": "LU" },
    { "name": "Macao", "code": "MO" },
    { "name": "Macedonia, The Former Yugoslav Republic of", "code": "MK" },
    { "name": "Madagascar", "code": "MG" },
    { "name": "Malawi", "code": "MW" },
    { "name": "Malaysia", "code": "MY" },
    { "name": "Maldives", "code": "MV" },
    { "name": "Mali", "code": "ML" },
    { "name": "Malta", "code": "MT" },
    { "name": "Marshall Islands", "code": "MH" },
    { "name": "Martinique", "code": "MQ" },
    { "name": "Mauritania", "code": "MR" },
    { "name": "Mauritius", "code": "MU" },
    { "name": "Mayotte", "code": "YT" },
    { "name": "Mexico", "code": "MX" },
    { "name": "Micronesia, Federated States of", "code": "FM" },
    { "name": "Moldova, Republic of", "code": "MD" },
    { "name": "Monaco", "code": "MC" },
    { "name": "Mongolia", "code": "MN" },
    { "name": "Montenegro", "code": "ME" },
    { "name": "Montserrat", "code": "MS" },
    { "name": "Morocco", "code": "MA" },
    { "name": "Mozambique", "code": "MZ" },
    { "name": "Myanmar", "code": "MM" },
    { "name": "Namibia", "code": "NA" },
    { "name": "Nauru", "code": "NR" },
    { "name": "Nepal", "code": "NP" },
    { "name": "Netherlands", "code": "NL" },
    { "name": "Netherlands Antilles", "code": "AN" },
    { "name": "New Caledonia", "code": "NC" },
    { "name": "New Zealand", "code": "NZ" },
    { "name": "Nicaragua", "code": "NI" },
    { "name": "Niger", "code": "NE" },
    { "name": "Nigeria", "code": "NG" },
    { "name": "Niue", "code": "NU" },
    { "name": "Norfolk Island", "code": "NF" },
    { "name": "Northern Mariana Islands", "code": "MP" },
    { "name": "Norway", "code": "NO" },
    { "name": "Oman", "code": "OM" },
    { "name": "Pakistan", "code": "PK" },
    { "name": "Palau", "code": "PW" },
    { "name": "Palestinian Territory, Occupied", "code": "PS" },
    { "name": "Panama", "code": "PA" },
    { "name": "Papua New Guinea", "code": "PG" },
    { "name": "Paraguay", "code": "PY" },
    { "name": "Peru", "code": "PE" },
    { "name": "Philippines", "code": "PH" },
    { "name": "Pitcairn", "code": "PN" },
    { "name": "Poland", "code": "PL" },
    { "name": "Portugal", "code": "PT" },
    { "name": "Puerto Rico", "code": "PR" },
    { "name": "Qatar", "code": "QA" },
    { "name": "Reunion", "code": "RE" },
    { "name": "Romania", "code": "RO" },
    { "name": "Russian Federation", "code": "RU" },
    { "name": "RWANDA", "code": "RW" },
    { "name": "Saint Helena", "code": "SH" },
    { "name": "Saint Kitts and Nevis", "code": "KN" },
    { "name": "Saint Lucia", "code": "LC" },
    { "name": "Saint Pierre and Miquelon", "code": "PM" },
    { "name": "Saint Vincent and the Grenadines", "code": "VC" },
    { "name": "Samoa", "code": "WS" },
    { "name": "San Marino", "code": "SM" },
    { "name": "Sao Tome and Principe", "code": "ST" },
    { "name": "Saudi Arabia", "code": "SA" },
    { "name": "Senegal", "code": "SN" },
    { "name": "Serbia", "code": "RS" },
    { "name": "Seychelles", "code": "SC" },
    { "name": "Sierra Leone", "code": "SL" },
    { "name": "Singapore", "code": "SG" },
    { "name": "Slovakia", "code": "SK" },
    { "name": "Slovenia", "code": "SI" },
    { "name": "Solomon Islands", "code": "SB" },
    { "name": "Somalia", "code": "SO" },
    { "name": "South Africa", "code": "ZA" },
    { "name": "South Georgia and the South Sandwich Islands", "code": "GS" },
    { "name": "Spain", "code": "ES" },
    { "name": "Sri Lanka", "code": "LK" },
    { "name": "Sudan", "code": "SD" },
    { "name": "Suriname", "code": "SR" },
    { "name": "Svalbard and Jan Mayen", "code": "SJ" },
    { "name": "Swaziland", "code": "SZ" },
    { "name": "Sweden", "code": "SE" },
    { "name": "Switzerland", "code": "CH" },
    { "name": "Syrian Arab Republic", "code": "SY" },
    { "name": "Taiwan, Province of China", "code": "TW" },
    { "name": "Tajikistan", "code": "TJ" },
    { "name": "Tanzania, United Republic of", "code": "TZ" },
    { "name": "Thailand", "code": "TH" },
    { "name": "Timor-Leste", "code": "TL" },
    { "name": "Togo", "code": "TG" },
    { "name": "Tokelau", "code": "TK" },
    { "name": "Tonga", "code": "TO" },
    { "name": "Trinidad and Tobago", "code": "TT" },
    { "name": "Tunisia", "code": "TN" },
    { "name": "Turkey", "code": "TR" },
    { "name": "Turkmenistan", "code": "TM" },
    { "name": "Turks and Caicos Islands", "code": "TC" },
    { "name": "Tuvalu", "code": "TV" },
    { "name": "Uganda", "code": "UG" },
    { "name": "Ukraine", "code": "UA" },
    { "name": "United Arab Emirates", "code": "AE" },
    { "name": "United Kingdom", "code": "GB" },
    { "name": "United States", "code": "US" },
    { "name": "United States Minor Outlying Islands", "code": "UM" },
    { "name": "Uruguay", "code": "UY" },
    { "name": "Uzbekistan", "code": "UZ" },
    { "name": "Vanuatu", "code": "VU" },
    { "name": "Venezuela", "code": "VE" },
    { "name": "Viet Nam", "code": "VN" },
    { "name": "Virgin Islands, British", "code": "VG" },
    { "name": "Virgin Islands, U.S.", "code": "VI" },
    { "name": "Wallis and Futuna", "code": "WF" },
    { "name": "Western Sahara", "code": "EH" },
    { "name": "Yemen", "code": "YE" },
    { "name": "Zambia", "code": "ZM" },
    { "name": "Zimbabwe", "code": "ZW" }
]

so that we have a list of countries for the Countries drop down in our contact form.

In environment.ts , we add:

export const environment = {
  production: false,
  apiUrl: 'http://localhost:3000'
};

so that we can talk to our back end.

Then in index.html , we have:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Angular Address Book App</title>
    <base href="/" />

    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link rel="icon" type="image/x-icon" href="favicon.ico" />
    <link
      href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"
      rel="stylesheet"
    />
  </head>
  <body>
    <app-root></app-root>
  </body>
</html>

to change the default title to our own and added Bootstrap CSS.

To run our app, first we use JSON server package located at https://github.com/typicode/json-server to create a simple back end without writing any code. Run npm i -g json-server to install it.

We create a file called db.json in the root folder and add:

{
  "contacts": []
}

so that we get the routes that we specified in contacts.service.ts .

Then we run json-server --watch db.json to start our back end, and run ng serve on to start our app.

Categories
Bootstrap HTML

Bootstrap 5 — Tables

Bootstrap 5 is in alpha when this is written and it’s subject to change.

Bootstrap is a popular UI library for any JavaScript apps.

In this article, we’ll look at how to style tables with Bootstrap 5.

Tables

Bootstrap table styles are opt-in because of the widespread of use of tables in other UI components.

The table styles aren’t inherited in Bootstrap.

So nested tables can be styles independently of the parent.

For example, we can use the table class to style tables.

We can create a simple table by writing:

<table class="table">  
  <thead>  
    <tr>  
      <th scope="col">#</th>  
      <th scope="col">First</th>  
      <th scope="col">Last</th>  
      <th scope="col">Age</th>  
    </tr>  
  </thead>  
  <tbody>  
    <tr>  
      <th scope="row">1</th>  
      <td>james</td>  
      <td>smith</td>  
      <td>20</td>  
    </tr>  
    <tr>  
      <th scope="row">2</th>  
      <td>mary</td>  
      <td>jones</td>  
      <td>20</td>  
    </tr>  
    <tr>  
      <th scope="row">3</th>  
      <td colspan="2">Larry</td>  
      <td>50</td>  
    </tr>  
  </tbody>  
</table>

Variants

There are many styling variants for a table.

We can style them with the following classes:

<table class="table-primary">...</table>  
<table class="table-secondary">...</table>  
<table class="table-success">...</table>  
<table class="table-danger">...</table>  
<table class="table-warning">...</table>  
<table class="table-info">...</table>  
<table class="table-light">...</table>  
<table class="table-dark">...</table>

They also work on rows:

<tr class="table-primary">...</tr>  
<tr class="table-secondary">...</tr>  
<tr class="table-success">...</tr>  
<tr class="table-danger">...</tr>  
<tr class="table-warning">...</tr>  
<tr class="table-info">...</tr>  
<tr class="table-light">...</tr>  
<tr class="table-dark">...</tr>

And they also work on table cells:

<tr>  
  <td class="table-primary">...</td>  
  <td class="table-secondary">...</td>  
  <td class="table-success">...</td>  
  <td class="table-danger">...</td>  
  <td class="table-warning">...</td>  
  <td class="table-info">...</td>  
  <td class="table-light">...</td>  
  <td class="table-dark">...</td>  
</tr>

Accented Tables

We can add the .table-striped class to add zebra striping to table rows within the tbody .

For example, we can write:

<table class="table table-striped">  
  <thead>  
    <tr>  
      <th scope="col">#</th>  
      <th scope="col">First</th>  
      <th scope="col">Last</th>  
      <th scope="col">Age</th>  
    </tr>  
  </thead>  
  <tbody>  
    <tr>  
      <th scope="row">1</th>  
      <td>james</td>  
      <td>smith</td>  
      <td>20</td>  
    </tr>  
    <tr>  
      <th scope="row">2</th>  
      <td>mary</td>  
      <td>jones</td>  
      <td>20</td>  
    </tr>  
    <tr>  
      <th scope="row">3</th>  
      <td colspan="2">Larry</td>  
      <td>50</td>  
    </tr>  
  </tbody>  
</table>

Stripes also work with other table variants:

<table class="table table-dark table-striped">  
  <thead>  
    <tr>  
      <th scope="col">#</th>  
      <th scope="col">First</th>  
      <th scope="col">Last</th>  
      <th scope="col">Age</th>  
    </tr>  
  </thead>  
  <tbody>  
    <tr>  
      <th scope="row">1</th>  
      <td>james</td>  
      <td>smith</td>  
      <td>20</td>  
    </tr>  
    <tr>  
      <th scope="row">2</th>  
      <td>mary</td>  
      <td>jones</td>  
      <td>20</td>  
    </tr>  
    <tr>  
      <th scope="row">3</th>  
      <td colspan="2">Larry</td>  
      <td>50</td>  
    </tr>  
  </tbody>  
</table>

Hoverable Rows

To make rows hoverable, we can add the table-hover class:

<table class="table table-hover">  
  <thead>  
    <tr>  
      <th scope="col">#</th>  
      <th scope="col">First</th>  
      <th scope="col">Last</th>  
      <th scope="col">Age</th>  
    </tr>  
  </thead>  
  <tbody>  
    <tr>  
      <th scope="row">1</th>  
      <td>james</td>  
      <td>smith</td>  
      <td>20</td>  
    </tr>  
    <tr>  
      <th scope="row">2</th>  
      <td>mary</td>  
      <td>jones</td>  
      <td>20</td>  
    </tr>  
    <tr>  
      <th scope="row">3</th>  
      <td colspan="2">Larry</td>  
      <td>50</td>  
    </tr>  
  </tbody>  
</table>

Now we see the row that we hovered on highlighted.

The hover effect can be combined with stripes:

<table class="table table-striped table-hover">  
  <thead>  
    <tr>  
      <th scope="col">#</th>  
      <th scope="col">First</th>  
      <th scope="col">Last</th>  
      <th scope="col">Age</th>  
    </tr>  
  </thead>  
  <tbody>  
    <tr>  
      <th scope="row">1</th>  
      <td>james</td>  
      <td>smith</td>  
      <td>20</td>  
    </tr>  
    <tr>  
      <th scope="row">2</th>  
      <td>mary</td>  
      <td>jones</td>  
      <td>20</td>  
    </tr>  
    <tr>  
      <th scope="row">3</th>  
      <td colspan="2">Larry</td>  
      <td>50</td>  
    </tr>  
  </tbody>  
</table>

Active Tables

We can highlight a table row or cell with the table-active class:

<table class="table">  
  <thead>  
    <tr>  
      <th scope="col">#</th>  
      <th scope="col">First</th>  
      <th scope="col">Last</th>  
      <th scope="col">Age</th>  
    </tr>  
  </thead>  
  <tbody>  
    <tr>  
      <th scope="row">1</th>  
      <td class="table-active">james</td>  
      <td>smith</td>  
      <td>20</td>  
    </tr>  
    <tr>  
      <th scope="row">2</th>  
      <td>mary</td>  
      <td>jones</td>  
      <td>20</td>  
    </tr>  
    <tr>  
      <th scope="row">3</th>  
      <td colspan="2">Larry</td>  
      <td>50</td>  
    </tr>  
  </tbody>  
</table>

We add the table-active class on the cell so it’ll be highlighted.

This also works with other variants:

<table class="table table-success">  
  <thead>  
    <tr>  
      <th scope="col">#</th>  
      <th scope="col">First</th>  
      <th scope="col">Last</th>  
      <th scope="col">Age</th>  
    </tr>  
  </thead>  
  <tbody>  
    <tr>  
      <th scope="row">1</th>  
      <td class="table-active">james</td>  
      <td>smith</td>  
      <td>20</td>  
    </tr>  
    <tr>  
      <th scope="row">2</th>  
      <td>mary</td>  
      <td>jones</td>  
      <td>20</td>  
    </tr>  
    <tr>  
      <th scope="row">3</th>  
      <td colspan="2">Larry</td>  
      <td>50</td>  
    </tr>  
  </tbody>  
</table>

The styling works by setting the background with the --bs-table-bg property.

Then the gradient on the table is added with the background-image: linear-gradient(var( — bs-table-accent-bg), var( — bs-table-accent-bg)); CSS property.

When .table-striped , .table-hover or .table-active classes are added, the --bs-table-accent-bg is set to semitransparent color to change the color of the background.

--bs-table-accent-bg color with the highest contrast is generated for the highlights.

Text and border colors are generated the same way.

Table Borders

We can add borders with the table-bordered class:

<table class="table table-bordered">  
  <thead>  
    <tr>  
      <th scope="col">#</th>  
      <th scope="col">First</th>  
      <th scope="col">Last</th>  
      <th scope="col">Age</th>  
    </tr>  
  </thead>  
  <tbody>  
    <tr>  
      <th scope="row">1</th>  
      <td>james</td>  
      <td>smith</td>  
      <td>20</td>  
    </tr>  
    <tr>  
      <th scope="row">2</th>  
      <td>mary</td>  
      <td>jones</td>  
      <td>20</td>  
    </tr>  
    <tr>  
      <th scope="row">3</th>  
      <td colspan="2">Larry</td>  
      <td>50</td>  
    </tr>  
  </tbody>  
</table>

Conclusion

We can add tables with various effects like stripes, hover, and various colors.

Categories
Bootstrap HTML

Bootstrap 5 — Table Captions, Figures, and Form Fields

Bootstrap 5 is in alpha when this is written and it’s subject to change.

Bootstrap is a popular UI library for any JavaScript apps.

In this article, we’ll look at how to style tables, figures, and form fields with Bootstrap 5.

Captions

We can add a caption to the top of the table with the caption-top class:

<table class="table caption-top">
  <caption>List of people</caption>
  <thead class="table-dark">
    <tr>
      <th scope="col">#</th>
      <th scope="col">First</th>
      <th scope="col">Last</th>
      <th scope="col">Age</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th scope="row">1</th>
      <td>james</td>
      <td>smith</td>
      <td>20</td>
    </tr>
    <tr>
      <th scope="row">2</th>
      <td>mary</td>
      <td>jones</td>
      <td>20</td>
    </tr>
    <tr>
      <th scope="row">3</th>
      <td colspan="2">Larry</td>
      <td>50</td>
    </tr>
  </tbody>
  <tfoot>
    <tr>
      <th>footer</th>
      <td>footer</td>
      <td>footer</td>
      <td>footer</td>
    </tr>
  </tfoot>
</table>

Responsive Tables

We can make tables responsive with the table-responsive class

To make it always responsive, we can use the table-responsive class:

<div class="table-responsive">
  <table class="table">
    <thead class="table-dark">
      <tr>
        <th scope="col">#</th>
        <th scope="col">First</th>
        <th scope="col">Last</th>
        <th scope="col">Age</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <th scope="row">1</th>
        <td>james</td>
        <td>smith</td>
        <td>20</td>
      </tr>
      <tr>
        <th scope="row">2</th>
        <td>mary</td>
        <td>jones</td>
        <td>20</td>
      </tr>
      <tr>
        <th scope="row">3</th>
        <td colspan="2">Larry</td>
        <td>50</td>
      </tr>
    </tbody>
    <tfoot>
      <tr>
        <th>footer</th>
        <td>footer</td>
        <td>footer</td>
        <td>footer</td>
      </tr>
    </tfoot>
  </table>
</div>

We make the table always responsive with the class in a div outside the table.

Also, we can make them responsive at a given breakpoint.

For example, we can write:

<div class="table-responsive-sm">
  <table class="table">
    <thead class="table-dark">
      <tr>
        <th scope="col">#</th>
        <th scope="col">First</th>
        <th scope="col">Last</th>
        <th scope="col">Age</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <th scope="row">1</th>
        <td>james</td>
        <td>smith</td>
        <td>20</td>
      </tr>
      <tr>
        <th scope="row">2</th>
        <td>mary</td>
        <td>jones</td>
        <td>20</td>
      </tr>
      <tr>
        <th scope="row">3</th>
        <td colspan="2">Larry</td>
        <td>50</td>
      </tr>
    </tbody>
    <tfoot>
      <tr>
        <th>footer</th>
        <td>footer</td>
        <td>footer</td>
        <td>footer</td>
      </tr>
    </tfoot>
  </table>
</div>

to make it responsive when the screen is wide enough to hit the sm breakpoint or wider.

sm can be substituted with md , lg , xl , or xxl .

Customizing in SASS

The table style presets can be changed in SASS.

The $table-striped-bg-factor, $table-active-bg-factor and $table-hover-bg-factor variables are used to determine the contrast in table variants.

Theme colors are lightened by the $table-bg-level variable.

Figures

We can add figures with captions with the figure tag.

The figcaption tag adds a caption for the figure.

Bootstrap 5 provides classes to make styling them easier.

For example, we can write:

`<figure class="figure">
  <img src="`http://placekitten.com/200/200`" class="figure-img img-fluid rounded" alt="cat">
  <figcaption class="figure-caption">A cat.</figcaption>
</figure>`

With Bootstrap 5’s text utilities, aligning the figure captions are easy:

`<figure class="figure">
  <img src="`http://placekitten.com/200/200`" class="figure-img img-fluid rounded" alt="cat">
  <figcaption class="figure-caption text-right">A cat.</figcaption>
</figure>`

With the text-right class, we aligned the caption to the right.

Form Controls

We can add form controls with Bootstrap 5 styles with the included classes.

For example, we can write:

<div class="mb-3">
  <label for="email" class="form-label">Email address</label>
  <input type="email" class="form-control" id="email" placeholder="name@example.com">
</div>

<div class="mb-3">
  <label for="text" class="form-label">Example textarea</label>
  <textarea class="form-control" id="text" rows="3"></textarea>
</div>

to add some form controls.

We have the form-control class to add the form control styles.

form-label class add the form label styles.

Sizing

To change the size of the controls, we can use the .form-control-lg and .form-control-sm classes:

<div class="mb-3">
  <label for="email" class="form-label">Email address</label>
  <input type="email" class="form-control-lg" id="email" placeholder="name@example.com">
</div>

We make the form control large with the form-control-lg class.

Likewise, we can make them smaller with the .form-control.sm class:

<div class="mb-3">
  <label for="email" class="form-label">Email address</label>
  <input type="email" class="form-control-sm" id="email" placeholder="name@example.com">
</div>

Readonly

We can add the readonly boolean attribute to prevent users from changing the input value of the form.

For example, we can write:

<div class="mb-3">
  <label for="email" class="form-label">Email address</label>
  <input type="email" class="form-control" id="email" placeholder="name@example.com" readonly>
</div>

The form input will be grayed out.

Readonly Plain Text

If we want to have a readonly form field without the any styles, then we can use the .form-control-plaintext class to make the field display as plain text.

For example, we can write:

<div class="mb-3">
  <label for="email" class="form-label">Email address</label>
  <input type="email" class="form-control-plaintext" id="email" placeholder="name@example.com" readonly>
</div>

Now there’re no borders and other things displayed.

Conclusion

We can add styles to tables, figures, and form fields.

Categories
Bootstrap HTML

Bootstrap 5 — Radio Buttons, Checkboxes, and File Inputs

Bootstrap 5 is in alpha when this is written and it’s subject to change.

Bootstrap is a popular UI library for any JavaScript apps.

In this article, we’ll look at how to style radio buttons, checkboxes, and file inputs with Bootstrap 5.

Radio Buttons and Checkboxes without Labels

We can add radio buttons and checkboxes without labels.

To do that, we can write:

<div>
  <input class="form-check-input" type="checkbox" id="checkboxNoLabel" value="">
</div>

<div>
  <input class="form-check-input" type="radio" name="radioNoLabel" id="radioNoLabel1" value="">
</div>

to add them.

Toggle Buttons

Bootstrap lets us turn checkboxes and radio buttons into toggle buttons.

To do that, we can write:

<input type="checkbox" class="btn-check" id="btn-check" autocomplete="off">
<label class="btn btn-primary" for="btn-check">toggle</label>

to add the toggle button.

It’ll display differently depending on if it’s toggled on or off.

The id of the input and the for attribute of the label have to match.

Radio Toggle Buttons

We can do the same for radio buttons.

For example, we can write:

<div class="btn-group">
  <input type="radio" class="btn-check" name="options" id="apple" autocomplete="off" checked>
  <label class="btn btn-secondary" for="apple">apple</label>

<input type="radio" class="btn-check" name="options" id="orange" autocomplete="off">
  <label class="btn btn-secondary" for="orange">orange</label>

<input type="radio" class="btn-check" name="options" id="grape" autocomplete="off">
  <label class="btn btn-secondary" for="grape">grape</label>
</div>

to add a button group.

Each button is a group is a radio button.

We use the btn-group class to style the button group.

The input type is set to radio .

btn-check is the class to make them display as toggle buttons.

And the id of the input has to match the for attribute value of the label .

Outlined Styles

The toggle can have outlined styles instead of a background color.

For example, we can write:

<div class="btn-group">
  <input type="radio" class="btn-check" name="options" id="apple" autocomplete="off" checked>
  <label class="btn btn-outline-secondary" for="apple">apple</label>

<input type="radio" class="btn-check" name="options" id="orange" autocomplete="off">
  <label class="btn btn-outline-secondary" for="orange">orange</label>

<input type="radio" class="btn-check" name="options" id="grape" autocomplete="off">
  <label class="btn btn-outline-secondary" for="grape">grape</label>
</div>

to make the buttons that aren’t chosen display an outline.

We add the word outline in the class name for the label.

For checkboxes, we can write:

<input type="checkbox" class="btn-check" id="btn-check-outlined" autocomplete="off">
<label class="btn btn-outline-primary" for="btn-check-outlined">apple</label><br>

to make the toggle button displayed with outline styles instead of a background color.

File Browser

Bootstrap 5 provides a file browser to let users select a file.

For example, we can write:

<div class="form-file">
  <input type="file" class="form-file-input" id="customFile">
  <label class="form-file-label" for="customFile">
    <span class="form-file-text">Choose a file...</span>
    <span class="form-file-button">Browse</span>
  </label>
</div>

to add a file input with an input with the type set to file .

Also, we add the label with the form-file-label class.

Inside it, we add our placeholder, which is the span with the form-file-text class.

We also have a span with the form-file-button class to display a button we can click to show the file browser to let us select a file.

To disable the file input, we can add the disabled prop to the input.

For example, we can write:

<div class="form-file">
  <input type="file" class="form-file-input" id="customFile" disabled>
  <label class="form-file-label" for="customFile">
    <span class="form-file-text">Choose file...</span>
    <span class="form-file-button">Browse</span>
  </label>
</div>

to disable the file input.

If we have longer placeholder text, it’ll be truncated with an ellipsis added at the end:

<div class="form-file">
  <input type="file" class="form-file-input" id="customFile">
  <label class="form-file-label" for="customFile">
    <span class="form-file-text">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque quis nunc ultricies enim ullamcorper pretium at cursus tellus. Curabitur sit amet leo arcu. Integer vitae tincidunt odio. Duis id nunc dignissim, fringilla lacus ut, rutrum ligula. Morbi euismod accumsan augue, sit amet finibus ipsum ultrices ac. Ut convallis quis lacus in volutpat. Pellentesque volutpat dui et enim mattis, egestas posuere nisl maximus. Aenean commodo laoreet enim, sit amet tincidunt nisl porttitor non. Ut vestibulum mauris urna, eget consectetur tellus maximus at. Suspendisse pharetra ut erat sed euismod.
    </span>
    <span class="form-file-button">Browse</span>
  </label>
</div>

Conclusion

We can style radio buttons and checkboxes as toggle buttons.

Also, Bootstrap 5 comes with its own file input.