Skip to content

Instantly share code, notes, and snippets.

@jdanyow
Created May 3, 2022 19:00
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jdanyow/274afb95f16213a0c266be9e2063178c to your computer and use it in GitHub Desktop.
Save jdanyow/274afb95f16213a0c266be9e2063178c to your computer and use it in GitHub Desktop.
Star rating web component - slot refactor
<!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>
console.log('Hello World!');
/* todo: add styles */
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment