Skip to content

Instantly share code, notes, and snippets.

@monokee
Last active May 28, 2024 13:03
Show Gist options
  • Save monokee/03230511f1e2214dc1f0b17763d85369 to your computer and use it in GitHub Desktop.
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…
/**
* 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.
*/
@monokee
Copy link
Author

monokee commented Jun 27, 2022

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