@skirbi/sugar 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Changes CHANGED
@@ -1,5 +1,26 @@
1
1
  Revision history for @skirbi/sugar
2
2
 
3
+ 0.1.6 2026-06-08 01:05:36Z
4
+
5
+ * Add attributeChangedCallback hook system
6
+ Extend attributeChangedCallback with a declarative hook system in
7
+ attributeDefs. When an observed attribute changes and the component
8
+ has a rendered shape, sugar automatically updates the DOM:
9
+
10
+ Convention: attribute 'foo' maps to [semtic-foo] textContent
11
+ String hook: CSS selector target, updates textContent
12
+ Function hook: full control, called with (el, newValue, oldValue)
13
+ Escape hatch: attributeChangedCallbackSugar(name, old, new)
14
+
15
+ Requires renderGuardSelector to be set. Hooks only fire when the
16
+ component has already rendered. This makes renderGuardSelector
17
+ effectively mandatory for components that want reactive attribute
18
+ updates.
19
+
20
+ 0.1.5 2026-06-06 18:09:56Z
21
+
22
+ * Now actually do it.. pls send coffee
23
+
3
24
  0.1.4 2026-06-06 16:41:57Z
4
25
 
5
26
  * Semtic already had this, but sugar in itself also has side-effects.
package/README.md CHANGED
@@ -20,6 +20,8 @@ It provides:
20
20
  You can create Web Components like so:
21
21
 
22
22
  ```javascript
23
+ import { HTMLElementSugar } from '@skirbi/sugar';
24
+
23
25
  class MyNewElement extends HTMLElementSugar {
24
26
  static tag = 'my-new-element';
25
27
  static attributeDefs = {
@@ -27,20 +29,21 @@ class MyNewElement extends HTMLElementSugar {
27
29
  bar: { parser: parseBoolean },
28
30
  };
29
31
  }
32
+ MyNewElement.register();
30
33
  ```
31
34
 
32
35
  You can also create aliases:
33
36
 
34
37
  ```javascript
35
- TrackItem.alias('some-song', { type: 'song' });
36
- YourBook.alias('a-book');
38
+ import { registerDevAlias } from '@skirbi/sugar';
39
+ registerDevAlias('some-song', 'track-item', { type: 'song' });
40
+ registerDevAlias('a-book', 'your-book');
37
41
  ```
38
42
 
39
43
  And to register them:
40
44
 
41
45
  ```javascript
42
- TrackItem.register();
43
- YourBook.register();
46
+ MyNewElement.register();
44
47
  ```
45
48
 
46
49
  ## Observable patterns
@@ -288,21 +291,21 @@ HTMLElementSugarSelect extends HTMLElementSugarInput and provides a
288
291
  structured way to author `<select>`-based components.
289
292
 
290
293
  It supports:
291
- * Static option lists (via JSON)
292
- * Remote option loading (via endpoint)
293
- * Flexible data mapping via jpath
294
- * Label/value templates
295
- * Optional search input
296
- * Attribute forwarding to the underlying `<select>`
294
+ - Static option lists (via JSON)
295
+ - Remote option loading (via endpoint)
296
+ - Flexible data mapping via jpath
297
+ - Label/value templates
298
+ - Optional search input
299
+ - Attribute forwarding to the underlying `<select>`
297
300
 
298
301
  It always renders a real `<select>` in the light DOM.
299
302
 
300
303
  ### Contract
301
304
 
302
305
  A subclass must:
303
- * Extend HTMLElementSugarSelect
304
- * Render exactly one `<select wc-control>`
305
- * Optionally include a search input if searchable is enabled
306
+ - Extend HTMLElementSugarSelect
307
+ - Render exactly one `<select wc-control>`
308
+ - Optionally include a search input if searchable is enabled
306
309
 
307
310
  Example:
308
311
 
@@ -336,11 +339,11 @@ You may provide options via the options attribute as a JSON array:
336
339
  ### Mapping Options
337
340
 
338
341
  Data mapping is controlled via:
339
- * `jpath`: path to the array in a nested JSON response
340
- * `jpath-label`: path for the option label
341
- * `jpath-value`: path for the option value
342
- * `jpath-label-template`: template string for label
343
- * `jpath-value-template`: template string for value
342
+ - `jpath`: path to the array in a nested JSON response
343
+ - `jpath-label`: path for the option label
344
+ - `jpath-value`: path for the option value
345
+ - `jpath-label-template`: template string for label
346
+ - `jpath-value-template`: template string for value
344
347
 
345
348
  Templates take precedence over path mappings.
346
349
 
@@ -375,53 +378,77 @@ Options can be loaded from a remote endpoint:
375
378
  ```
376
379
 
377
380
  Behavior:
378
- * Fetches JSON from endpoint
379
- * Adds search query via param
380
- * Applies mapping rules
381
- * Replaces options on each fetch
381
+ - Fetches JSON from endpoint
382
+ - Adds search query via param
383
+ - Applies mapping rules
384
+ - Replaces options on each fetch
382
385
 
383
386
  If `searchable` is enabled:
384
- * A search input is rendered
385
- * Input is debounced
386
- * Fetch is triggered after min-chars is reached
387
- * By default, a remote endpoint triggers an initial fetch with an empty query
388
- on connect. If `nosearch-initial` is enabled this behavior is negated.
389
-
390
-
391
- * No initial searches are
387
+ - A search input is rendered
388
+ - Input is debounced
389
+ - Fetch is triggered after min-chars is reached
390
+ - By default, a remote endpoint triggers an initial fetch with an empty query
391
+ on connect. If `nosearch-initial` is enabled this behavior is negated and no
392
+ initial searches are executed.
392
393
 
393
394
  ### Search Behavior (Static Mode)
394
395
 
395
396
  When using static options, enabling searchable:
396
- * Filters `<option>` elements in place
397
- * Uses case-insensitive substring matching
398
- * Does not modify underlying data
397
+ - Filters `<option>` elements in place
398
+ - Uses case-insensitive substring matching
399
+ - Does not modify underlying data
399
400
 
400
401
  ### Value Handling
401
402
 
402
403
  The value attribute behaves consistently with `HTMLElementSugarInput`:
403
- * Initial value is applied after options render
404
- * With data-sync, updates are mirrored to the `<select>` element
405
- * Native input and change events are re-emitted from the host
404
+ - Initial value is applied after options render
405
+ - With data-sync, updates are mirrored to the `<select>` element
406
+ - Native input and change events are re-emitted from the host
406
407
 
407
408
  ### Framework Compatibility
408
409
 
409
410
  Because it renders a real `<select>` in the light DOM, it works naturally with:
410
- * Livewire (`wire:*`)
411
- * Alpine (`x-*`)
412
- * HTMX (`hx-*`)
413
- * `aria-*`
414
- * `data-*`
411
+ - Livewire (`wire:*`)
412
+ - Alpine (`x-*`)
413
+ - HTMX (`hx-*`)
414
+ - `aria-*`
415
+ - `data-*`
415
416
 
416
417
  Attributes not defined in attributeDefs are forwarded to the `<select>`
417
418
  element.
418
419
 
420
+ #### Livewire
421
+
422
+ For Livewire applications using Sugar components, import the optional
423
+ Livewire integration:
424
+
425
+ ```js
426
+ import '@skirbi/sugar/livewire';
427
+ ```
428
+
429
+ This fixes a fundamental incompatibility between Livewire's morph algorithm
430
+ and Web Components. Livewire compares its authored HTML against an already
431
+ rendered DOM — structurally different trees cause cascade disconnects,
432
+ reconnects, and focus loss on every update.
433
+
434
+ The integration hydrates Livewire's new HTML in a staging element before
435
+ morphing starts, so Alpine compares two fully rendered trees instead of
436
+ authored vs rendered. No cascade, no focus loss.
437
+
438
+ Import it before registering your components:
439
+
440
+ ```js
441
+ import '@skirbi/sugar/livewire';
442
+ import '@skirbi/semtic/register-semtic';
443
+ ```
444
+
419
445
  ### Why This Works
420
- * No shadow DOM
421
- * No custom dropdown UI
422
- * No CSS lock-in
423
- * Real `<option>` elements
424
- * Progressive enhancement friendly
446
+
447
+ - No shadow DOM
448
+ - No custom dropdown UI
449
+ - No CSS lock-in
450
+ - Real `<option>` elements
451
+ - Progressive enhancement friendly
425
452
 
426
453
  ## Code of Conduct
427
454
 
@@ -347,11 +347,31 @@ export class HTMLElementSugar extends HTMLElement {
347
347
  /**
348
348
  * Called when an observed attribute changes.
349
349
  * @param {string} name - The attribute name
350
- * @param {string|null} _oldVal - Previous value (unused)
350
+ * @param {string|null} oldVal - Previous value (unused)
351
351
  * @param {string|null} newVal - New value
352
352
  */
353
- attributeChangedCallback(name, _oldVal, newVal) {
353
+ attributeChangedCallback(name, oldVal, newVal) {
354
354
  this._applyAttribute(name, newVal);
355
+
356
+ if (!this.hasRenderedShape?.()) return;
357
+
358
+ const def = this.constructor.attributeDefs?.[name];
359
+
360
+ if (def?.hook) {
361
+ if (typeof def.hook === 'function') {
362
+ def.hook(this, newVal, oldVal);
363
+ } else if (typeof def.hook === 'string') {
364
+ const el = this.querySelector(def.hook);
365
+ if (el) el.textContent = newVal ?? '';
366
+ }
367
+ } else {
368
+ const el = this.querySelector(`[semtic-${name}]`);
369
+ if (el) el.textContent = newVal ?? '';
370
+ }
371
+
372
+ if (this.attributeChangedCallbackSugar) {
373
+ this.attributeChangedCallbackSugar(name, oldVal, newVal);
374
+ }
355
375
  }
356
376
 
357
377
  /**
package/lib/index.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  //
3
3
  // SPDX-License-Identifier: MIT
4
4
 
5
- const VERSION = "0.1.4";
5
+ const VERSION = "0.1.6";
6
6
 
7
7
  export { HTMLElementSugar } from './htmlelement.mjs';
8
8
  export { HTMLElementSugarInput } from './htmlelement-input.mjs';
package/package.json CHANGED
@@ -51,7 +51,7 @@
51
51
  "release": "rzil release",
52
52
  "test": "tap"
53
53
  },
54
- "sideEffects": false,
54
+ "sideEffects": true,
55
55
  "type": "module",
56
- "version": "0.1.4"
56
+ "version": "0.1.6"
57
57
  }