npm package: @opndev/webcomponents-core
Description: Webcomponent authoring class
Author: Wesley Schwengle
Latest version: 0.0.3
Deprecated: Moved to @skirbi/sugar

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

SPDX-License-Identifier: MIT
-->

# Welcome to webcomponents-core

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;
  }
}
```

### 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;
    }
  ];
}
```

### Custom template with override

Alternatively you can allow users to setup a simple HTML template while
providing a 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.

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

In general the follow hard guarantee will be given: We will not break your
code. In case it happens, we will fix it accordingly. In any other situation we
announce it as a breaking change and/or we will first add deprecation warnings.
Giving you time to fix things before a breaking change will be introduced.

### Lockfiles in this project

You'll also see that `package-lock.json` and/or `yarn.lock` is ignored. This
module has very little dependencies and are mostly owned by the organisation
itself. You are free to implement a lockfile in your downstream project.

### Releases

This package is released by `@opndev/rzilla`, changes to `package.json` will be
overriden.