Skip to content

Instantly share code, notes, and snippets.

@krhoyt
Last active April 9, 2025 19:31
Show Gist options
  • Save krhoyt/c0fbb7ad1fbb1bdf8e808115e6d62487 to your computer and use it in GitHub Desktop.
Save krhoyt/c0fbb7ad1fbb1bdf8e808115e6d62487 to your computer and use it in GitHub Desktop.
Template attempting to show the many facets of a baseline web standards-based component. Not intended to function as a component.
export default class HoytBox extends HTMLElement {
constructor() {
super();
const template = document.createElement( 'template' );
template.innerHTML = /* template */ `
<style>
:host {
box-sizing: border-box;
display: inline-block;
position: relative;
}
:host( [center] ) div {
align-items: center;
}
:host( [direction=column] ) div {
flex-direction: column;
}
:host( [direction=column-reverse] ) div {
flex-direction: column-reverse;
}
:host( [direction=row-reverse] ) div {
flex-direction: row-reverse;
}
:host( [gap=xs] ) div {
gap: 2px;
}
:host( [gap=s] ) div {
gap: 4px;
}
:host( [gap=m] ) div {
gap: 8px;
}
:host( [gap=l] ) div {
gap: 16px;
}
:host( [gap=xl] ) div {
gap: 32px;
}
:host( [grow] ) {
flex-basis: 0;
flex-grow: 1;
}
:host( [hidden] ) {
display: none;
}
:host( [justify] ) div {
justify-content: center;
}
:host( :not( [label] ) ) hoyt-label {
display: none;
}
div {
box-sizing: border-box;
display: flex;
flex-direction: row;
height: 100%;
width: 100%;
}
</style>
<hoyt-label exportparts="content" part="label"></hoyt-label>
<div part="box">
<slot></slot>
</div>
`;
// Root
this.attachShadow( {mode: 'open'} );
this.shadowRoot.appendChild( template.content.cloneNode( true ) );
// Elements
this.$label = this.shadowRoot.querySelector( 'hoyt-label' );
}
// When attributes change
_render() {
this.$label.textContent = this.label === null ? '' : this.label;
}
// Promote properties
// Values may be set before module load
_upgrade( property ) {
if( this.hasOwnProperty( property ) ) {
const value = this[property];
delete this[property];
this[property] = value;
}
}
// Setup
connectedCallback() {
this._upgrade( 'center' );
this._upgrade( 'direction' );
this._upgrade( 'gap' );
this._upgrade( 'grow' );
this._upgrade( 'hidden' );
this._upgrade( 'justify' );
this._upgrade( 'label' );
this._render();
}
// Watched attributes
static get observedAttributes() {
return [
'center',
'direction',
'gap',
'grow',
'hidden',
'justify',
'label'
];
}
// Observed attribute has changed
// Update render
attributeChangedCallback( name, old, value ) {
this._render();
}
// Attributes
// Reflected
// Boolean, Number, String, null
get center() {
return this.hasAttribute( 'center' );
}
set center( value ) {
if( value !== null ) {
if( typeof value === 'boolean' ) {
value = value.toString();
}
if( value === 'false' ) {
this.removeAttribute( 'center' );
} else {
this.setAttribute( 'center', '' );
}
} else {
this.removeAttribute( 'center' );
}
}
get direction() {
if( this.hasAttribute( 'direction' ) ) {
return this.getAttribute( 'direction' );
}
return null;
}
set direction( value ) {
if( value !== null ) {
this.setAttribute( 'direction', value );
} else {
this.removeAttribute( 'direction' );
}
}
get gap() {
if( this.hasAttribute( 'gap' ) ) {
return this.getAttribute( 'gap' );
}
return null;
}
set gap( value ) {
if( value !== null ) {
this.setAttribute( 'gap', value );
} else {
this.removeAttribute( 'gap' );
}
}
get grow() {
return this.hasAttribute( 'grow' );
}
set grow( value ) {
if( value !== null ) {
if( typeof value === 'boolean' ) {
value = value.toString();
}
if( value === 'false' ) {
this.removeAttribute( 'grow' );
} else {
this.setAttribute( 'grow', '' );
}
} else {
this.removeAttribute( 'grow' );
}
}
get hidden() {
return this.hasAttribute( 'hidden' );
}
set hidden( value ) {
if( value !== null ) {
if( typeof value === 'boolean' ) {
value = value.toString();
}
if( value === 'false' ) {
this.removeAttribute( 'hidden' );
} else {
this.setAttribute( 'hidden', '' );
}
} else {
this.removeAttribute( 'hidden' );
}
}
get justify() {
return this.hasAttribute( 'justify' );
}
set justify( value ) {
if( value !== null ) {
if( typeof value === 'boolean' ) {
value = value.toString();
}
if( value === 'false' ) {
this.removeAttribute( 'justify' );
} else {
this.setAttribute( 'justify', '' );
}
} else {
this.removeAttribute( 'justify' );
}
}
get label() {
if( this.hasAttribute( 'label' ) ) {
return this.getAttribute( 'label' );
}
return null;
}
set label( value ) {
if( value !== null ) {
this.setAttribute( 'label', value );
} else {
this.removeAttribute( 'label' );
}
}
}
window.customElements.define( 'hoyt-box', HoytBox );
export default class HoytIcon extends HTMLElement {
constructor() {
super();
const template = document.createElement( 'template' );
template.innerHTML = /* template */ `
<style>
:host {
box-sizing: border-box;
display: inline-block;
position: relative;
}
:host( [hidden] ) {
display: none;
}
i {
box-sizing: border-box;
color: var( --icon-color, #161616 );
cursor: var( --icon-cursor, default );
direction: ltr;
display: flex;
font-family: 'Material Symbols';
font-size: var( --icon-size, 24px );
font-style: normal;
font-weight: normal;
height: var( --icon-size, 24px );
letter-spacing: normal;
line-height: var( --icon-size, 24px );
margin: 0;
max-height: var( --icon-size, 24px );
max-width: var( --icon-size, 24px );
min-height: var( --icon-size, 24px );
min-width: var( --icon-size, 24px );
padding: 0;
text-align: center;
text-rendering: optimizeLegibility;
text-transform: none;
white-space: nowrap;
width: var( --icon-size, 24px );
word-wrap: normal;
}
:host( [disabled] ) i {
color: #aaaaaa;
}
:host( [size=xs] ) i {
font-size: 16px;
height: 16px;
line-height: 16px;
max-height: 16px;
max-width: 16px;
min-height: 16px;
min-width: 16px;
width: 16px;
}
:host( [size=s] ) i {
font-size: 20px;
height: 20px;
line-height: 20px;
max-height: 20px;
max-width: 20px;
min-height: 20px;
min-width: 20px;
width: 20px;
}
:host( [size=l] ) i {
font-size: 32px;
height: 32px;
line-height: 32px;
max-height: 32px;
max-width: 32px;
min-height: 32px;
min-width: 32px;
width: 32px;
}
:host( [size=xl] ) i {
font-size: 48px;
height: 48px;
line-height: 48px;
max-height: 48px;
max-width: 48px;
min-height: 48px;
min-width: 48px;
width: 48px;
}
</style>
<i part="icon"></i>
`;
// Root
this.attachShadow( {mode: 'open'} );
this.shadowRoot.appendChild( template.content.cloneNode( true ) );
// Elements
this.$icon = this.shadowRoot.querySelector( 'i' );
}
// When attributes change
_render() {
this.$icon.textContent = this.name === null ? '' : this.name;
if( this.name !== null ) {
const variation = [];
variation.push( '\'FILL\' ' + ( this.filled ? 1 : 0 ) );
variation.push( '\'wght\' ' + ( this.weight === null ? 400 : this.weight ) );
variation.push( '\'GRAD\' ' + ( this.filled ? 0 : 0 ) );
variation.push( '\'opsz\' ' + ( this.optical === null ? 24 : this.optical ) );
this.$icon.style.fontVariationSettings = variation.toString();
}
}
// Promote properties
// Values may be set before module load
_upgrade( property ) {
if( this.hasOwnProperty( property ) ) {
const value = this[property];
delete this[property];
this[property] = value;
}
}
// Setup
connectedCallback() {
this._upgrade( 'disabled' );
this._upgrade( 'filled' );
this._upgrade( 'hidden' );
this._upgrade( 'name' );
this._upgrade( 'optical' );
this._upgrade( 'size' );
this._upgrade( 'weight' );
this._render();
}
// Watched attributes
static get observedAttributes() {
return [
'disabled',
'filled',
'hidden',
'name',
'optical',
'size',
'weight'
];
}
// Observed attribute has changed
// Update render
attributeChangedCallback( name, old, value ) {
this._render();
}
// Attributes
// Reflected
// Boolean, Number, String, null
get disabled() {
return this.hasAttribute( 'disabled' );
}
set disabled( value ) {
if( value !== null ) {
if( typeof value === 'boolean' ) {
value = value.toString();
}
if( value === 'false' ) {
this.removeAttribute( 'disabled' );
} else {
this.setAttribute( 'disabled', '' );
}
} else {
this.removeAttribute( 'disabled' );
}
}
get filled() {
return this.hasAttribute( 'filled' );
}
set filled( value ) {
if( value !== null ) {
if( typeof value === 'boolean' ) {
value = value.toString();
}
if( value === 'false' ) {
this.removeAttribute( 'filled' );
} else {
this.setAttribute( 'filled', '' );
}
} else {
this.removeAttribute( 'filled' );
}
}
get hidden() {
return this.hasAttribute( 'hidden' );
}
set hidden( value ) {
if( value !== null ) {
if( typeof value === 'boolean' ) {
value = value.toString();
}
if( value === 'false' ) {
this.removeAttribute( 'hidden' );
} else {
this.setAttribute( 'hidden', '' );
}
} else {
this.removeAttribute( 'hidden' );
}
}
get name() {
if( this.hasAttribute( 'name' ) ) {
return this.getAttribute( 'name' );
}
return null;
}
set name( value ) {
if( value !== null ) {
this.setAttribute( 'name', value );
} else {
this.removeAttribute( 'name' );
}
}
get optical() {
if( this.hasAttribute( 'optical' ) ) {
return parseInt( this.getAttribute( 'optical' ) );
}
return null;
}
set optical( value ) {
if( value !== null ) {
this.setAttribute( 'optical', value );
} else {
this.removeAttribute( 'optical' );
}
}
get size() {
if( this.hasAttribute( 'size' ) ) {
return this.getAttribute( 'size' );
}
return null;
}
set size( value ) {
if( value !== null ) {
this.setAttribute( 'size', value );
} else {
this.removeAttribute( 'size' );
}
}
get weight() {
if( this.hasAttribute( 'weight' ) ) {
return parseInt( this.getAttribute( 'weight' ) );
}
return null;
}
set weight( value ) {
if( value !== null ) {
this.setAttribute( 'weight', value );
} else {
this.removeAttribute( 'weight' );
}
}
}
window.customElements.define( 'hoyt-icon', HoytIcon );
export default class HoytLabel extends HTMLElement {
constructor() {
super();
const template = document.createElement( 'template' );
template.innerHTML = /* template */ `
<style>
:host {
box-sizing: border-box;
display: inline-block;
position: relative;
}
:host( [hidden] ) {
display: none;
}
p {
box-sizing: border-box;
color: var( --label-color, #161616 );
cursor: var( --label-cursor, default );
font-family: 'IBM Plex Sans', sans-serif;
font-size: var( --label-font-size, 16px );
font-style: normal;
font-weight: var( --label-font-weight, 400 );
line-height: var( --label-line-height, 20px );
margin: 0;
padding: 0;
text-align: var( --label-text-align, left );
text-decoration: var( --label-text-decoration, none );
text-transform: var( --label-text-transform, none );
text-rendering: optimizeLegibility;
}
:host( [size=xs] ) p {
font-size: 12px;
line-height: 14px;
}
:host( [size=s] ) p {
font-size: 14px;
line-height: 18px;
}
:host( [size=l] ) p {
font-size: 18px;
line-height: 22px;
}
:host( [size=xl] ) p {
font-size: 24px;
line-height: 30px;
}
:host( [size=heading] ) p {
font-size: 32px;
line-height: 36px;
}
:host( [weight=bold] ) p {
font-weight: 600;
}
:host( [truncate] ) {
overflow: hidden;
}
:host( [truncate] ) p {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
:host( :not( [text] ) ) span {
display: none;
}
::slotted( strong ) {
font-weight: 600;
}
</style>
<p part="content">
<span></span>
<slot></slot>
</p>
`;
// Root
this.attachShadow( {mode: 'open'} );
this.shadowRoot.appendChild( template.content.cloneNode( true ) );
// Elements
this.$inline = this.shadowRoot.querySelector( 'span' );
}
// When attributes change
_render() {
this.$inline.textContent = this.text === null ? '' : this.text;
}
// Promote properties
// Values may be set before module load
_upgrade( property ) {
if( this.hasOwnProperty( property ) ) {
const value = this[property];
delete this[property];
this[property] = value;
}
}
// Setup
connectedCallback() {
this._upgrade( 'hidden' );
this._upgrade( 'size' );
this._upgrade( 'text' );
this._upgrade( 'truncate' );
this._upgrade( 'weight' );
this._render();
}
// Watched attributes
static get observedAttributes() {
return [
'hidden',
'size',
'text',
'truncate',
'weight'
];
}
// Observed attribute has changed
// Update render
attributeChangedCallback( name, old, value ) {
this._render();
}
// Attributes
// Reflected
// Boolean, Number, String, null
get hidden() {
return this.hasAttribute( 'hidden' );
}
set hidden( value ) {
if( value !== null ) {
if( typeof value === 'boolean' ) {
value = value.toString();
}
if( value === 'false' ) {
this.removeAttribute( 'hidden' );
} else {
this.setAttribute( 'hidden', '' );
}
} else {
this.removeAttribute( 'hidden' );
}
}
get size() {
if( this.hasAttribute( 'size' ) ) {
return this.getAttribute( 'size' );
}
return null;
}
set size( value ) {
if( value !== null ) {
this.setAttribute( 'size', value );
} else {
this.removeAttribute( 'size' );
}
}
get text() {
if( this.hasAttribute( 'text' ) ) {
return this.getAttribute( 'text' );
}
return null;
}
set text( value ) {
if( value !== null ) {
this.setAttribute( 'text', value );
} else {
this.removeAttribute( 'text' );
}
}
get truncate() {
return this.hasAttribute( 'truncate' );
}
set truncate( value ) {
if( value !== null ) {
if( typeof value === 'boolean' ) {
value = value.toString();
}
if( value === 'false' ) {
this.removeAttribute( 'truncate' );
} else {
this.setAttribute( 'truncate', '' );
}
} else {
this.removeAttribute( 'truncate' );
}
}
get weight() {
if( this.hasAttribute( 'weight' ) ) {
return this.getAttribute( 'weight' );
}
return null;
}
set weight( value ) {
if( value !== null ) {
this.setAttribute( 'weight', value );
} else {
this.removeAttribute( 'weight' );
}
}
}
window.customElements.define( 'hoyt-label', HoytLabel );
export default class HoytTemplate extends HTMLElement {
constructor() {
super();
const template = document.createElement( 'template' );
template.innerHTML = /* template */ `
<style>
:host {
box-sizing: border-box;
display: inline-block;
position: relative;
}
:host( [a-boolean] ) {
display: none;
}
hoyt-component::part( inner ) {
--component-stylet: var( --my-component-style, #123456 );
}
</style>
<hoyt-component exportparts="inner: outer" part="also-outer"></hoyt-component>
`;
// Properties
this._anArray = [];
this._aDate = null;
this._anObject = null;
// Consider for mobile
this._touch = ( 'ontouchstart' in document.documentElement ) ? 'touchstart' : 'click';
// Events
this.onComponentClick = this.onComponentClick.bind( this );
// Root
this.attachShadow( {mode: 'open'} );
this.shadowRoot.appendChild( template.content.cloneNode( true ) );
// Elements
this.$component = this.shadowRoot.querySelector( 'element-name' );
}
onComponentClick( evt ) {
this.dispatchEvent( new CustomEvent( 'krh-event', {
bubbles: true,
cancelable: false,
composed: true,
detail: {
'abc': 123
}
} ) );
}
// When attributes change
_render() {
this.$component.aBoolean = this.aInteger === null ? false : true;
}
// Promote properties
// Values may be set before module load
_upgrade( property ) {
if( this.hasOwnProperty( property ) ) {
const value = this[property];
delete this[property];
this[property] = value;
}
}
// Setup
connectedCallback() {
this.$component.addEventListener( 'click', this.onComponentClick );
this._upgrade( 'anArray' );
this._upgrade( 'aBoolean' );
this._upgrade( 'aDate' );
this._upgrade( 'aFloat' );
this._upgrade( 'anInteger' );
this._upgrade( 'anObject' );
this._upgrade( 'aString' );
this._render();
}
// Set down
diconnectedCallback() {
this.$component.removeEventListener( 'click', this.onComponentClick );
}
// Watched attributes
static get observedAttributes() {
return [
'a-boolean',
'a-float',
'an-integer',
'a-string'
];
}
// Observed attribute has changed
// Update render
attributeChangedCallback( name, old, value ) {
this._render();
}
// Properties
// Not reflected
// Array, Date, Object, null
get anArray() {
return this._anArray.length === 0 ? null : this._anArray;
}
set anArray( value ) {
this._anArray = value === null ? [] : [... value];
}
get aDate() {
return this._aDate;
}
set aDate( value ) {
this._aDate = value === null ? null : new Date( value.getTime() );
}
get anObject() {
return this._anObject;
}
set anObject( value ) {
this._anObject = Object.assign( value, {} );
}
// Attributes
// Reflected
// Boolean, Float, Integer, String, null
get aBoolean() {
return this.hasAttribute( 'a-boolean' );
}
set aBoolean( value ) {
if( value !== null ) {
if( typeof value === 'boolean' ) {
value = value.toString();
}
if( value === 'false' ) {
this.removeAttribute( 'a-boolean' );
} else {
this.setAttribute( 'a-boolean', '' );
}
} else {
this.removeAttribute( 'a-boolean' );
}
}
get aFloat() {
if( this.hasAttribute( 'a-float' ) ) {
return parseFloat( this.getAttribute( 'a-float' ) );
}
return null;
}
set aFloat( value ) {
if( value !== null ) {
this.setAttribute( 'a-float', value );
} else {
this.removeAttribute( 'a-float' );
}
}
get anInteger() {
if( this.hasAttribute( 'an-integer' ) ) {
return parseInt( this.getAttribute( 'an-integer' ) );
}
return null;
}
set anInteger( value ) {
if( value !== null ) {
this.setAttribute( 'an-integer', value );
} else {
this.removeAttribute( 'an-integer' );
}
}
get aString() {
if( this.hasAttribute( 'a-string' ) ) {
return this.getAttribute( 'a-string' );
}
return null;
}
set aString( value ) {
if( value !== null ) {
this.setAttribute( 'a-string', value );
} else {
this.removeAttribute( 'a-string' );
}
}
}
window.customElements.define( 'krh-template', HoytTemplate );
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment