Last active
September 28, 2024 00:52
-
-
Save monokee/03230511f1e2214dc1f0b17763d85369 to your computer and use it in GitHub Desktop.
Tiny customElement wrapper that enables scalable web component architecture. Define custom elements with a configuration object that separates markup from css and javascript. Uses a slotted light DOM (no shadow DOM) to allow for powerful component extension, composition and easier styling with external stylesheets and global css variables. Expor…
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
/** | |
* Tiny customElement wrapper that enables scalable web component architecture. | |
* Define custom elements with a configuration object that separates markup from css and javascript. | |
* Uses a slotted light DOM (no shadow DOM) to allow for powerful component extension, | |
* composition and easier styling with external stylesheets and global css variables. | |
* Exports a component class that can be imported and explicitly used to be picked up by module bundlers. | |
* See comments for examples and GNU license below. | |
*/ | |
export function defineComponent(name, config) { | |
const [USED, INIT] = defineComponent.x || (defineComponent.x = [Symbol(), Symbol()]); | |
const TEMPLATE = config.element ? Object.assign(document.createElement('template'), { | |
innerHTML: config.element | |
}).content : null; | |
const HAS_SLOTS = TEMPLATE ? TEMPLATE.querySelector('slot') !== null : false; | |
const REFS = defineComponent.y || (defineComponent.y = { | |
get: (comp, ref) => comp.querySelector(`[ref="${ref}"]`) | |
}); | |
const STYLESHEET = defineComponent.z || (defineComponent.z = document.head.appendChild(document.createElement('style'))); | |
if (config.style) { | |
STYLESHEET.innerHTML += config.style.replaceAll(':host', `:is(${name}, [extends*="${name}"])`); | |
} | |
class Component extends HTMLElement { | |
static use() { | |
if (this[USED]) return; | |
this[USED] = true; | |
customElements.define(name, this); | |
} | |
static extend(variantName, variantConfig) { | |
const {element, style, initialize, ...base} = config; | |
return defineComponent(variantName, Object.assign(base, variantConfig, { | |
extends: this.prototype.extends ? this.prototype.extends + ' ' + name : name, | |
element: (config.element || '') + (variantConfig.element || ''), | |
initialize(refs) { | |
config.initialize && config.initialize.call(this, refs); | |
variantConfig.initialize && variantConfig.initialize.call(this, refs); | |
} | |
})); | |
} | |
static html(attributes = '', children = '') { | |
this.use(); | |
return `<${name} ${attributes}>${children}</${name}>`; | |
} | |
connectedCallback() { | |
if (this[INIT]) return; | |
this[INIT] = true; | |
config.extends && this.setAttribute('extends', config.extends); | |
if (TEMPLATE) { | |
this.appendChild(TEMPLATE.cloneNode(true)); | |
HAS_SLOTS && this.querySelectorAll('slot').forEach(slot => { | |
const child = this.querySelector(`[slot="${slot.getAttribute('name')}"]`); | |
child ? slot.replaceWith(child) : slot.remove(); | |
}); | |
} | |
config.initialize && requestAnimationFrame(() => { | |
config.initialize.call(this, new Proxy(this, REFS)) | |
}); | |
} | |
} | |
const {element, style, ...proto} = Object.getOwnPropertyDescriptors(config); | |
Object.defineProperties(Component.prototype, proto); | |
return Component; | |
} | |
/** | |
* License | |
* Copyright (C) 2022 Jonathan M. Ochmann | |
* This program is free software: you can redistribute it and/or modify | |
* it under the terms of the GNU General Public License as published by | |
* the Free Software Foundation, either version 3 of the License, or | |
* (at your option) any later version. | |
* | |
* This program is distributed in the hope that it will be useful, | |
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
* GNU General Public License for more details. | |
* | |
* You should have received a copy of the GNU General Public License | |
* along with this program. If not, see https://www.gnu.org/licenses. | |
*/ |
Author
monokee
commented
Jun 26, 2022
•
Create variants of a shared base component via Component.extend()
import { BaseComponent } from "./comment-above/base-component.js"
export const DerivedComponent = BaseCompnent.extend('derived-component', {
// the input element is composed into the slot position defined by BaseComponent
element: (`
<input slot="content" ref="$input" type="number" value="0">
`),
// :host refers only to this derived component. All styles from BaseComponent are inherited.
style: (`
:host {
background-color: blue;
}
`),
// this initialize will run after the initialize of the BaseComponent.
// both receive all refs nodes i.e. the BaseComponent could also access the $input ref
initialize({$button, $input}) {
$button.addEventListener('click', e => {
$input.value = parseInt($input.value) + 1;
});
if (this.hasAttribute('title')) {
$button.textContent = this.getAttribute('title');
}
}
})
Using Components
In Markup
import { DerivedComponent } from './comment-above/derived-component.js'
// Defines the component and ensures it gets picked up by bundlers.
DerivedComponent.use();
document.body.innerHTML += (`<derived-component
title="Hello World"
></derived-component>`);
In Markup via Component.html()
import { DerivedComponent } from './comment-above/derived-component.js'
// Component.html accepts two string arguments: attributes and innerHTML
document.body.innerHTML += DerivedComponent.html(`title="Hello World"`);
Using regular DOM APIs
import { DerivedComponent } from './comment-above/derived-component.js'
// Defines the component and ensures it gets picked up by bundlers.
DerivedComponent.use();
const $el = document.createElement('derived-component');
$el.setAttribute('title', 'Hello World');
document.body.appendChild($el);
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment