Created
May 3, 2022 19:00
-
-
Save jdanyow/274afb95f16213a0c266be9e2063178c to your computer and use it in GitHub Desktop.
Star rating web component - slot refactor
This file contains hidden or 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>GistRun</title> | |
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@microsoft/atlas-css/dist/index.css"> | |
<!-- fake design system styles or application styles to illustrate styling the "public interface" of the web component (the component itself as well as "parts" it's exposed for styling) --> | |
<style> | |
.visually-hidden, | |
*::part(visually-hidden) { | |
clip: rect(0 0 0 0); | |
clip-path: inset(50%); | |
height: 1px; | |
overflow: hidden; | |
position: absolute; | |
white-space: nowrap; | |
width: 1px; | |
} | |
.custom-star-rating-1 { | |
color: darkcyan; | |
} | |
.custom-star-rating-2 { | |
color: chocolate; | |
} | |
.custom-star-rating-2::part(star) { | |
color: deeppink; | |
stroke: rebeccapurple; | |
} | |
</style> | |
</head> | |
<body> | |
<!-- application html --> | |
<form class="padding-sm" id="rating-form"> | |
<star-rating class="margin-bottom-sm" name="rating-1" value="4"> | |
<legend slot="legend">How are we doing?</legend> | |
<span slot="label-1">Terrible</span> | |
<span slot="label-2">Poor</span> | |
<span slot="label-3">Fair</span> | |
<span slot="label-4">Good</span> | |
<span slot="label-5">Great</span> | |
</star-rating> | |
<star-rating class="theme-dark padding-sm margin-bottom-sm" name="rating-1" value="4"> | |
<legend slot="legend">How are <strong>we doing</strong>?</legend> | |
<span slot="label-1">Terrible</span> | |
<span slot="label-2">Poor</span> | |
<span slot="label-3">Fair</span> | |
<span slot="label-4">Good</span> | |
<span slot="label-5">Great</span> | |
</star-rating> | |
<star-rating class="margin-bottom-sm custom-star-rating-1" name="rating-2" value="1"> | |
<legend slot="legend">Did this answer tickle your fancy?</legend> | |
<span slot="label-1">Not in the <strong>slightest</strong></span> | |
<span slot="label-2">No</span> | |
<span slot="label-3">Maybe</span> | |
<span slot="label-4">A bit</span> | |
<span slot="label-5">Yes!</span> | |
</star-rating> | |
<star-rating class="margin-block-sm custom-star-rating-2" name="rating-3" value="2"> | |
<legend slot="legend">Did this web component make you <strong>smile</strong>?</legend> | |
<span slot="label-1">Not in the slightest</span> | |
<span slot="label-2">No</span> | |
<span slot="label-3">Maybe</span> | |
<span slot="label-4">A bit</span> | |
<span slot="label-5">Yes!</span> | |
</star-rating> | |
<star-rating class="margin-block-sm" id="disabled-rating" name="rating-4" value="3" disabled required> | |
<legend slot="legend">Enabling and disabling, value and name changing, via javascript api <span style="color: red">(REQUIRED)</span></legend> | |
<span slot="label-1">Not in the slightest</span> | |
<span slot="label-2">No</span> | |
<span slot="label-3">Maybe</span> | |
<span slot="label-4">A bit</span> | |
<span slot="label-5">Yes!</span> | |
</star-rating> | |
<button type="submit" class="button">Submit</button> | |
<ul class="margin-block-sm"> | |
<li><a href="https://web.dev/more-capable-form-controls/">https://web.dev/more-capable-form-controls/</a></li> | |
<li><a href="https://dev.to/43081j/using-css-shadow-parts-in-web-components-7h5">https://dev.to/43081j/using-css-shadow-parts-in-web-components-7h5</a></li> | |
<li><a href="https://css-tricks.com/styling-web-components/">https://css-tricks.com/styling-web-components/</a></li> | |
<li><a href="https://medium.com/swlh/adopt-a-design-system-inside-your-web-components-with-constructable-stylesheets-dd24649261e">https://medium.com/swlh/adopt-a-design-system-inside-your-web-components-with-constructable-stylesheets-dd24649261e</a></li> | |
</ul> | |
</form> | |
<!-- application code --> | |
<script> | |
const form = document.getElementById('rating-form'); | |
form.addEventListener('submit', event => { | |
event.preventDefault(); | |
// read the form data and convert it to an object. | |
const data = Object.fromEntries(new FormData(form)); | |
// display the data | |
alert(JSON.stringify(data, null, 2)) | |
}); | |
// try out the javascript api (name, value, disabled) | |
setInterval(() => { | |
const rating = document.getElementById('disabled-rating'); | |
rating.disabled = !rating.disabled; | |
rating.value = rating.value === '' ? '5' : parseInt(rating.value) - 1; | |
rating.name = `rating-${new Date().toISOString()}`; | |
}, 1000); | |
</script> | |
<!-- web component template --> | |
<template id="star-rating-template"> | |
<style type="text/css"> | |
*, ::before, ::after { | |
box-sizing: border-box; | |
} | |
:host { | |
display: block; | |
} | |
fieldset { | |
display: flex; | |
gap: 3px; | |
align-items: center; | |
border: none; | |
margin: 0; | |
padding: 0; | |
} | |
svg { | |
fill: none; | |
stroke: currentColor; | |
} | |
label:hover > svg, | |
input:checked + label > svg { | |
fill: currentColor; | |
} | |
input:focus-visible + label { | |
outline-width: 2px; | |
outline-style: dashed; | |
} | |
/* checked styles */ | |
[id^="label-"] { | |
display: none; | |
} | |
input[value="1"]:checked ~ #alert #label-1, | |
input[value="2"]:checked ~ #alert #label-2, | |
input[value="3"]:checked ~ #alert #label-3, | |
input[value="4"]:checked ~ #alert #label-4, | |
input[value="5"]:checked ~ #alert #label-5 { | |
display: inline; | |
} | |
/* override checked styles with hover styles */ | |
input:hover ~ #alert [id^="label-"] { | |
display: none !important; | |
} | |
input[value="1"]:hover ~ #alert #label-1, | |
input[value="2"]:hover ~ #alert #label-2, | |
input[value="3"]:hover ~ #alert #label-3, | |
input[value="4"]:hover ~ #alert #label-4, | |
input[value="5"]:hover ~ #alert #label-5 { | |
display: inline !important; | |
} | |
/* disabled styles */ | |
:disabled svg { | |
stroke: gray !important; | |
} | |
:disabled input:checked + label > svg { | |
fill: gray !important; | |
} | |
</style> | |
<fieldset> | |
<slot name="legend">Enter rating</slot> | |
<input type="radio" value="1" id="radio-1" part="visually-hidden" /> | |
<label for="radio-1"><svg part="star" aria-label="5" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 51 48"><path d="m25,1 6,17h18l-14,11 5,17-15-10-15,10 5-17-14-11h18z"/></svg></label> | |
<input type="radio" value="2" id="radio-2" part="visually-hidden" /> | |
<label for="radio-2"><svg part="star" aria-label="5" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 51 48"><path d="m25,1 6,17h18l-14,11 5,17-15-10-15,10 5-17-14-11h18z"/></svg></label> | |
<input type="radio" value="3" id="radio-3" part="visually-hidden" /> | |
<label for="radio-3"><svg part="star" aria-label="5" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 51 48"><path d="m25,1 6,17h18l-14,11 5,17-15-10-15,10 5-17-14-11h18z"/></svg></label> | |
<input type="radio" value="4" id="radio-4" part="visually-hidden" /> | |
<label for="radio-4"><svg part="star" aria-label="5" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 51 48"><path d="m25,1 6,17h18l-14,11 5,17-15-10-15,10 5-17-14-11h18z"/></svg></label> | |
<input type="radio" value="5" id="radio-5" part="visually-hidden" /> | |
<label for="radio-5"><svg part="star" aria-label="5" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 51 48"><path d="m25,1 6,17h18l-14,11 5,17-15-10-15,10 5-17-14-11h18z"/></svg></label> | |
<span id="alert" aria-live="polite"> | |
<span id="label-1"><slot name="label-1"></slot></span> | |
<span id="label-2"><slot name="label-2"></slot></span> | |
<span id="label-3"><slot name="label-3"></slot></span> | |
<span id="label-4"><slot name="label-4"></slot></span> | |
<span id="label-5"><slot name="label-5"></slot></span> | |
</span> | |
</fieldset> | |
</template> | |
<!-- web component javascript --> | |
<script> | |
const template = document.getElementById("star-rating-template"); | |
class StarRatingElement extends HTMLElement { | |
static get observedAttributes() { return ['name', 'value', 'disabled', 'required']; } | |
coercedValue = ''; | |
constructor() { | |
super(); | |
this.attachShadow({ mode: "open" }); | |
this.shadowRoot.appendChild(template.content.cloneNode(true)); | |
partPolyfill(this.shadowRoot); | |
} | |
get type() { return 'star-rating'; } | |
get name() { return this.getAttribute('name') ?? ''; } | |
set name(value) { this.setAttribute('name', value); } | |
get value() { return this.coercedValue; }; | |
set value(value) { | |
value = String(value); | |
this.coercedValue = ['', '1', '2', '3', '4', '5'].includes(value) ? value : ''; | |
const checkbox = this.shadowRoot.querySelector(`[value="${this.coercedValue}"]`); | |
if (checkbox){ | |
checkbox.checked = true; | |
} else { | |
const uncheck = this.shadowRoot.querySelector(':checked'); | |
if (uncheck) { | |
uncheck.checked = false; | |
} | |
} | |
} | |
get disabled() { return this.hasAttribute('disabled'); } | |
set disabled(value) { this.toggleAttribute('disabled', value); } | |
get required() { return this.hasAttribute('required'); } | |
set required(value) { this.toggleAttribute('required', value); } | |
get validity() { return this.shadowRoot.querySelector('input').validity; } | |
connectedCallback() { | |
this.shadowRoot.addEventListener('change', this); | |
this.closest('form')?.addEventListener('formdata', this); | |
} | |
disconnectedCallback() { | |
this.shadowRoot.removeEventListener('change', this); | |
this.closest('form')?.removeEventListener('formdata', this); | |
} | |
attributeChangedCallback(name, oldValue, newValue) { | |
if (name === 'value') { | |
this.value = newValue; | |
} else if (name === 'disabled') { | |
this.shadowRoot.querySelector('fieldset').disabled = newValue !== null; | |
} else if (name === 'name') { | |
this.shadowRoot.querySelectorAll('input[type="radio"]').forEach(input => input.name = newValue); | |
} else if (name === 'required') { | |
this.shadowRoot.querySelectorAll('input[type="radio"]').forEach(input => input.required = newValue !== null); | |
} | |
// console.log(`name: "${this.name}""; value: "${this.value}"; disabled: ${this.disabled}; required: ${this.required}; validity.valueMissing: ${this.validity.valueMissing};`); | |
} | |
handleEvent(event) { | |
switch(event.type) { | |
case 'change': | |
this.setAttribute('value', event.target.value); | |
this.dispatchEvent(new Event('change', { bubbles: true })); | |
break; | |
case 'formdata': | |
// https://web.dev/more-capable-form-controls/ | |
event.formData.append(this.name, this.value); | |
break; | |
} | |
} | |
} | |
customElements.define('star-rating', StarRatingElement); | |
// need to test in browserstack... | |
function partPolyfill(root) { | |
// Firefox for Android does not support the "part" attribute but it does support the DOM property. | |
// Copy the attribute value to the DOM property... | |
// https://caniuse.com/mdn-html_global_attributes_part | |
root.querySelectorAll('[part]').forEach(el => { el.part = el.getAttribute('part'); }); | |
} | |
</script> | |
</body> | |
</html> |
This file contains hidden or 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
console.log('Hello World!'); |
This file contains hidden or 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
/* todo: add styles */ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment