Last active
December 7, 2024 17:07
-
-
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 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
/** | |
* 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. | |
*/ |
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
Create variants of a shared base component via
Component.extend()