@skirbi/sugar
@skirbi/sugar is the base layer. It handles the plumbing so everything built
on top of it doesn’t have to.
It is purely infrastructure. It has no opinions about structure, styling, or layout.
What it provides
- Declarative attribute → config mapping
- Template resolution and caching
- Component registration (define-once)
- Form control wrapping primitives
- Alias support
Installation
npm install @skirbi/sugar
Defining a component
class MyArticle extends HTMLElementSugar {
static tag = 'my-article';
}
MyArticle.register();
That’s the minimum. The component is registered, the tag is defined, and registration is idempotent — if another class already claimed the tag, this is a no-op.
Attributes
Declare attributes with attributeDefs:
class MyHeader extends HTMLElementSugar {
static tag = 'my-header';
static attributeDefs = {
title: { default: '' },
subtitle: { default: '' },
};
}
This gives you observedAttributes, defaultConfig, attributeMap, and
automatic attributeChangedCallback handling. In your connectedCallback you
can rely on this.config.title and this.config.subtitle being correct and
initialized.
Attributes are observable by default. You can opt specific attributes out:
static attributeDefs = {
'observed-no': { observed: false },
'observed-yes': { observed: true },
};
Templates
Templates define structure. They are resolved once at register time, not lazily.
Fallback tuple (recommended)
static HtmlTemplate = [
'my-article-template', // look for this id in the DOM
() => { // use this if not found
const t = document.createElement('template');
t.innerHTML = `
<article>
<my-header></my-header>
<section my-body></section>
</article>
`;
return t;
}
];
Works by default, overridable by anyone who puts a <template id="my-article-template"> in the page before the JS runs.
Strict ID (fail-fast)
static HtmlTemplate = 'my-article-template';
Throws immediately if the template isn’t found. Use when the template must exist.
Inline via helper
static HtmlTemplate = this.tpl(`
<article>
<section my-body></section>
</article>
`);
No template
class MinimalistComponent extends HTMLElementSugar {
static tag = 'ultimate-minimalist';
}
Valid. Render manually in connectedCallback.
Rendering pattern
connectedCallback() {
super.connectedCallback();
const frag = this.renderFromTemplate();
// mutate the fragment
// move light-DOM children
// validate placeholders
this.replaceChildren(frag);
}
Always mutate the cloned fragment, never the cached template. Use
replaceChildren() to keep the custom element in the DOM as a CSS hook. Use
replaceWith() only if you intentionally want to remove it.
Placeholder convention
Use attribute markers, not classes:
<section my-body></section>
<h1 my-title></h1>
They’re classless, hard to collide with user HTML, and easy to query and validate. Always fail fast if a required placeholder is missing.
Aliases
MyArticle.alias('my-post');
MyArticle.alias('my-entry', { type: 'entry' });
Aliases point to the same behavior. They can also set default attributes.
Form controls
HTMLElementSugarInput extends Sugar for components that wrap a real form
control.
class MyInput extends HTMLElementSugarInput {
static tag = 'my-input';
static attributeDefs = {
label: { default: '' },
value: { default: '' },
};
static HtmlTemplate = this.tpl(`
<div>
<label wc-label></label>
<input wc-control type="text">
</div>
`);
connectedCallback() {
super.connectedCallback();
const frag = this.renderFromTemplate();
const control = this.enhanceControl(frag);
const { label, value } = this.getConfig();
frag.querySelector('[wc-label]').textContent = label;
control.value = value ?? '';
this.replaceChildren(frag);
}
}
MyInput.register();
The contract: render exactly one element matching [wc-control]. Sugar handles
the rest — attribute forwarding, event re-emission, and optional data-sync
mirroring for reactive frameworks.
Attributes not defined in attributeDefs are forwarded to the control
automatically. id, class, and style stay on the host.
HTMLElementSugarSelect
HTMLElementSugarSelect extends Sugar for select components.
// This one is more about using than extending?
// Perhaps move to semtic
MySelect.register();
The contract: render exactly one element matching [wc-control]. Sugar handles
the rest — attribute forwarding, event re-emission, and optional data-sync
mirroring for reactive frameworks.
Attributes not defined in attributeDefs are forwarded to the control
automatically. id, class, and style stay on the host.