R. S. Doiel, rsdoiel@caltech.edu
Caltech Library, Digital Library Development
TBD
Welcome everyone,
This presentation builds on what we built in the Part 1.
This workshop is focused on enhancing our application using Web Components.
<hello-clock></hello-clock>
”<a-to-z-list></a-to-z-list>
” (for listing our
recipes)<csv-textarea></csv-textarea>
from CL-web-components
for our ingredient listshtdocs
directory under
htdocs1
recipes_api.yaml
as
recipes_api1.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.
datasetd recipes_api.yaml
connectedCallback()
methodcustomElements.define()
method
customElements.define( 'hello-clock', HelloClock );
This will display something like, “Hi There! 09:27:23 GMT-0700 (Pacific Daylight Time)”.
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 );
Let’s take a quick break and stretch before moving forward
Q: What does the A to Z list web component do?
A: Wraps a standard UL list providing A to Z navigation.
htdocs/a-to-z-list.html
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
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>
(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.
(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?
(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?
(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);
(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?
(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);
});
}
(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
});
}
(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);
});
(source a-to-z-list_v6.js)
Objective: Implement “Back to Menu” link.
render() {
// This code goes below the `Object.keys(section).foreach( ...` loop
const backToMenuLink = this.shadowRoot.querySelector('.back-to-menu');
if (backToMenuLink) {
backToMenuLink.addEventListener('click', (event) => {
event.preventDefault();
this.scrollToSection(menu);
});
}
// Add event listeners to menu links for smooth scrolling
const menuLinks = this.shadowRoot.querySelectorAll('menu li a');
menuLinks.forEach(link => {
link.addEventListener('click', (event) => {
event.preventDefault();
const targetSection = this.shadowRoot.querySelector(link.getAttribute('href'));
this.scrollToSection(targetSection);
});
});
}
(source a-to-z-list_v6.js)
Objective: Implement smooth scrolling by providing a new method
(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
}
(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);
hello-clock
a-to-z-list
Congratulations! Time to update our application.
htdocs/index.html
ul
list with our
<a-to-z-list></a-to-z-list>
<script type="model" src="modules/a-to-z-list.js"></script>
index_recipes.js
functions don’t see the internal
UL list nowBefore picking a path review our existing codebase against our constraints
index_recipes.js
listRecipes()
populateUL()
populateUL()
Are either easy to adapt to our component?
listRecipes()
<a-to-z-list></a-to-z-list>
listRecipes()
triggers retrieving the datapopulateUL()
which populates that
innerHTML!We can keep our component general with by adapting how
listRecipes()
works
(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?
aToZList.render()
to
update?aToZList.innerHTML = ul.outerHTML
?index_recipes.js
.(source a-to-z-list_v8.js) (source index_recipes_v3.js)
<csv-textarea></csv-textarea>
<csv-textarea></csv-textara>
do?
<csv-textarea></csv-textarea>
Next steps
utils.js
by adding a saveRecipe
functionOn 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
<csv-textarea></csv-textarea>
What happens when I press “save” button?
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
Include our web component module in the head.
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>
Add the following at the bottom of the page before the
</body>
.
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?