A recipe for applications: Dataset & Web Components (Part 2)

R. S. Doiel,

Caltech Library, Digital Library Development

TBD

Welcome to “A recipe for applications”

Welcome everyone,

This presentation builds on what we built in the Part 1.

This workshop is focused on enhancing our application using Web Components.

What we’ll learn

What we’ll do

Workshop: “A recipe for applications”

Getting started, Part II

You’ll need the following from Part I

Part 2.1: Setting up

Next steps

Part 2.1: Copying htdocs and recipes_api.yaml

On macOS, Linux and Windows using WSL

cp -fR htdocs htdocs1
cp recipes_api.yaml recipes_api1.yaml

On Windows in PowerShell

copy -Recurse htdocs htdocs1
copy recipes_api.yaml recipes_api1.yaml

HOME WORK: Make recipes_api1.yaml work. Change the the htdocs attribute to point to htdocs1 directory. Compare version 1 with the results of the workshop. Discuss among friends.

Part 2.1: Starting up our web service

datasetd recipes_api.yaml

Part 2.2: The “What” of Web Components

Part 2.2: The “Why” of Web Components

Part 2.2: The Anatomy of a Web Component

Part 2.2: Why extend HTML elements?

  1. So we don’t have to define everything in JavaScript
  1. So we align with existing HTML element expectations

Part 2.2: What’s the connectedCallback() method?

Part 2.2.: What’s registration?

Part 2.2: The “Hello Clock” Web Component

<hello-clock>Hi There!</hello-clock>

This will display something like, “Hi There! 09:27:23 GMT-0700 (Pacific Daylight Time)”.

Part 2.2: “hello-clock.js” defines the web component

Create htdocs/modules/hello-clock.js

// Define our new element as a class
class HelloClock extends HTMLElement {
  // Hook used by browser to instantiate the element in the page
  connectedCallback() {
    // Get the current time as an object
    const d = new Date();
    // Update the inner text to include our time string
    const message = this.innerHTML.trim();
    this.textContent = `${message} ${d.toTimeString()}`;
  }
}
// This is how the browsers learns to use the new HTML element.
customElements.define( 'hello-clock', HelloClock );

Part 2.2: Now lets create an HTML Page using “Hello Clock”

<!DOCTYPE html>
<html lang="en-US">
    <head>
        <title>Hello Clock Example</title>
        <link rel="stylesheet" href="css/style.css">
        <script type="module" src="modules/hello-clock.js" defer></script>
    </head>
    <body>
        <h1>Hello Clock Example</h1>
        <hello-clock>Hi there!</hello-clock>
    </body>
</html>

Part 2.2: Fire up our web service and test “Hello Clock”

  1. Point your web browser at http://localhost:8001/hello-clock.html
  2. Open your developer tools and reload the page
  3. Notice the logs provided by Dataset Web Service

Part 2.2: Congratulations

You’ve built your first Web Component!!!!

Let’s take a quick break and stretch before moving forward

Part 2.3: Building an A to Z list web component

Q: What does the A to Z list web component do?

A: Wraps a standard UL list providing A to Z navigation.

Part 2.3: Building an A to Z list web component

  1. Create an HTML file for testing, htdocs/ul-a-to-z-list.html
  2. Create our Web Component, htdocs/modules/ul-a-to-z-list.js

macOS and Linux:

touch htdocs/ul-a-to-z-list.html htdocs/modules/ul-a-to-z-list.js

Windows:

New-Item -Path htdocs/ul-a-to-z-list.html ; New-Item htdocs/modules/ul-a-to-z-list.js

Part 2.3: Building an A to Z list web component

Here’s the HTML we’ll use for our test page, ul-a-to-z-list.html.

<!DOCTYPE html>
<html lang="en-US">
    <head>
        <title>A to Z List Clock Example</title>
        <link rel="stylesheet" href="css/style.css">
        <script type="module" src="modules/ul-a-to-z-list.js" defer></script>
    </head>
    <body>
        <h1>A to Z List Example</h1>

        <ul-a-to-z-list>
            <ul>
                <li>Alex</li> <li>Betty</li> <li>Connor</li> <li>David</li>
                <li>Edwina</li> <li>Fiona</li> <li>George</li> <li>Harry</li>
                <li>Ishmael</li> <li>Leonardo</li> <li>Millie</li> <li>Nellie</li>
                <li>Ollie</li> <li>Petra</li> <li>Quincy</li> <li>Rowino</li> <li>Selvina</li>
                <li>Terry</li> <li>Ulma</li> <li>Victorio</li> <li>Willamina</li> <li>Xavier</li>
                <li>Zoran</li>
            </ul>
        </ul-a-to-z-list>
    </body>
</html>

Part 2.3: Building an A to Z list web component

(source ul-a-to-z-list_v0.js)

Start out with a minimal Class definition and customElement definition

export class AToZList extends HTMLElement {
  constructor() {
    super();
  }

  connectedCallback() {
  }
}
customElements.define('ul-a-to-z-list', AToZList);

NOTE: The constructor and callback are empty. Open the web page. Use “the inspector” in your browser’s dev tools.

Part 2.3: Introducing Shadow DOM

(source ul-a-to-z-list_v1.js)

You can build your web component using the Shadow DOM. We need to include that in our constructor.

export class AToZList extends HTMLElement {
  constructor() {
    super();
    // This next line engages the Shadow DOM
    this.attachShadow({ mode: 'open' });
  }
  connectedCallback() {
  }
}
customElements.define('ul-a-to-z-list', AToZList);

Reload you web page, what does does it look like? What shows up in the inspector?

Part 2.3: What do we want the callback to do?

We use the connectedCallback() method to to call a render method. This is what makes our shadow DOM element visible.

export class AToZList extends HTMLElement {
  constructor() { super(); this.attachShadow({ mode: 'open' }); }

  connectedCallback() { this.render(); }

  render() {
    const template = document.createElement('template');
    template.innerHTML = '<!-- Our HTML markup will go here ... -->';
    this.shadowRoot.appendChild(template.content.cloneNode(true));
  }
}
customElements.define('ul-a-to-z-list', AToZList);

What happened in our web page? What does the inspector show?

Part 2.3: Basic Structure

(source ul-a-to-z-list_v2.js)

export class AToZList extends HTMLElement {
  constructor() { super(); this.attachShadow({ mode: 'open' }); }

  connectedCallback() { this.render(); }

  render() {
    const template = document.createElement('template');
    template.innerHTML = `<style>
        /* Basic styles */
        menu { list-style-type: none; padding: 0; }
      </style>
      <menu id="menu"></menu>
      <div id="list-container"></div>`;

    this.shadowRoot.appendChild(template.content.cloneNode(true));
  }
}
customElements.define('ul-a-to-z-list', AToZList);

Part 2.3: Display Items

(source ul-a-to-z-list_v3.js)

  render() {
    const template = document.createElement('template');
    template.innerHTML = `<style>
        /* Basic styles */
        menu { list-style-type: none; padding: 0; }
      </style>
      <menu id="menu"></menu>
      <div id="list-container"></div>`;

    this.shadowRoot.appendChild(template.content.cloneNode(true));

    const listContainer = this.shadowRoot.querySelector('#list-container');
    const ulElement = this.querySelector('ul');

    if (ulElement) {
      listContainer.appendChild(ulElement.cloneNode(true));
    }
  }

What happened in our web page? What shows up in the inspector?

Part 2.3: Organize Items by Starting Letter

(source ul-a-to-z-list_v4.js)

render() {
  const template = document.createElement('template');
  template.innerHTML = `<style>
      /* Basic styles */
      menu { list-style-type: none; padding: 0; }
      .letter-section { list-style-type: none; }
    </style>
    <menu id="menu"></menu>
    <div id="list-container"></div>`;

  this.shadowRoot.appendChild(template.content.cloneNode(true));

  const listContainer = this.shadowRoot.querySelector('#list-container');
  const ulElement = this.querySelector('ul');

  if (!ulElement) return;

  const items = Array.from(ulElement.querySelectorAll('li'));
  const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
  const sections = {};

  items.forEach(item => {
    const firstLetter = item.textContent.trim()[0].toUpperCase();
    if (!sections[firstLetter]) sections[firstLetter] = [];
    sections[firstLetter].push(item);
  });
  Object.keys(sections).forEach(letter => {
    // NOTE: Menu setup will go here
    const section = document.createElement('ul');
    section.classList.add('letter-section');
    section.id = `section-${letter}`;

    sections[letter].forEach(item => {
      section.appendChild(item.cloneNode(true));
    });

    listContainer.appendChild(section);
  });
}

Part 2.3: Add Navigation Menu

(source ul-a-to-z-list_v5.js)

render() {
  // Previous code remains the same until the sections loop but we
  // need to grab the menu element in the template.
  const menu = this.shadowRoot.querySelector('#menu');

  Object.keys(sections).forEach(letter => {
    const menuItem = document.createElement('li');
    const menuLink = document.createElement('a');
    menuLink.href = `#section-${letter}`;
    menuLink.textContent = letter;
    menuItem.appendChild(menuLink);
    menu.appendChild(menuItem);

    // Rest of the section linking code goes here
  });
}

Part 2.3: Linking

(source ul-a-to-z-list_v6.js)

  // Section linking inside the `Object.keys(sections).forEach(letter => {` loop
  const section = document.createElement('ul');
      section.classList.add('letter-section');
      section.id = `section-${letter}`;

      sections[letter].forEach(item => {
        section.appendChild(item.cloneNode(true));
      });

      listContainer.appendChild(section);
  });

Part 2.3: Improve Scrolling

(source ul-a-to-z-list_v6.js)

// This is a new method, it can go after the render method in our class
scrollToSection(section) {
  const yOffset = -100;
  const y = section.getBoundingClientRect().top +
              window.pageYOffset + yOffset;

  window.scrollTo({ top: y, behavior: 'smooth' });
}

Part 2.3: Style and CSS

(source ul-a-to-z-list_v7.js)

render() {
  const template = document.createElement('template');
  template.innerHTML = `
<style>
  menu { list-style-type: none; padding: 0; }
  menu li { display: inline; margin-right: 10px; }
  .letter-section { list-style-type: none; }
  .letter-section li { text-decoration: none; font-weight: none; }
  .back-to-menu { display: block; margin-top: 20px; }
</style>
<menu id="menu"></menu>
<div id="list-container"></div> 
${this.hasAttribute('long') ? 
  '<a class="back-to-menu" href="#menu">Back to Menu</a>' : ''
}`;

  // Rest of the render method remains the same
}

Part 2.3: A final working A to Z list

(source ul-a-to-z-list.js)

/**
 * ul-a-to-z-list.js, this wraps a standard UL list providing A to Z navigation list
 */
export class AToZList extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
  }

  connectedCallback() {
    this.render();
  }

  render() {
    const template = document.createElement('template');
    template.innerHTML = `
      <style>
        menu { list-style-type: none; padding: 0; }
        menu li { display: inline; margin-right: 10px; }
        .letter-section { list-style-type: none; }
        .letter-section li { text-decoration: none; font-weight: normal; }
        .back-to-menu { display: block; margin-top: 20px; }
      </style>
      <menu id="menu"></menu>
      <div id="list-container"></div>
      ${this.hasAttribute('long') ? '<a class="back-to-menu" href="#menu">Back to Menu</a>' : ''}
    `;

    this.shadowRoot.appendChild(template.content.cloneNode(true));

    const listContainer = this.shadowRoot.querySelector('#list-container');
    const menu = this.shadowRoot.querySelector('#menu');

    const ulElement = this.querySelector('ul');
    if (!ulElement) return;

    const items = Array.from(ulElement.querySelectorAll('li'));
    const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
    const sections = {};

    items.forEach(item => {
      const firstLetter = item.textContent.trim()[0].toUpperCase();
      if (!sections[firstLetter]) {
        sections[firstLetter] = [];
      }
      sections[firstLetter].push(item);
    });

    alphabet.split('').forEach(letter => {
      if (sections[letter]) {
        const menuItem = document.createElement('li');
        const menuLink = document.createElement('a');
        menuLink.href = `#section-${letter}`;
        menuLink.textContent = letter;
        menuLink.addEventListener('click', (event) => {
          event.preventDefault();
          const targetSection = this.shadowRoot.querySelector(`#section-${letter}`);
          this.scrollToSection(targetSection);
        });
        menuItem.appendChild(menuLink);
        menu.appendChild(menuItem);

        const section = document.createElement('ul');
        section.classList.add('letter-section');
        section.id = `section-${letter}`;
        const sectionHeading = document.createElement('li');
        const sectionHeadingLink = document.createElement('a');
        sectionHeadingLink.href = `#menu`;
        sectionHeadingLink.textContent = letter;
        sectionHeadingLink.addEventListener('click', (event) => {
          event.preventDefault();
          this.scrollToSection(menu);
        });
        sectionHeading.appendChild(sectionHeadingLink);
        section.appendChild(sectionHeading);

        sections[letter].forEach(item => {
          const clonedItem = item.cloneNode(true);
          section.appendChild(clonedItem);
        });
        listContainer.appendChild(section);
      }
    });

    const backToMenuLink = this.shadowRoot.querySelector('.back-to-menu');
    if (backToMenuLink) {
      backToMenuLink.addEventListener('click', (event) => {
        event.preventDefault();
        this.scrollToSection(menu);
      });
    }
  }

  scrollToSection(section) {
    const yOffset = -100;
    const y = section.getBoundingClientRect().top + 
                      window.pageYOffset + yOffset;

    window.scrollTo({
      top: y,
      behavior: 'smooth'
    });
  }
}

customElements.define('ul-a-to-z-list', AToZList);

Part 2.3: Congratulations! You’ve created two web components!

Let’s take a quick break and stretch before moving forward

Part 2.4: Using our A to Z list

  1. Modify is htdocs/index.html
  2. Wrap the ul list with our <ul-a-to-z-list></ul-a-to-z-list>
  3. Add our component module <script type="module" src="modules/ul-a-to-z-list.js" defer></script>
  4. Test, what happens on page reload?

Part 2.4: Integrating the A to Z list

Part 2.4, Approaches

Part 2.4, Which approach?

Before picking a path review our existing code base against our constraints

Part 2.4: Think about the innerHTML, look at index_recipes.js

listRecipes()
Gets the outer element, retrieves data and then invokes populateUL()
populateUL()
Populates the UL lists with the data

Are either easy to adapt to our component?

Part 2.4: Review listRecipes()

We can keep our component general with by adapting how listRecipes() works

Part 2.4: Taking advantage of inheriting HTML element

(source index_recipes_v2.js)

async function listRecipes() {
 const aToZList = document.querySelector('ul-a-to-z-list');
 console.log(aToZList);
 const ul = document.createElement('ul');
 const data = await getRecipes();
 populateUL(ul, data);
 aToZList.innerHTML = ul.outerHTML;
 // NOTE: we need to trigger aToZList render method still.
 // Feel ugly?
 aToZList.render()
}

What does the updates look like?

Part 2.4: Problems?

Part 2.4: Ordering problem

Part 2.4: Adding an event handler to our component

  1. Update our component to use a mutation observer
  2. Remove our render call from index_recipes.js.

(source ul-a-to-z-list_v8.js) (source index_recipes_v3.js)

Part 2.4: Lessons learned

Part 2.4: Things to remember

Reference: Dataset

Reference: Web Components

Reference: Programming Languages

Reference: Data formats

Thank you for listening