@skirbi/sugar 0.0.8 → 0.0.10
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 +67 -0
- package/lib/index.mjs +2 -1
- package/lib/testing.mjs +50 -14
- package/lib/with-connected-sugar.mjs +56 -0
- package/package.json +6 -5
package/Changes
CHANGED
|
@@ -1,5 +1,72 @@
|
|
|
1
1
|
Revision history for @skirbi/sugar
|
|
2
2
|
|
|
3
|
+
0.0.10 2026-02-24 03:09:34Z
|
|
4
|
+
|
|
5
|
+
* Improve Livewire/morphdom support in withConnectedSugar
|
|
6
|
+
|
|
7
|
+
In certain morph scenarios the host element instance remains the same
|
|
8
|
+
while its child nodes are replaced. In that case `connectedCallback`
|
|
9
|
+
is not triggered again.
|
|
10
|
+
|
|
11
|
+
The mixin now provides `_observeChildListOnce(when, fn)` which allows
|
|
12
|
+
components to react to child mutations and re-apply their compile logic
|
|
13
|
+
when necessary.
|
|
14
|
+
|
|
15
|
+
This keeps DOM-compiling components (e.g. semtic-steps in @skirbi/semtic)
|
|
16
|
+
stable even when morphing frameworks replace their internal markup without
|
|
17
|
+
recreating the custom element instance.
|
|
18
|
+
|
|
19
|
+
The pattern for a `connectedCallbackSugar` now becomes:
|
|
20
|
+
|
|
21
|
+
connectedCallbackSugar() {
|
|
22
|
+
this._compile();
|
|
23
|
+
|
|
24
|
+
this._observeChildListOnce(
|
|
25
|
+
() => this.querySelector(':scope > your-tag-here'),
|
|
26
|
+
() => this._compile()
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
Where `your-tag-here` represents the authored child element that
|
|
31
|
+
indicates the component has been reverted to its pre-compiled state.
|
|
32
|
+
|
|
33
|
+
0.0.9 2026-02-23 10:56:27Z
|
|
34
|
+
|
|
35
|
+
* Fix a bug for connectedCallbacks with Livewire
|
|
36
|
+
|
|
37
|
+
In tests with livewire we found out that the connectedCallback doesn't
|
|
38
|
+
always work when others do DOM manipulation. The solution is an optional
|
|
39
|
+
mixin that solves the problem for those cases when you need more logic
|
|
40
|
+
applied to your system.
|
|
41
|
+
|
|
42
|
+
The idea is such, you define `renderGuardSelector`, eg `footer`, we look
|
|
43
|
+
for this element in the DOM under your custom element. If we cannot find
|
|
44
|
+
it, we know we must rerender. So we do that, the only thing you need to
|
|
45
|
+
define if the how. You do that by implementing `connectedCallbackSugar()`.
|
|
46
|
+
|
|
47
|
+
Example:
|
|
48
|
+
class XActions extends withConnectedSugar(HTMLElementSugar) {
|
|
49
|
+
static tag = 'x-actions';
|
|
50
|
+
static renderGuardSelector = 'footer';
|
|
51
|
+
|
|
52
|
+
static _tpl = this.tpl(`
|
|
53
|
+
<footer>
|
|
54
|
+
<div semtic-body></div>
|
|
55
|
+
</footer>
|
|
56
|
+
`);
|
|
57
|
+
|
|
58
|
+
connectedCallbackSugar() {
|
|
59
|
+
const frag = this.renderFromTemplate();
|
|
60
|
+
const body = frag.querySelector('[semtic-body]');
|
|
61
|
+
|
|
62
|
+
// move children into body
|
|
63
|
+
while (this.firstChild) body.appendChild(this.firstChild);
|
|
64
|
+
|
|
65
|
+
this.replaceChildren(frag);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
XActions.register();
|
|
69
|
+
|
|
3
70
|
0.0.8 2026-02-23 03:32:55Z
|
|
4
71
|
|
|
5
72
|
* Add assertComponentContract testing method. The idea is this:
|
package/lib/index.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
//
|
|
3
3
|
// SPDX-License-Identifier: MIT
|
|
4
4
|
|
|
5
|
-
const VERSION = "0.0.
|
|
5
|
+
const VERSION = "0.0.10";
|
|
6
6
|
|
|
7
7
|
export { HTMLElementSugar } from './htmlelement.mjs';
|
|
8
8
|
export { HTMLElementSugarInput } from './htmlelement-input.mjs';
|
|
@@ -10,3 +10,4 @@ export { HTMLElementSugarSelect } from './htmlelement-select.mjs';
|
|
|
10
10
|
export { parseBoolean } from './boolean.mjs';
|
|
11
11
|
export { registerDevAlias, applyDevAliases } from './aliases.mjs';
|
|
12
12
|
export { registerAliases } from './aliases-register.mjs';
|
|
13
|
+
export { withConnectedSugar } from './with-connected-sugar.mjs';
|
package/lib/testing.mjs
CHANGED
|
@@ -56,23 +56,21 @@ export async function setupDomForTesting(
|
|
|
56
56
|
*
|
|
57
57
|
* This helper verifies:
|
|
58
58
|
*
|
|
59
|
-
* 1. `Class.exampleHTML` matches the provided `example` snippet.
|
|
60
|
-
* 2. After mounting and lifecycle execution, the rendered output
|
|
61
|
-
*
|
|
59
|
+
* 1. `Class.exampleHTML` matches the provided `options.example` snippet.
|
|
60
|
+
* 2. After mounting and lifecycle execution, the rendered output matches
|
|
61
|
+
* `Class.exampleRenderedHTML`.
|
|
62
62
|
*
|
|
63
|
-
* The component must define:
|
|
63
|
+
* The component class must define:
|
|
64
64
|
*
|
|
65
|
-
* - `static exampleHTML`
|
|
66
|
-
* - `static exampleRenderedHTML`
|
|
65
|
+
* - `static exampleHTML` (string)
|
|
66
|
+
* - `static exampleRenderedHTML` (string)
|
|
67
67
|
*
|
|
68
|
-
*
|
|
68
|
+
* It may also define:
|
|
69
69
|
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
* exampleRenderedHTML: string
|
|
75
|
-
* }} Class
|
|
70
|
+
* - `static register()` (optional)
|
|
71
|
+
* - `static init()` (optional)
|
|
72
|
+
*
|
|
73
|
+
* @param {Function} Class
|
|
76
74
|
* The component class under test.
|
|
77
75
|
*
|
|
78
76
|
* @param {Object} options
|
|
@@ -82,7 +80,7 @@ export async function setupDomForTesting(
|
|
|
82
80
|
* @param {string} options.example
|
|
83
81
|
* Expected authoring HTML to compare against `Class.exampleHTML`.
|
|
84
82
|
*
|
|
85
|
-
* @param {
|
|
83
|
+
* @param {Object} options.tap
|
|
86
84
|
* Tap test instance used for assertions.
|
|
87
85
|
*
|
|
88
86
|
* @returns {Promise<void>}
|
|
@@ -116,6 +114,8 @@ export async function assertComponentContract(Class, example) {
|
|
|
116
114
|
needle.isEqualNode(haystack),
|
|
117
115
|
'connectedCallback works',
|
|
118
116
|
);
|
|
117
|
+
|
|
118
|
+
if (el.isConnected) document.body.removeChild(el);
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
/**
|
|
@@ -133,3 +133,39 @@ function htmlToFragment(html) {
|
|
|
133
133
|
return tpl.content;
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
/**
|
|
137
|
+
* Wait for the DOM lifecycle to settle.
|
|
138
|
+
*
|
|
139
|
+
* Resolves after one microtask and one macrotask tick.
|
|
140
|
+
*
|
|
141
|
+
* Useful in tests to ensure:
|
|
142
|
+
* - customElements upgrades have completed
|
|
143
|
+
* - connectedCallback has run
|
|
144
|
+
* - DOM mutations triggered by innerHTML have flushed
|
|
145
|
+
*
|
|
146
|
+
* @param {number} [times=1] - Number of ticks to wait.
|
|
147
|
+
* @returns {Promise<void>}
|
|
148
|
+
*/
|
|
149
|
+
export async function tick(times = 1) {
|
|
150
|
+
for (let i = 0; i < times; i++) {
|
|
151
|
+
await Promise.resolve(); // microtask
|
|
152
|
+
await new Promise(r => setTimeout(r, 0)); // macrotask
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Simulate a morph-style DOM replacement (e.g. Livewire / morphdom).
|
|
158
|
+
*
|
|
159
|
+
* Replaces the element's innerHTML and waits for lifecycle hooks
|
|
160
|
+
* and custom element upgrades to complete.
|
|
161
|
+
*
|
|
162
|
+
* Intended for testing idempotent connectedCallback behavior.
|
|
163
|
+
*
|
|
164
|
+
* @param {Element} el - The container element to replace content in.
|
|
165
|
+
* @param {string} html - The HTML string to inject.
|
|
166
|
+
* @returns {Promise<void>}
|
|
167
|
+
*/
|
|
168
|
+
export async function morphReplace(el, html) {
|
|
169
|
+
el.innerHTML = html;
|
|
170
|
+
await tick();
|
|
171
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export const withConnectedSugar = (Base) =>
|
|
2
|
+
class extends Base {
|
|
3
|
+
static renderGuardSelector = '';
|
|
4
|
+
|
|
5
|
+
_syncObservedAttributesToConfig() {
|
|
6
|
+
for (const attr of this.constructor.observedAttributes ?? []) {
|
|
7
|
+
const val = this.getAttribute(attr);
|
|
8
|
+
if (val !== null) this._applyAttribute(attr, val);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
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) {
|
|
32
|
+
if (this._sugarMo) return;
|
|
33
|
+
|
|
34
|
+
this._sugarMo = new MutationObserver(() => {
|
|
35
|
+
if (when()) fn();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
this._sugarMo.observe(this, { childList: true });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
disconnectedCallback() {
|
|
42
|
+
// polite cleanup
|
|
43
|
+
this._sugarMo?.disconnect();
|
|
44
|
+
this._sugarMo = null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
connectedCallback() {
|
|
48
|
+
this._syncObservedAttributesToConfig();
|
|
49
|
+
if (!this._shouldRenderOnConnect()) return;
|
|
50
|
+
|
|
51
|
+
if (typeof this.connectedCallbackSugar === 'function') {
|
|
52
|
+
this.connectedCallbackSugar();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"name": "Wesley Schwengle"
|
|
5
5
|
},
|
|
6
6
|
"bugs": {
|
|
7
|
-
"url": "https://gitlab.com/skirbi/
|
|
7
|
+
"url": "https://gitlab.com/skirbi/skirbi/-/issues"
|
|
8
8
|
},
|
|
9
9
|
"dependencies": {
|
|
10
10
|
"@opndev/util": "latest"
|
|
@@ -26,9 +26,10 @@
|
|
|
26
26
|
"./htmlelement": "./lib/htmlelement.mjs",
|
|
27
27
|
"./htmlelement-input": "./lib/htmlelement-input.mjs",
|
|
28
28
|
"./htmlelement-select": "./lib/htmlelement-select.mjs",
|
|
29
|
-
"./testing": "./lib/testing.mjs"
|
|
29
|
+
"./testing": "./lib/testing.mjs",
|
|
30
|
+
"./with-connected-sugar": "./lib/with-connected-sugar.mjs"
|
|
30
31
|
},
|
|
31
|
-
"homepage": "https://gitlab.com/skirbi/
|
|
32
|
+
"homepage": "https://gitlab.com/skirbi/skirbi",
|
|
32
33
|
"keywords": [
|
|
33
34
|
"custom elements",
|
|
34
35
|
"web components",
|
|
@@ -41,7 +42,7 @@
|
|
|
41
42
|
"name": "@skirbi/sugar",
|
|
42
43
|
"repository": {
|
|
43
44
|
"type": "git",
|
|
44
|
-
"url": "git+https://gitlab.com/skirbi/
|
|
45
|
+
"url": "git+https://gitlab.com/skirbi/skirbi.git"
|
|
45
46
|
},
|
|
46
47
|
"scripts": {
|
|
47
48
|
"build": "rzil build",
|
|
@@ -52,5 +53,5 @@
|
|
|
52
53
|
},
|
|
53
54
|
"sideEffects": false,
|
|
54
55
|
"type": "module",
|
|
55
|
-
"version": "0.0.
|
|
56
|
+
"version": "0.0.10"
|
|
56
57
|
}
|