Created
October 23, 2024 16:33
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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