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>
”<ul-a-to-z-list></ul-a-to-z-list>
” (for
listing our recipes)htdocs
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
among friends.
datasetd recipes_api.yaml
connectedCallback()
methodcustomElements.define()
method
customElements.define( 'hello-clock', HelloClock );
connectedCallback()
method?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
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 );
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/ul-a-to-z-list.html
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
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>
(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.
(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?
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?
(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);
(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?
(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);
});
}
(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
});
}
(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);
});
(source ul-a-to-z-list_v6.js)
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 ul-a-to-z-list_v6.js)
(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
}
(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);
<hello-clock>
(minimal)<ul-a-to-z-list>
(complex)Let’s take a quick break and stretch before moving forward
htdocs/index.html
ul
list with our
<ul-a-to-z-list></ul-a-to-z-list>
<script type="module" src="modules/ul-a-to-z-list.js" defer></script>
index_recipes.js
functions don’t see the internal
UL list nowBefore picking a path review our existing code base against our constraints
index_recipes.js
listRecipes()
populateUL()
populateUL()
Are either easy to adapt to our component?
listRecipes()
<ul-a-to-z-list></ul-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('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?
aToZList.render()
to
update?aToZList.innerHTML = ul.outerHTML
?index_recipes.js
.(source ul-a-to-z-list_v8.js) (source index_recipes_v3.js)