npm package: @opndev/skirbi
Description: Lightweight base layer for writing custom elements with declarative attributes and template sugar.
Author: Wesley Schwengle
Latest version: 0.0.5
Deprecated: Moved to @skirbi/sugar

<!--
SPDX-FileCopyrightText: 2025 Wesley Schwengle <wesleys@opperschaap.net>

SPDX-License-Identifier: MIT
-->

# Welcome to @opndev/skirbi

This module provides HTMLElementSugar. HTMLElementSugar provides ways to easily
create and maintain Web Components, or HTMLElements as they are officially
called.

You can create Web Components like so:

```
class MyNewElement extends HTMLElementSugar {
  static tag = 'my-new-element';
  static attributeDefs = {
    foo: { default: 'bar' },
    'bar': { parser: parseBoolean },
    'baz': null,
    'qux': {},
    'toto': { observed: "" }, // not observed
    'titi': { observed: 0 }, // not observed
    'tata': { observed: false }, // not observed
    'tete': { observed: null }, // not observed
    'yes': { observed: true },
    'yez': { observed: 1 },
    'yep': { observed: "here be dragons" },
  };
}
```

Additionally you can create aliases:

```
TrackItem.alias('song', { type: 'song' });
YourBook.alias('book');
```

## Templates

### Template via HTML template reference

```javascript
class HTMLTemplate extends HTMLElementSugar {
  static tag = 'html-template';
  static HtmlTemplate = 'track-item-template'; // find in html
}
```

HTMLElementSugar asserts that it can find the template on registration. Which
means you need to have the template ready in HTML. It is therefore essential
that you register your component in window.addEventListener('DOMContentLoaded',
() => { }.

### Template via javascript element reference

```javascript
const tpl = document.createElement('template');
tpl.id = 'inline';
tpl.innerHTML =
  `<div class="track-row"><span class="song">song</span></div>`;
document.body.appendChild(tpl);

class JSElement extends HTMLElementSugar {
  static tag = 'js-element';
  static HtmlTemplate = tpl;
}
```

### Template via javascript function

```javascript
class TemplateFunction extends HTMLElementSugar {
  static tag = 'template-function';

  static HtmlTemplate() {
    const t = document.createElement('template');
    t.innerHTML =
      `<div class="track-row"><div class="track-info">from fn</div></div>`;
    return t;
  }
}
```

We recently added a helper you can now do this too:

```javascript
class TemplateFunction extends HTMLElementSugar {
  static tag = 'template-function';

  static HtmlTemplate = this.tpl(`
      <div class="track-row">
        <div class="track-info">from tpl</div>
      </div>
  `);
}
```

### Custom template with fallback

```javascript
class TemplateFunction extends HTMLElementSugar {
  static tag = 'template-function';

  static HtmlTemplate = [
    'some-id-in-html',
    () => {
      const t = document.createElement('template');
      t.innerHTML =
        `<div class="track-row"><div class="track-info">from fn</div></div>`;
      return t;
    }
  ];
}
```

### No template (the ultimate minimalism)

```javascript
class MinimalistComponent extends HTMLElementSugar {
  static tag = 'ultimate-minimalist';
  // Look at all this template code I'm not writing
}
```

You can register your Web Components by running:

```
MyComponent.register();
```

# HTMLElementSugarInput

`HTMLElementSugarInput` extends `HTMLElementSugar` and is designed for
components that wrap a real form control in the light DOM.

It provides:

- Attribute forwarding to the real control
- Native `input` and `change` re-emission
- Optional attribute mirroring via `data-sync`
- Proper `value` property handling

## Contract

A subclass must:

- Extend `HTMLElementSugarInput`
- Render exactly one element matching `[wc-control]`
  (or override `static controlSelector`)

Example:

```javascript
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();
```

## Attribute Forwarding

All host attributes that are **not defined in `attributeDefs`**
are forwarded to the real control element.

Example:

```html
<my-input
  label="Name"
  wire:model.defer="name"
  aria-label="Name"
></my-input>
```

Results in:

```html
<input
  wire:model.defer="name"
  aria-label="Name"
>
```

### Not forwarded

- Attributes defined in `attributeDefs`
- `id`, `class`, `style` (kept on the host)

This keeps components framework-agnostic:

- Livewire (`wire:*`)
- Alpine (`x-*`)
- HTMX (`hx-*`)
- `aria-*`
- `data-*`

## Event Re-Emission

Native events from the control are re-emitted from the host:

- `input`
- `change`

Listeners may bind to either the host or the internal control.

## data-sync (Optional Attribute Mirroring)

By default, attributes are forwarded once during connect.

If you enable `data-sync`, subsequent attribute changes on the host are
mirrored to the control using a MutationObserver.

Example:

```html
<my-input wire:model.defer="name" data-sync></my-input>
```

When `data-sync` is enabled:

- Host attribute changes are mirrored to the control
- `value` is mirrored as a property (not an attribute)
- Removing attributes removes them from the control

This is particularly useful in reactive environments such as Livewire.

## Code of Conduct

Be human.

## Developer notes about this package

### Semver

This project does not adhere to semver and one should not rely on the version
x.y.z notation to infer stability or reliability. Read the Changes file to see
any updates a version may bring. The fact that this module sits currently at
0.x.z ranges does not indicate alpha or beta or even unstable associations. It
is just a number and we started at 0.0.1.

In general the following hard guarantee will be given: We will not break your
code. In case we do happen to cause breakage: we will fix it accordingly.

In case we foresee breaking changes we'll add deprecation warnings. Giving you
time to fix things before a breaking change will be introduced. When a change
will be introduced is communicated in the Changes file. Security fixes may
cause breakage at any given time without notice.

This package is released by `@opndev/rzilla`, changes to `package.json` will be
overridden. In addition to a little bit of promotion, this also means that
version numbers are autoincremented at release time and bumped in all relevant
files: Versioning for humans, not machines.