@skirbi/sugar 0.1.1 → 0.1.3

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,50 @@
1
1
  Revision history for @skirbi/sugar
2
2
 
3
+ 0.1.3 2026-06-05 23:23:43Z
4
+
5
+ * Add Livewire integration module
6
+
7
+ Add @skirbi/sugar/livewire.mjs. An optional side-effect module that
8
+ fixes structural mismatches between Livewire's morphing algorithm and
9
+ Sugar web components.
10
+
11
+ Problem? Yes.
12
+ Livewire sends new HTML in its authored (dehydrated) state. Custom
13
+ elements in that HTML have not yet run connectedCallback, so their
14
+ DOM structure differs from the current rendered (hydrated) DOM.
15
+ Alpine's morph algorithm compares these two structurally different
16
+ trees, sees mismatches, and calls swapElements — replacing entire
17
+ subtrees, disconnecting all custom element descendants, triggering
18
+ cascading connectedCallback re-renders, and causing focus loss on
19
+ every Livewire update.
20
+
21
+ Fix, days:
22
+ Before Livewire's morph runs, parse the new HTML into a hidden
23
+ staging element and wait two microtasks for custom elements to
24
+ hydrate. Alpine then morphs two fully hydrated trees — apples to
25
+ apples — no structural mismatches, no swaps, no cascade, no focus
26
+ loss.
27
+
28
+ Usage:
29
+ import '@skirbi/sugar/livewire';
30
+
31
+ Only import when using Sugar/Semtic/Skirbi components inside a Livewire
32
+ application. The guard on typeof document makes it safe in SSR/Node
33
+ environments.
34
+
35
+ This fix is a candidate for upstreaming to Livewire once it has been
36
+ validated.
37
+
38
+ 0.1.2 2026-06-05 13:39:51Z
39
+
40
+ * Add replaceChildrenSugarSafe to allow repaints without focus loss
41
+
42
+ Add initial version of a safer version for `replaceChildren()` called
43
+ `replaceChildrenSugarSafe()`. During real life usage it became apparant
44
+ that inputs lose focus with `wire:model` changes. This is slightly
45
+ annoying. Allow for a more fine grained approach where we update
46
+ existing fragements rather than replacing them full fledged.
47
+
3
48
  0.1.1 2026-06-04 15:34:20Z
4
49
 
5
50
  * Added morph-* attributes to withConnectedSugar, the API of this is
@@ -101,7 +101,7 @@ export class HTMLElementSugarSelect extends HTMLElementSugarInput {
101
101
 
102
102
  this.setupSelectControl(frag);
103
103
 
104
- this.replaceChildren(frag);
104
+ this.replaceChildrenSugarSafe(frag);
105
105
  }
106
106
 
107
107
  _addPlaceholder(select, text) {
@@ -437,5 +437,59 @@ export class HTMLElementSugar extends HTMLElement {
437
437
  $$(selector) {
438
438
  return [...this.querySelectorAll(selector)]
439
439
  }
440
+
441
+ _morphElement(oldEl, newEl) {
442
+ this._morphAttributes(oldEl, newEl);
443
+
444
+ const newChildren = Array.from(newEl.childNodes);
445
+ const oldChildren = Array.from(oldEl.childNodes);
446
+
447
+ newChildren.forEach((newNode, i) => {
448
+ const oldNode = oldChildren[i];
449
+ if (!oldNode) {
450
+ oldEl.appendChild(newNode);
451
+ }
452
+ else if (oldNode.nodeType === newNode.nodeType && oldNode.nodeName === newNode.nodeName) {
453
+ if (oldNode.nodeType === Node.TEXT_NODE) {
454
+ if (oldNode.textContent !== newNode.textContent) {
455
+ oldNode.textContent = newNode.textContent;
456
+ }
457
+ }
458
+ else {
459
+ this._morphElement(oldNode, newNode);
460
+ }
461
+ }
462
+ else {
463
+ oldEl.replaceChild(newNode, oldNode);
464
+ }
465
+ });
466
+
467
+ oldChildren.slice(newChildren.length).forEach(n => oldEl.removeChild(n));
468
+ }
469
+
470
+ _morphAttributes(oldEl, newEl) {
471
+ if (!oldEl.attributes || !newEl.attributes) return;
472
+
473
+ for (const attr of newEl.attributes) {
474
+ if (oldEl.getAttribute(attr.name) !== attr.value) {
475
+ oldEl.setAttribute(attr.name, attr.value);
476
+ }
477
+ }
478
+ for (const attr of Array.from(oldEl.attributes)) {
479
+ if (!newEl.hasAttribute(attr.name)) {
480
+ oldEl.removeAttribute(attr.name);
481
+ }
482
+ }
483
+ }
484
+
485
+ _morphChildren(frag) {
486
+ this._morphElement(this, frag);
487
+ }
488
+
489
+
490
+ replaceChildrenSugarSafe(frag) {
491
+ this._morphElement(this, frag);
492
+ }
493
+
440
494
  }
441
495
 
package/lib/index.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  //
3
3
  // SPDX-License-Identifier: MIT
4
4
 
5
- const VERSION = "0.1.1";
5
+ const VERSION = "0.1.3";
6
6
 
7
7
  export { HTMLElementSugar } from './htmlelement.mjs';
8
8
  export { HTMLElementSugarInput } from './htmlelement-input.mjs';
@@ -0,0 +1,42 @@
1
+ // SPDX-FileCopyrightText: 2026 Wesley Schwengle <wesleys@opperschaap.net>
2
+ //
3
+ // SPDX-License-Identifier: MIT
4
+
5
+ /**
6
+ * @skirbi/sugar — Livewire integration
7
+ *
8
+ * Hydrates custom elements in Livewire's new HTML before morphing starts.
9
+ * This ensures Alpine morphs hydrated vs hydrated trees — apples to apples —
10
+ * preventing structural mismatches that cause swapElements cascades,
11
+ * disconnected/reconnected custom elements, and focus loss on every update.
12
+ *
13
+ * Usage:
14
+ * import '@skirbi/sugar/livewire';
15
+ *
16
+ * Only import this when using Sugar components inside a Livewire application.
17
+ */
18
+
19
+ if (typeof document !== 'undefined') {
20
+ document.addEventListener('livewire:init', () => {
21
+ window.Livewire.hook('component.init', ({ component }) => {
22
+ const original = component.processEffects.bind(component);
23
+
24
+ component.processEffects = function(effects) {
25
+ if (!effects.html) return original(effects);
26
+ if (effects._sugarHydrated) return original(effects);
27
+
28
+ const staging = document.createElement('div');
29
+ staging.style.display = 'none';
30
+ document.body.appendChild(staging);
31
+ staging.innerHTML = effects.html;
32
+
33
+ Promise.resolve().then(() => Promise.resolve().then(() => {
34
+ effects.html = staging.innerHTML;
35
+ effects._sugarHydrated = true;
36
+ staging.remove();
37
+ original(effects);
38
+ }));
39
+ };
40
+ });
41
+ });
42
+ }
package/package.json CHANGED
@@ -24,6 +24,7 @@
24
24
  "./htmlelement": "./lib/htmlelement.mjs",
25
25
  "./htmlelement-input": "./lib/htmlelement-input.mjs",
26
26
  "./htmlelement-select": "./lib/htmlelement-select.mjs",
27
+ "./livewire": "./lib/livewire.mjs",
27
28
  "./testing": "./lib/testing.mjs",
28
29
  "./with-connected-sugar": "./lib/with-connected-sugar.mjs"
29
30
  },
@@ -52,5 +53,5 @@
52
53
  },
53
54
  "sideEffects": false,
54
55
  "type": "module",
55
- "version": "0.1.1"
56
+ "version": "0.1.3"
56
57
  }