@skirbi/sugar

3 min

@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

bash
npm install @skirbi/sugar

Defining a component

js
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:

js
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:

js
static attributeDefs = {
  'observed-no': { observed: false },
  'observed-yes': { observed: true },
};

Templates

Templates define structure. They are resolved once at register time, not lazily.

js
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)

js
static HtmlTemplate = 'my-article-template';

Throws immediately if the template isn’t found. Use when the template must exist.

Inline via helper

js
static HtmlTemplate = this.tpl(`
  <article>
    <section my-body></section>
  </article>
`);

No template

js
class MinimalistComponent extends HTMLElementSugar {
  static tag = 'ultimate-minimalist';
}

Valid. Render manually in connectedCallback.

Rendering pattern

js
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:

html
<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

js
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.

js
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.

js
// 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.