<h1>Progressively Enchanced Stepper Web Component</h1>
<div class="two-up">
<form class="form" id="quantity-form" action="#">
<div class="field">
<label for="stepper">Quantity 1 (using data-is)</label>
<input type="number" id="stepper" name="quantity-1" value="0" data-is="my-stepper">
</div>
<div class="field">
<label for="stepper2">Quantity 2 (input as child)</label>
<my-stepper>
<input type="number" id="stepper2" name="quantity-2" value="0">
</my-stepper>
</div>
<div class="field">
<button class="button">Check form data</button>
</div>
</form>
<div class="result">
<h2>Form Data</h2>
<pre id="report"></pre>
</div>
</div>
<p>This is my attempt at upgrading a form element to an enhanced Web Component. The benefit here is if JavaScript doesn't load, the input functions like a plain old number input.</p>
<p>Since the inputs never leave the light DOM, they work seamlessly. IDs work with the labels, the form detects the values, etc.</p>
html {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
line-height: 1.5;
}
p {
max-width: 60ch;
}
input[type=number] {
-moz-appearance: textfield;
text-align: center;
border: solid 2px black;
border-radius: 6px;
max-width: 75px;
}
my-stepper:defined input[type=number]::-webkit-inner-spin-button,
my-stepper:defined input[type=number]::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
my-stepper::part(wrapper) {
position: relative;
}
my-stepper::part(control) {
position: absolute;
-webkit-appearance: none;
background: transparent;
border: none;
height: 100%;
top: 1px;
}
my-stepper::part(increment) {
right: 0;
}
.form {
background-color: #eee;
padding: 1.5rem;
}
.form > * + * {
margin-top: 0.75rem;
}
.field label {
display: block;
font-weight: 700;
}
.field > * + * {
margin-top: 0.25rem;
}
.button {
-webkit-appearance: none;
border: none;
font: inherit;
font-weight: 700;
color: white;
background-color: #333;
border-radius: 6px;
}
.result {
padding: 1.5rem;
}
.result > * {
margin: 0;
}
.result > * + * {
margin-top: 0.75rem;
}
.two-up {
display: flex;
width: min(100%, 80ch);
flex-wrap: wrap;
}
.two-up > * {
flex-grow: 1;
}
const template = document.createElement("template");
template.innerHTML = `
<span part="wrapper">
<button part="decrement control" tabindex="-1" aria-hidden="true">−</button>
<slot>REPLACE ME</slot>
<button part="increment control" tabindex="-1" aria-hidden="true">+</button>
<span>
`;
class MyStepper extends HTMLElement {
static validateElement(el) {
if (notNumberInput(el)) throw new Error('Must be an input of type number!');
}
constructor() {
super();
this.attachShadow({ mode: "open" });
this.shadowRoot.appendChild(template.content.cloneNode(true));
}
connectedCallback() {
if (this.isConnected) {
this.increment = this.increment.bind(this);
this.decrement = this.decrement.bind(this);
this.shadowRoot.querySelector("[part*=decrement]").addEventListener('click', this.decrement);
this.shadowRoot.querySelector("[part*=increment]").addEventListener('click', this.increment);
}
}
disconnectedCallback() {
this.shadowRoot.querySelector("[part*=decrement]").removeEventListener('click', this.decrement);
this.shadowRoot.querySelector("[part*=increment]").removeEventListener('click', this.increment);
}
increment(event) {
if (this.firstElementChild) {
this.firstElementChild.stepUp();
}
}
decrement(event) {
if (this.firstElementChild) {
this.firstElementChild.stepDown();
}
}
}
customElements.define("my-stepper", MyStepper);
function notNumberInput(el) {
return !(el instanceof HTMLInputElement && el.type === "number");
}
document.querySelectorAll("[data-is]").forEach((el) => {
const cEl = el.dataset.is;
delete el.dataset.is;
if (cEl) {
const CElement = customElements.get(cEl);
CElement.validateElement?.(el);
const cElInstance = new CElement();
el.after(cElInstance);
cElInstance.append(el);
}
});
// form stuff here
const form = document.getElementById("quantity-form");
form.addEventListener("submit", function (event) {
event.preventDefault();
const data = new FormData(form);
const entryObj = {};
const reportDiv = document.getElementById('report')
for (const [key, value] of data.entries()) {
entryObj[key] = value;
}
reportDiv.textContent = JSON.stringify(entryObj, null, 2);
});
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.