@skirbi/sugar 0.1.0 → 0.1.2

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.2 2026-06-05 13:39:51Z
4
+
5
+ * Add replaceChildrenSugarSafe to allow repaints without focus loss
6
+
7
+ Add initial version of a safer version for `replaceChildren()` called
8
+ `replaceChildrenSugarSafe()`. During real life usage it became apparant
9
+ that inputs lose focus with `wire:model` changes. This is slightly
10
+ annoying. Allow for a more fine grained approach where we update
11
+ existing fragements rather than replacing them full fledged.
12
+
13
+ 0.1.1 2026-06-04 15:34:20Z
14
+
15
+ * Added morph-* attributes to withConnectedSugar, the API of this is
16
+ unstable and should probably only be used when debugging Livewire morphing
17
+ or similar tools.
18
+
19
+ Some debugging artifacts have become morph-* attributes:
20
+
21
+ * morph-ignore — disarm the observer entirely (no-op against full node
22
+ replacement by Livewire, but prevents cascade
23
+ re-renders during normal morphing)
24
+ * morph-child — observe childList only (explicit opt-in)
25
+ * morph-attribute — observe attribute changes only
26
+ * morph-subtree — observe the full descendant subtree
27
+ * morph-debug — Enable debuging used by `_armMorphObserver`.
28
+
29
+ When none are present, the existing default (childList: true) applies.
30
+
31
+ * Add `withMorphObserverPaused(fn)` to withConnectedSugar.
32
+ Components that mutate their own children in morphedCallbackSugar or
33
+ connectedCallbackSugar risk triggering their own MutationObserver,
34
+ causing infinite loops.
35
+
36
+ `withMorphObserverPaused(fn)` disconnects the observer, runs `fn`, then
37
+ reconnects with the original options. It is a public API intended for
38
+ use by component authors.
39
+
40
+ Observer options are now stored as `_morphObserveOptions` when the
41
+ observer is armed, so they can be reused by withMorphObserverPaused
42
+ and any future callers without re-deriving them.
43
+
44
+ This again is a WIP feature that may change based on Livewire morphing
45
+ insights.
46
+
47
+
3
48
  0.1.0 2026-06-04 01:20:59Z
4
49
 
5
50
  * BREAKING: Sugar components may no longer manually define
@@ -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.0";
5
+ const VERSION = "0.1.2";
6
6
 
7
7
  export { HTMLElementSugar } from './htmlelement.mjs';
8
8
  export { HTMLElementSugarInput } from './htmlelement-input.mjs';
@@ -27,19 +27,51 @@ export const withConnectedSugar = (Base) =>
27
27
 
28
28
  _armMorphObserver() {
29
29
  if (this._sugarMo) return;
30
+ if (this.hasAttribute('morph-ignore')) return;
30
31
 
31
- this._sugarMo = new MutationObserver(() => {
32
- if (this._shouldRenderOnMorph()) {
33
- this.connectedCallbackSugar();
34
- return;
35
- }
36
- if (this.hasRenderedShape() && this.morphedCallbackSugar) {
37
- this.morphedCallbackSugar();
38
- return;
32
+ const hasExplicit = this.hasAttribute('morph-child') ||
33
+ this.hasAttribute('morph-attribute') ||
34
+ this.hasAttribute('morph-subtree');
35
+
36
+ this._morphObserveOptions = hasExplicit ? {
37
+ childList: this.hasAttribute('morph-child'),
38
+ attributes: this.hasAttribute('morph-attribute'),
39
+ subtree: this.hasAttribute('morph-subtree'),
40
+ } : {
41
+ childList: true,
42
+ };
43
+
44
+ this._sugarMo = new MutationObserver((mutations) => {
45
+
46
+ if (this.hasAttribute('morph-debug')) {
47
+ console.group(`[sugar-morph] ${this.tagName}${this.id ? '#' + this.id : ''}${this.getAttribute('label') ? `[label="${this.getAttribute('label')}"]` : ''}${this.getAttribute('title') ? `[title="${this.getAttribute('title')}"]` : ''}`);
48
+ console.log('parent:', this.parentElement?.tagName);
49
+ console.log('mutations:', mutations.map(m => ({
50
+ type: m.type,
51
+ added: Array.from(m.addedNodes).map(n => n.nodeName),
52
+ removed: Array.from(m.removedNodes).map(n => n.nodeName)
53
+ })));
54
+ console.log('hasRenderedShape:', this.hasRenderedShape());
55
+ console.log('shouldRenderOnMorph:', this._shouldRenderOnMorph());
56
+ setTimeout(() => console.log('innerHTML after:', this.innerHTML), 0);
57
+ console.groupEnd();
39
58
  }
59
+
60
+
61
+ queueMicrotask(() => {
62
+ if (this._shouldRenderOnMorph()) {
63
+ this.connectedCallbackSugar();
64
+ return;
65
+ }
66
+ if (this.hasRenderedShape() && this.morphedCallbackSugar) {
67
+ this.morphedCallbackSugar();
68
+ return;
69
+ }
70
+ });
71
+
40
72
  });
41
73
 
42
- this._sugarMo.observe(this, { childList: true });
74
+ this._sugarMo.observe(this, this._morphObserveOptions);
43
75
  }
44
76
 
45
77
  _shouldRenderOnMorph() {
@@ -58,6 +90,17 @@ export const withConnectedSugar = (Base) =>
58
90
  return true;
59
91
  }
60
92
 
93
+ withMorphObserverPaused(fn) {
94
+ this._sugarMo?.disconnect();
95
+ try {
96
+ fn();
97
+ } finally {
98
+ if (this._morphObserveOptions) {
99
+ this._sugarMo?.observe(this, this._morphObserveOptions);
100
+ }
101
+ }
102
+ }
103
+
61
104
  disconnectedCallback() {
62
105
  this._sugarMo?.disconnect();
63
106
  this._sugarMo = null;
package/package.json CHANGED
@@ -52,5 +52,5 @@
52
52
  },
53
53
  "sideEffects": false,
54
54
  "type": "module",
55
- "version": "0.1.0"
55
+ "version": "0.1.2"
56
56
  }