@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 +45 -0
- package/lib/htmlelement-select.mjs +1 -1
- package/lib/htmlelement.mjs +54 -0
- package/lib/index.mjs +1 -1
- package/lib/livewire.mjs +42 -0
- package/package.json +2 -1
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
|
package/lib/htmlelement.mjs
CHANGED
|
@@ -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
package/lib/livewire.mjs
ADDED
|
@@ -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.
|
|
56
|
+
"version": "0.1.3"
|
|
56
57
|
}
|