@skirbi/sugar 0.0.10 → 0.0.12

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,58 @@
1
1
  Revision history for @skirbi/sugar
2
2
 
3
+ 0.0.12 2026-04-13 00:51:51Z
4
+
5
+ * While checking other Semtic units, we tightened a few tests and exposed an
6
+ issue in how components were rendered and rerendered.
7
+
8
+ We added new test cases to separate what already worked from what did not,
9
+ and validated those cases against Livewire. That led to a small redesign of
10
+ the `connectedSugar` API.
11
+
12
+ The result is fairly clean: a consumer now defines two selectors:
13
+
14
+ * `renderGuardSelector` indicates whether the component is already fully
15
+ rendered.
16
+ * `morphTriggerSelector` indicates whether the component has been morphed back
17
+ to its authored state.
18
+
19
+ Based on these checks, `connectedCallbackSugar()` is called when needed, and
20
+ the component can normalize itself back to its canonical DOM.
21
+
22
+ The previously advised pattern for `connectedCallbackSugar()` is now
23
+ obsolete.
24
+
25
+ This
26
+
27
+ connectedCallbackSugar() {
28
+ this._compile();
29
+ this._observeChildListOnce(...);
30
+ }
31
+
32
+ Becomes:
33
+
34
+ class SugarParent extends withConnectedSugar(HTMLElementSugar) {
35
+ static tag = 'sugar-parent';
36
+
37
+ static renderGuardSelector = ':scope > div[data-sugar-parent]'
38
+ static morphTriggerSelector = ':scope > sugar-child';
39
+
40
+ static _tpl = this.tpl(`
41
+ <div data-sugar-parent></div>
42
+ `);
43
+
44
+ connectedCallbackSugar() {
45
+ /* Whatever you want to do here to (re-)render the component */
46
+ }
47
+ }
48
+
49
+
50
+ 0.0.11 2026-02-24 16:53:49Z
51
+
52
+ * Fix comparing HTML nodes. Whitespace text nodes are a PITA, we deal with
53
+ <F2>them and now you can just compare two nodes regardless how you wrote it.
54
+ Whitespace kinda matters in HTML I guess, until it doesn't. &nbsp;
55
+
3
56
  0.0.10 2026-02-24 03:09:34Z
4
57
 
5
58
  * Improve Livewire/morphdom support in withConnectedSugar
@@ -30,6 +83,28 @@ Revision history for @skirbi/sugar
30
83
  Where `your-tag-here` represents the authored child element that
31
84
  indicates the component has been reverted to its pre-compiled state.
32
85
 
86
+ The `_compile` function is part of the contract as it authors child
87
+ elements into the final DOM structure. But you are free give it any name
88
+ you'd like. An example implementation may look like this:
89
+
90
+ _compile() {
91
+ const steps = Array.from(this.children)
92
+ .filter(n => n.tagName?.toLowerCase() === 'x-step')
93
+ .map(n => n.textContent ?? '');
94
+
95
+ const frag = this.renderFromTemplate();
96
+ const ol = frag.querySelector('ol[data-steps]');
97
+
98
+ for (const label of steps) {
99
+ const li = document.createElement('li');
100
+ li.textContent = label;
101
+ ol.appendChild(li);
102
+ }
103
+
104
+ this.replaceChildren(frag);
105
+ }
106
+ }
107
+
33
108
  0.0.9 2026-02-23 10:56:27Z
34
109
 
35
110
  * Fix a bug for connectedCallbacks with Livewire
package/lib/index.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  //
3
3
  // SPDX-License-Identifier: MIT
4
4
 
5
- const VERSION = "0.0.10";
5
+ const VERSION = "0.0.12";
6
6
 
7
7
  export { HTMLElementSugar } from './htmlelement.mjs';
8
8
  export { HTMLElementSugarInput } from './htmlelement-input.mjs';
package/lib/testing.mjs CHANGED
@@ -129,8 +129,29 @@ export async function assertComponentContract(Class, example) {
129
129
  */
130
130
  function htmlToFragment(html) {
131
131
  const tpl = document.createElement('template');
132
- tpl.innerHTML = html.trim();
133
- return tpl.content;
132
+ tpl.innerHTML = html.trim().normalize();
133
+ return stripWhitespaceTextNodes(tpl.content);
134
+ }
135
+
136
+ /**
137
+ * Strip whitespace textnodes from an HTML fragment
138
+ *
139
+ * Use primairly to sanitize HTML to compare it
140
+ *
141
+ * @param {node} node - an HTML node
142
+ * @returns {node}
143
+ */
144
+ function stripWhitespaceTextNodes(node) {
145
+ for (const child of Array.from(node.childNodes)) {
146
+ if (child.nodeType === 3) { // TEXT_NODE
147
+ if (!child.nodeValue.trim()) {
148
+ child.remove();
149
+ }
150
+ } else {
151
+ stripWhitespaceTextNodes(child);
152
+ }
153
+ }
154
+ return node;
134
155
  }
135
156
 
136
157
  /**
@@ -1,6 +1,16 @@
1
1
  export const withConnectedSugar = (Base) =>
2
2
  class extends Base {
3
3
  static renderGuardSelector = '';
4
+ static morphTriggerSelector = '';
5
+
6
+ connectedCallback() {
7
+ this._syncObservedAttributesToConfig();
8
+ this._armMorphObserver();
9
+
10
+ if (this._shouldRenderOnConnect()) {
11
+ this.connectedCallbackSugar();
12
+ }
13
+ }
4
14
 
5
15
  _syncObservedAttributesToConfig() {
6
16
  for (const attr of this.constructor.observedAttributes ?? []) {
@@ -9,48 +19,43 @@ export const withConnectedSugar = (Base) =>
9
19
  }
10
20
  }
11
21
 
12
- _shouldRenderOnConnect() {
13
- const guard = this.constructor.renderGuardSelector;
14
- if (guard) return !this.querySelector(guard);
15
-
16
- if (this._rendered) return false;
17
- this._rendered = true;
18
- return true;
19
- }
20
-
21
- /**
22
- * Install a single MutationObserver that calls `fn()` when direct children
23
- * change and `when()` returns true.
24
- *
25
- * Intended for morphdom/livewire scenarios where the host stays the same
26
- * instance.
27
- *
28
- * @param {Function} when - predicate
29
- * @param {Function} fn - callback
30
- */
31
- _observeChildListOnce(when, fn) {
22
+ _armMorphObserver() {
32
23
  if (this._sugarMo) return;
33
24
 
34
25
  this._sugarMo = new MutationObserver(() => {
35
- if (when()) fn();
26
+ if (this._shouldRenderOnMorph()) {
27
+ this.connectedCallbackSugar();
28
+ }
36
29
  });
37
30
 
38
31
  this._sugarMo.observe(this, { childList: true });
39
32
  }
40
33
 
34
+ _shouldRenderOnMorph() {
35
+ return this._hasMorphTrigger() && !this._hasRenderedShape();
36
+ }
37
+
38
+ _hasMorphTrigger() {
39
+ const sel = this.constructor.morphTriggerSelector;
40
+ return !!(sel && this.querySelector(sel));
41
+ }
42
+
43
+ _shouldRenderOnConnect() {
44
+ const guard = this.constructor.renderGuardSelector;
45
+ if (guard) return !this._hasRenderedShape();
46
+
47
+ return true;
48
+ }
49
+
50
+ _hasRenderedShape() {
51
+ const sel = this.constructor.renderGuardSelector;
52
+ return !!(sel && this.querySelector(sel));
53
+ }
54
+
41
55
  disconnectedCallback() {
42
- // polite cleanup
43
56
  this._sugarMo?.disconnect();
44
57
  this._sugarMo = null;
45
58
  }
46
59
 
47
- connectedCallback() {
48
- this._syncObservedAttributesToConfig();
49
- if (!this._shouldRenderOnConnect()) return;
50
-
51
- if (typeof this.connectedCallbackSugar === 'function') {
52
- this.connectedCallbackSugar();
53
- }
54
- }
55
60
  };
56
61
 
package/package.json CHANGED
@@ -53,5 +53,5 @@
53
53
  },
54
54
  "sideEffects": false,
55
55
  "type": "module",
56
- "version": "0.0.10"
56
+ "version": "0.0.12"
57
57
  }