Skip to content

Instantly share code, notes, and snippets.

@cferdinandi
Created October 23, 2024 16:33
Show Gist options
  • Save cferdinandi/58d272dd09ef4fe76609b7ae9be89c58 to your computer and use it in GitHub Desktop.
Save cferdinandi/58d272dd09ef4fe76609b7ae9be89c58 to your computer and use it in GitHub Desktop.
How to extend a Web Component with Custom Events. Watch the tutorial here: https://youtu.be/fCXUwkei0uk
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Pick at Random</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style type="text/css">
body {
margin: 1em auto;
max-width: 40em;
width: 88%;
}
pick-at-random [role="status"]:not(:empty) {
background-color: #f7f7f7;
border: 1px solid #e5e5e5;
border-radius: 0.25em;
padding: 0.5rem 1rem;
margin-top: 0.5rem;
}
pick-at-random li button {
background-color: transparent;
border: 0;
color: inherit;
margin-inline-start: 0.5em;
padding: 0;
}
pick-at-random li button svg {
margin-bottom: -0.25em;
}
pick-at-random [clear-list] {
background-color: transparent;
border: 0;
font: inherit;
color: inherit;
padding: 0;
}
</style>
</head>
<body>
<h1>Pick at Random</h1>
<pick-at-random
add-label="Add a Person"
add-button="Add Person"
add-status='You added "${value}" to the list!'
pick-button="Pick a Person"
clear-button="or remove everyone"
clear-all-confirm="You sure, bro?"
remove-status="You have removed ${value}"
selected-status='"${value}" has to drive!'
remove-button="❌"
remove-label="Get rid of ${value}"
local-storage="designed-driver"
></pick-at-random>
<br><br><br><br>
<pick-at-random local-storage="rando"></pick-at-random>
<script>
// document.addEventListener('pick-at-random:ready', function (event) {
// console.log(event.target, event.detail);
// });
// document.addEventListener('pick-at-random:add-item', function (event) {
// console.log(event.target, event.detail);
// });
// document.addEventListener('pick-at-random:before-add-item', function (event) {
// if (event.detail.items.length > 4) {
// event.preventDefault();
// console.log(`Can't add any more items, sorry!`);
// }
// });
document.addEventListener('pick-at-random:before-add-item', function (event) {
console.log('before-add', event.detail.newItem);
});
document.addEventListener('pick-at-random:add-item', function (event) {
console.log('add', event.detail.newItem);
});
document.addEventListener('pick-at-random:before-clear-list', function (event) {
console.log('before-clear-list', event.detail.items);
});
document.addEventListener('pick-at-random:clear-list', function (event) {
console.log('clear-list');
});
document.addEventListener('pick-at-random:before-remove-item', function (event) {
console.log('before-remove-item', event.detail.item);
});
document.addEventListener('pick-at-random:remove-item', function (event) {
console.log('remove-item', event.detail.item);
});
document.addEventListener('pick-at-random:before-pick-item', function (event) {
console.log('before-pick-item', event.detail.pickedItem);
});
document.addEventListener('pick-at-random:pick-item', function (event) {
console.log('pick-item', event.detail.pickedItem);
});
customElements.define('pick-at-random', class extends HTMLElement {
/**
* Instantiate the component
*/
constructor () {
// Inherits parent class properties
super();
// Create a unique ID for the instance
this.uuid = `pick-${crypto.randomUUID()}`;
// Settings
this.settings = {
addLabel: this.getAttribute('add-label') || 'Add an Item',
addButton: this.getAttribute('add-button') || 'Add Item',
pickButton: this.getAttribute('pick-button') || 'Pick an Item',
clearButton: this.getAttribute('clear-button') || 'or remove all items',
addStatus: this.getAttribute('add-status') || '"${value}" has been added to the list.',
clearAllConfirm: this.getAttribute('clear-all-confirm') || 'Are you sure you want to do this? It cannot be undone.',
removeStatus: this.getAttribute('remove-status') || '"${value}" has been removed from the list.',
selectedStatus: this.getAttribute('selected-status') || 'You picked ${value}',
removeButton: this.getAttribute('remove-button') || `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16" aria-hidden="true"><path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708"/></svg>`,
removeLabel: this.getAttribute('remove-label') || 'Remove ${value}',
localStorageID: this.getAttribute('local-storage')
};
// Render our initial HTML
this.innerHTML =
`<form>
<label for="${this.uuid}">${this.settings.addLabel}</label>
<input type="text" id="${this.uuid}">
<button>${this.settings.addButton}</button>
</form>
<ul></ul>
<p><button pick-item>${this.settings.pickButton}</button> <button clear-list>${this.settings.clearButton}</button></p>
<div role="status" pick-result></div>`;
// Get our elements
this.form = this.querySelector('form');
this.list = this.querySelector('ul');
this.field = this.form.querySelector('input');
this.pickBtn = this.querySelector('[pick-item]');
this.result = this.querySelector('[pick-result]');
// Listen for event
this.form.addEventListener('submit', this);
this.addEventListener('click', this);
// Load list from localStorage
this.loadItems();
// Emit ready event
this.emit('ready', {
items: this.getItems()
});
}
/**
* Handle events
* @param {Event} event The event object
*/
handleEvent (event) {
this[`on${event.type}`](event);
}
/**
* Handle submit events
* @param {Event} event The event object
*/
onsubmit (event) {
// Stop the form from reloading the page
event.preventDefault();
// If there's no item to add, bail
if (!this.field.value.length) return;
// Get item value
let val = this.field.value;
// Emit before add item event
let cancel = !(this.emit('before-add-item', {
newItem: val,
items: this.getItems()
}));
// If the event was canceled, bail early
if (cancel) return;
// Create a list item
this.createListItem(val);
// Show a status message
this.showStatus(this.settings.addStatus.replace('${value}', this.field.value));
// Clear text from field
this.field.value = '';
// Save our list to localStorage
this.saveItems();
// Emit item added event
this.emit('add-item', {
newItem: val,
items: this.getItems()
});
}
/**
* Handle click event
* @param {Event} event The event object
*/
onclick (event) {
this.onPickButton(event);
this.onRemove(event);
this.onClearList(event);
}
/**
* Clear the list of items
* @param {Event} event The event object
*/
onClearList (event) {
// Only run on [clear-list] button
if (!event.target.closest('[clear-list]')) return;
// Double check the user actually wants to do this
let doClear = confirm(this.settings.clearAllConfirm);
if (!doClear) return;
// Emit before event
let cancel = !(this.emit('before-clear-list', {
items: this.getItems()
}));
// If the event was canceled, bail early
if (cancel) return;
// Clear the list
this.list.innerHTML = '';
// Remove items from localStorage
this.removeItems();
// Emit custom event
this.emit('clear-list');
}
/**
* Handle remove button click
* @param {Event} event The event object
*/
onRemove (event) {
// Only run on remove buttons
let btn = event.target.closest('[data-remove]');
if (!btn) return;
let txt = btn.getAttribute('data-remove');
// Get the list item
let li = event.target.closest('li');
if (!li) return;
// Emit before event
let itemText = li.textContent.replace(this.settings.removeButton, '');
let cancel = !(this.emit('before-remove-item', {
item: itemText,
items: this.getItems()
}));
// If the event was canceled, bail early
if (cancel) return;
// Remove it
li.remove();
// Show remove message
this.showStatus(this.settings.removeStatus.replace('${value}', txt));
// Save updated list
this.saveItems();
// Emit custom event
this.emit('remove-item', {
item: itemText,
items: this.getItems()
});
}
/**
* Handle pick button click
* @param {Event} event The event object
*/
onPickButton (event) {
// Only run on [pick-item] button
if (!event.target.closest('[pick-item]')) return;
// Get all of the list items
let items = this.getItems();
if (!items.length) return;
// Randomize the items
this.shuffle(items);
// Emit before event
let cancel = !(this.emit('before-pick-item', {
pickedItem: items[0],
items: items
}));
// If the event was canceled, bail early
if (cancel) return;
// Show the result
this.result.textContent = this.settings.selectedStatus.replace('${value}', items[0]);
// Emit custom event
this.emit('pick-item', {
pickedItem: items[0],
items: items
});
}
/**
* Create list item
* @param {String} The text to add to the item
*/
createListItem (txt) {
// Create list item
let li = document.createElement('li');
li.textContent = txt;
// Create remove button
let btn = document.createElement('button');
btn.innerHTML = this.settings.removeButton;
btn.setAttribute('aria-label', this.settings.removeLabel.replace('${value}', txt));
btn.setAttribute('data-remove', txt);
li.append(btn);
this.list.append(li);
}
/**
* Get an array of user-added items
* @return {Array} The items
*/
getItems () {
return Array.from(this.list.querySelectorAll('li')).map((item) => {
return item.textContent.replace(this.settings.removeButton, '');
});
}
/**
* Save items to localStorage
*/
saveItems () {
if (!this.settings.localStorageID) return;
let items = JSON.stringify(this.getItems());
localStorage.setItem(`pickAtRandom_${this.settings.localStorageID}`, items);
}
/**
* Remove items to localStorage
*/
removeItems () {
if (!this.settings.localStorageID) return;
localStorage.removeItem(`pickAtRandom_${this.settings.localStorageID}`);
}
/**
* Load saved list from localStorage
*/
loadItems () {
if (!this.settings.localStorageID) return;
let items = JSON.parse(localStorage.getItem(`pickAtRandom_${this.settings.localStorageID}`));
if (!items) return;
for (let item of items) {
this.createListItem(item);
}
}
/**
* Show a status message in the form
* @param {String} msg The message to display
*/
showStatus (msg) {
// Create a notification
let notification = document.createElement('div');
notification.setAttribute('role', 'status');
// Inject it into the DOM
this.form.append(notification);
// Add text after it's in the UI
setTimeout(function () {
notification.textContent = msg;
}, 1);
// Remove it after 4 seconds
setTimeout(function () {
notification.remove();
}, 4000);
}
/**
* Randomly shuffle an array
* https://stackoverflow.com/a/2450976/1293256
* @param {Array} array The array to shuffle
* @return {Array} The shuffled array
*/
shuffle (array) {
let currentIndex = array.length;
let temporaryValue, randomIndex;
// While there remain elements to shuffle...
while (0 !== currentIndex) {
// Pick a remaining element...
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex -= 1;
// And swap it with the current element.
temporaryValue = array[currentIndex];
array[currentIndex] = array[randomIndex];
array[randomIndex] = temporaryValue;
}
return array;
}
/**
* Emit a custom event
* (c) Chris Ferdinandi, MIT License, https://gomakethings.com
* @param {String} type The event type
* @param {Object} detail Any details to pass along with the event
*/
emit (type, detail = {}) {
// Create a new event
let event = new CustomEvent(`pick-at-random:${type}`, {
bubbles: true,
cancelable: true,
detail: detail
});
// Dispatch the event
return this.dispatchEvent(event);
}
});
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment