@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 +75 -0
- package/lib/index.mjs +1 -1
- package/lib/testing.mjs +23 -2
- package/lib/with-connected-sugar.mjs +35 -30
- package/package.json +1 -1
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.
|
|
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
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
|
-
|
|
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 (
|
|
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