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 amoung 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: 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
    this.textContent = `${this.textContent} ${d.toTimeString()}`.trim();
  }
}
// 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="style" href="css/style.css">
        <script type="module" src="modules/hello-clock.js"></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/a-to-z-list.html
  2. Create our Web Component, htdocs/modules/a-to-z-list.js

macOS and Linux:

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

Windows:

New-Item -Path htdocs/a-to-z-list.html ; New-Item htdocs/modules/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, a-to-z-list.html.

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

        <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>
        </a-to-z-list>
    </body>
</html>

Part 2.3: Building an A to Z list web component

(source 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('a-to-z-list', AToZList);

NOTE: the constructor and that the web component does do any thing yet. Open the web page.

Part 2.3: Introducing Shadow DOM

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

You can build your web component in the Shadow DOM that way you can sprinkle it into your document as needed. 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('a-to-z-list', AToZList);

Reload you web page, what does does it look like?

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

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

We use the connectedCallback() method to to call a render method. This is what makes our Shadow DOM take control.

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">This is where our A to List will go</div>`;

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

What happened in our web page?

Part 2.3: Basic Structure of our component using Shadow DOM

(source 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('a-to-z-list', AToZList);

Part 2.3: Display Items

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

Objective: Display the list items in the component without any categorization.

  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?

Part 2.3: Categorize Items by Letter

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

Objective: Organize items by their starting letter.

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 a-to-z-list_v5.js)

Objective: Add a navigation menu to jump to sections by letter.

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: Complete our section linking

(source 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 a-to-z-list_v6.js)

Objective: Implement smooth scrolling by providing a new method

// 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: Styling

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

Objective: Add final styling and conditional rendering based on attributes.

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 a-to-z-list.js)

/**
 * 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('a-to-z-list', AToZList);

Part 2.3: You’ve created two web components

Congratulations! Time to update our application.

Part 2.4: Using our A to Z list

  1. Modify is htdocs/index.html
  2. Wrap the ul list with our <a-to-z-list></a-to-z-list>
  3. Add our component module <script type="model" src="modules/a-to-z-list.js"></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 codebase 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('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 a-to-z-list_v8.js) (source index_recipes_v3.js)

Part 2.4: Lessons learned

Part 2.5: Introducing CSV Textarea Web Component, <csv-textarea></csv-textarea>

Part 2.5: Using the CSV Textarea Web Component, <csv-textarea></csv-textarea>

Next steps

  1. Go to CL-web-components
  1. Copy the component into your modules directory
  2. Update your HTML markup
  3. Update utils.js by adding a saveRecipe function
  4. Test

Part 2.5: Retrieving csv-textarae.js

On macOS or Linux.

curl -L -o htdocs/modules/csv-textarea.js \
  https://raw.githubusercontent.com/caltechlibrary/CL-web-components/refs/heads/main/csv-textarea.js 

On Windows

irm `
  https://raw.githubusercontent.com/caltechlibrary/CL-web-components/refs/heads/main/csv-textarea.js `
  -Outfile ./htdocs/modules/csv-textarea.js

Part 2.5: Using the CSV Textarea Web Component, <csv-textarea></csv-textarea>

What happens when I press “save” button?

Part 2.5: Fixing web form submission

The utils.js needs asaveRecipe function.

macOS and Linux

curl -L -o htdocs/modules/utils.js \
  https://raw.githubusercontent.com/caltechlibrary/t2t3_dataset_web_apps/refs/heads/main/htdocs2/modules/utils.js

Windows

irm `
  https://raw.githubusercontent.com/caltechlibrary/t2t3_dataset_web_apps/refs/heads/main/htdocs2/modules/utils.js `
  -Output htdocs/modules/utils.js

Part 2.5: Fixing web form submission

Part 2.5: Update the HEAD of edit_replace.html

Include our web component module in the head.

  <head>
    <title>A recipe collection</title>
    <link rel="style" href="css/style.css">
    <script type="module" src="modules/csv-textarea.js"></script>
    <script type="module" src="modules/edit_recipe.js"></script>
  </head>

Part 2.5: Update the HTML for edit_recipe.html

Wrap our ingredients textarea in a csv-textarea.

  <csv-textarea id="ingredients" name="ingredients"          
    title="ingredient,units (CSV data)" placeholder="flour,2 cups"
    cols="60"rows="10" 
    column-headings="Ingredients,Units" debug="true">
    <textarea id="ingredients" name="ingredients"
      title="ingredient,units (CSV data)" placeholder="flour,2 cups"
      cols="60"rows="10">
    </textarea>
  </csv-textarea>

Part 2.5: Update the HTML for edit_recipe.html

Add the following at the bottom of the page before the </body>.

<script type="module">
  import { saveRecipe } from './modules/utils.js';
  document.addEventListener('DOMContentLoaded', () => {
    const form = document.addEventListener('submit', saveRecipe);
  });
</script>

Part 2.5: Test updates

Part 3: Exploring further, browser side

Do web components contradict the division of responsibilities?

Is it OK to require JavaScript in a web page?

Is progressive enhancement still relevant in 2025?

Part 3: Exploring further, server side

Part 3: What I’ve learned

Reference: Dataset

Reference: CL-web-components

csv-textarea
Wraps a textarea element and presents a editable table of cells
a-to-z-list
Wraps a UL list and creates an A to Z list
sortable-table
Wraps an HTML table making it sort-able and filterable on a column

Reference: Web Components

Reference: Programming Languages

Reference: Data formats

Thank you for listening