@skirbi/sugar 0.0.6
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 +77 -0
- package/LICENSE +18 -0
- package/README.md +454 -0
- package/lib/aliases-register.mjs +21 -0
- package/lib/aliases.mjs +49 -0
- package/lib/boolean.mjs +22 -0
- package/lib/htmlelement-input.mjs +197 -0
- package/lib/htmlelement-select.mjs +251 -0
- package/lib/htmlelement.mjs +331 -0
- package/lib/index.mjs +7 -0
- package/lib/testing.mjs +35 -0
- package/package.json +53 -0
package/Changes
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
Revision history for @skirbi/sugar
|
|
2
|
+
|
|
3
|
+
0.0.6 2026-02-20 04:30:44Z
|
|
4
|
+
|
|
5
|
+
* Add a way to actually add aliases. It seems between time of initial
|
|
6
|
+
development and now my alias approach didn't work. Add a way to register a
|
|
7
|
+
non-valid spec breaking HTML tag which gets upgraded `OnDocumentLoad`. That
|
|
8
|
+
way you can still write `<book>` but have to target `<my-book>` in CSS.
|
|
9
|
+
Sad, but true.
|
|
10
|
+
* Yay renames. Since I'm building a whole suite or family of suites that deal
|
|
11
|
+
with webcomponents and HTML authoring I've decided to move everything to
|
|
12
|
+
the skirbi org on npm. So this package will from now onwards be called
|
|
13
|
+
@skirbi/sugar. Why? Well, because we will introduce @skirbi/semtic and
|
|
14
|
+
@skirbi/theme(s) in the near future. I feel this shouldn't be under my
|
|
15
|
+
companies name. Eventho that is a major sponsor of the project.
|
|
16
|
+
|
|
17
|
+
@skirbi/sugar meet world
|
|
18
|
+
|
|
19
|
+
No more renames, I promise :)
|
|
20
|
+
|
|
21
|
+
0.0.5 2026-02-17 17:05:05Z
|
|
22
|
+
|
|
23
|
+
* Fix small CI glitch for tag-builds
|
|
24
|
+
|
|
25
|
+
* For another project I wanted to have selectboxes that implement the
|
|
26
|
+
following API:
|
|
27
|
+
|
|
28
|
+
<my-select-box options='{"json": "here"}' jpath-label="foo"
|
|
29
|
+
jpath-value="bar" endpoint="https://foo.example.com"></my-select-box>
|
|
30
|
+
|
|
31
|
+
Instead of having to reinvent the wheel everywhere I've decided to
|
|
32
|
+
implement this in skirbi so this pattern can be reused by downstream
|
|
33
|
+
consumers.
|
|
34
|
+
|
|
35
|
+
* Fix bug to allow inheritence by removing hasOwnProperty()
|
|
36
|
+
|
|
37
|
+
Now subclasses can be “tag-only” building blocks and still inherit the
|
|
38
|
+
config contract from HTMLElementSugarSelect (HTMLElementSugar* in
|
|
39
|
+
general).
|
|
40
|
+
|
|
41
|
+
This also makes skirbi behave more like people expect:
|
|
42
|
+
base classes can define defaults + observed attrs, and subclasses can just
|
|
43
|
+
set tag.
|
|
44
|
+
|
|
45
|
+
0.0.4 2026-02-17 02:47:33Z
|
|
46
|
+
|
|
47
|
+
* Update README.md
|
|
48
|
+
* Rename module from @opndev/webcomponents-core to @opndev/skirbi
|
|
49
|
+
I'll leave the old module in place, so you stay on version 0.0.3 forever,
|
|
50
|
+
but going forward this is the new name.
|
|
51
|
+
|
|
52
|
+
0.0.3 2026-02-16 22:14:34Z
|
|
53
|
+
|
|
54
|
+
* jsdom got added as a runtime dep. This isn't the case. It is a test
|
|
55
|
+
dependency which you need if you use things from
|
|
56
|
+
@opndev/webcomponents-core/testing. Fixed the glitch.
|
|
57
|
+
|
|
58
|
+
* Add HTMLElementSugarInput for a base class that can be used for input
|
|
59
|
+
parameters. It adds logic so input types can be used in for example
|
|
60
|
+
livewire projects. Which was the main reason for the creation of the class.
|
|
61
|
+
Livewire isn't the only intended audience tho, feel free to use it in other
|
|
62
|
+
frameworks.
|
|
63
|
+
|
|
64
|
+
0.0.2 2026-02-16 01:50:02Z
|
|
65
|
+
|
|
66
|
+
* Add new HTMLTemplateSpec so you as webcomponent author can set a default
|
|
67
|
+
template but also allow consumers to use a html template found in HTML.
|
|
68
|
+
This is mainly intended for theme authors.
|
|
69
|
+
* Add tpl() static function for easier dealing with templates
|
|
70
|
+
* Derive the tag from the ClassName: class-name. You can now omit a tag and
|
|
71
|
+
it's derived from the class name. If you want a different class name and a
|
|
72
|
+
different tag, just set the tag to what-ever-you-want.
|
|
73
|
+
* rzilla release
|
|
74
|
+
|
|
75
|
+
0.0.1 2025-10-17T23:00:00-0400
|
|
76
|
+
|
|
77
|
+
* First release
|
package/LICENSE
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) <year> <copyright holders>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
|
6
|
+
associated documentation files (the "Software"), to deal in the Software without restriction, including
|
|
7
|
+
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
|
|
9
|
+
following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial
|
|
12
|
+
portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
|
15
|
+
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
|
|
16
|
+
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
17
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
|
18
|
+
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
SPDX-FileCopyrightText: 2025, 2026 Wesley Schwengle <wesleys@opperschaap.net>
|
|
3
|
+
|
|
4
|
+
SPDX-License-Identifier: MIT
|
|
5
|
+
-->
|
|
6
|
+
|
|
7
|
+
# @skirbi/sugar
|
|
8
|
+
|
|
9
|
+
A lightweight base layer for building Web Components.
|
|
10
|
+
|
|
11
|
+
It provides:
|
|
12
|
+
|
|
13
|
+
- Declarative attribute → config mapping
|
|
14
|
+
- Template helpers
|
|
15
|
+
- Alias utilities
|
|
16
|
+
- Form-control wrapping primitives
|
|
17
|
+
|
|
18
|
+
## TL;DR
|
|
19
|
+
|
|
20
|
+
You can create Web Components like so:
|
|
21
|
+
|
|
22
|
+
```javascript
|
|
23
|
+
class MyNewElement extends HTMLElementSugar {
|
|
24
|
+
static tag = 'my-new-element';
|
|
25
|
+
static attributeDefs = {
|
|
26
|
+
foo: { default: 'bar' },
|
|
27
|
+
bar: { parser: parseBoolean },
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
You can also create aliases:
|
|
33
|
+
|
|
34
|
+
```javascript
|
|
35
|
+
TrackItem.alias('some-song', { type: 'song' });
|
|
36
|
+
YourBook.alias('a-book');
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
And to register them:
|
|
40
|
+
|
|
41
|
+
```javascript
|
|
42
|
+
TrackItem.register();
|
|
43
|
+
YourBook.register();
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Observable patterns
|
|
47
|
+
|
|
48
|
+
You can define attributes to be observable:
|
|
49
|
+
|
|
50
|
+
```javascript
|
|
51
|
+
class MyNewElement extends HTMLElementSugar {
|
|
52
|
+
static tag = 'my-new-element';
|
|
53
|
+
static attributeDefs = {
|
|
54
|
+
'foo': { default: 'bar' },
|
|
55
|
+
'bar': { parser: parseBoolean },
|
|
56
|
+
'baz': null,
|
|
57
|
+
'qux': {},
|
|
58
|
+
'toto': { observed: "" }, // not observed
|
|
59
|
+
'titi': { observed: 0 }, // not observed
|
|
60
|
+
'tata': { observed: false }, // not observed
|
|
61
|
+
'tete': { observed: null }, // not observed
|
|
62
|
+
'yes': { observed: true },
|
|
63
|
+
'yez': { observed: 1 },
|
|
64
|
+
'yep': { observed: "here be dragons" },
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Templates
|
|
70
|
+
|
|
71
|
+
### Template via HTML template reference
|
|
72
|
+
|
|
73
|
+
```javascript
|
|
74
|
+
class HTMLTemplate extends HTMLElementSugar {
|
|
75
|
+
static tag = 'html-template';
|
|
76
|
+
static HtmlTemplate = 'track-item-template'; // find in html
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
HTMLElementSugar asserts that it can find the template on registration. Which
|
|
81
|
+
means you need to have the template ready in HTML. It is therefore essential
|
|
82
|
+
that you register your component in window.addEventListener('DOMContentLoaded',
|
|
83
|
+
() => { }.
|
|
84
|
+
|
|
85
|
+
### Template via javascript element reference
|
|
86
|
+
|
|
87
|
+
```javascript
|
|
88
|
+
const tpl = document.createElement('template');
|
|
89
|
+
tpl.id = 'inline';
|
|
90
|
+
tpl.innerHTML =
|
|
91
|
+
`<div class="track-row"><span class="song">song</span></div>`;
|
|
92
|
+
document.body.appendChild(tpl);
|
|
93
|
+
|
|
94
|
+
class JSElement extends HTMLElementSugar {
|
|
95
|
+
static tag = 'js-element';
|
|
96
|
+
static HtmlTemplate = tpl;
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Template via javascript function
|
|
101
|
+
|
|
102
|
+
```javascript
|
|
103
|
+
class TemplateFunction extends HTMLElementSugar {
|
|
104
|
+
static tag = 'template-function';
|
|
105
|
+
|
|
106
|
+
static HtmlTemplate() {
|
|
107
|
+
const t = document.createElement('template');
|
|
108
|
+
t.innerHTML =
|
|
109
|
+
`<div class="track-row"><div class="track-info">from fn</div></div>`;
|
|
110
|
+
return t;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
We recently added a helper you can now do this too:
|
|
116
|
+
|
|
117
|
+
```javascript
|
|
118
|
+
class TemplateFunction extends HTMLElementSugar {
|
|
119
|
+
static tag = 'template-function';
|
|
120
|
+
|
|
121
|
+
static HtmlTemplate = this.tpl(`
|
|
122
|
+
<div class="track-row">
|
|
123
|
+
<div class="track-info">from tpl</div>
|
|
124
|
+
</div>
|
|
125
|
+
`);
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Custom template with fallback
|
|
130
|
+
|
|
131
|
+
```javascript
|
|
132
|
+
class TemplateFunction extends HTMLElementSugar {
|
|
133
|
+
static tag = 'template-function';
|
|
134
|
+
|
|
135
|
+
static HtmlTemplate = [
|
|
136
|
+
'some-id-in-html',
|
|
137
|
+
() => {
|
|
138
|
+
const t = document.createElement('template');
|
|
139
|
+
t.innerHTML =
|
|
140
|
+
`<div class="track-row"><div class="track-info">from fn</div></div>`;
|
|
141
|
+
return t;
|
|
142
|
+
}
|
|
143
|
+
];
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### No template (the ultimate minimalism)
|
|
148
|
+
|
|
149
|
+
```javascript
|
|
150
|
+
class MinimalistComponent extends HTMLElementSugar {
|
|
151
|
+
static tag = 'ultimate-minimalist';
|
|
152
|
+
// Look at all this template code I'm not writing
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
You can register your Web Components by running:
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
MyComponent.register();
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
# HTMLElementSugarInput
|
|
163
|
+
|
|
164
|
+
`HTMLElementSugarInput` extends `HTMLElementSugar` and is designed for
|
|
165
|
+
components that wrap a real form control in the light DOM.
|
|
166
|
+
|
|
167
|
+
It provides:
|
|
168
|
+
|
|
169
|
+
- Attribute forwarding to the real control
|
|
170
|
+
- Native `input` and `change` re-emission
|
|
171
|
+
- Optional attribute mirroring via `data-sync`
|
|
172
|
+
- Proper `value` property handling
|
|
173
|
+
|
|
174
|
+
## Contract
|
|
175
|
+
|
|
176
|
+
A subclass must:
|
|
177
|
+
|
|
178
|
+
- Extend `HTMLElementSugarInput`
|
|
179
|
+
- Render exactly one element matching `[wc-control]`
|
|
180
|
+
(or override `static controlSelector`)
|
|
181
|
+
|
|
182
|
+
Example:
|
|
183
|
+
|
|
184
|
+
```javascript
|
|
185
|
+
class MyInput extends HTMLElementSugarInput {
|
|
186
|
+
static tag = 'my-input';
|
|
187
|
+
|
|
188
|
+
static attributeDefs = {
|
|
189
|
+
label: { default: '' },
|
|
190
|
+
value: { default: '' },
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
static HtmlTemplate = this.tpl(`
|
|
194
|
+
<div>
|
|
195
|
+
<label wc-label></label>
|
|
196
|
+
<input wc-control type="text">
|
|
197
|
+
</div>
|
|
198
|
+
`);
|
|
199
|
+
|
|
200
|
+
connectedCallback() {
|
|
201
|
+
super.connectedCallback();
|
|
202
|
+
|
|
203
|
+
const frag = this.renderFromTemplate();
|
|
204
|
+
const control = this.enhanceControl(frag);
|
|
205
|
+
|
|
206
|
+
const { label, value } = this.getConfig();
|
|
207
|
+
|
|
208
|
+
frag.querySelector('[wc-label]').textContent = label;
|
|
209
|
+
control.value = value ?? '';
|
|
210
|
+
|
|
211
|
+
this.replaceChildren(frag);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
MyInput.register();
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## Attribute Forwarding
|
|
219
|
+
|
|
220
|
+
All host attributes that are **not defined in `attributeDefs`**
|
|
221
|
+
are forwarded to the real control element.
|
|
222
|
+
|
|
223
|
+
Example:
|
|
224
|
+
|
|
225
|
+
```html
|
|
226
|
+
<my-input
|
|
227
|
+
label="Name"
|
|
228
|
+
wire:model.defer="name"
|
|
229
|
+
aria-label="Name"
|
|
230
|
+
></my-input>
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Results in:
|
|
234
|
+
|
|
235
|
+
```html
|
|
236
|
+
<input
|
|
237
|
+
wire:model.defer="name"
|
|
238
|
+
aria-label="Name"
|
|
239
|
+
>
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### Not forwarded
|
|
243
|
+
|
|
244
|
+
- Attributes defined in `attributeDefs`
|
|
245
|
+
- `id`, `class`, `style` (kept on the host)
|
|
246
|
+
|
|
247
|
+
This keeps components framework-agnostic:
|
|
248
|
+
|
|
249
|
+
- Livewire (`wire:*`)
|
|
250
|
+
- Alpine (`x-*`)
|
|
251
|
+
- HTMX (`hx-*`)
|
|
252
|
+
- `aria-*`
|
|
253
|
+
- `data-*`
|
|
254
|
+
|
|
255
|
+
## Event Re-Emission
|
|
256
|
+
|
|
257
|
+
Native events from the control are re-emitted from the host:
|
|
258
|
+
|
|
259
|
+
- `input`
|
|
260
|
+
- `change`
|
|
261
|
+
|
|
262
|
+
Listeners may bind to either the host or the internal control.
|
|
263
|
+
|
|
264
|
+
## data-sync (Optional Attribute Mirroring)
|
|
265
|
+
|
|
266
|
+
By default, attributes are forwarded once during connect.
|
|
267
|
+
|
|
268
|
+
If you enable `data-sync`, subsequent attribute changes on the host are
|
|
269
|
+
mirrored to the control using a MutationObserver.
|
|
270
|
+
|
|
271
|
+
Example:
|
|
272
|
+
|
|
273
|
+
```html
|
|
274
|
+
<my-input wire:model.defer="name" data-sync></my-input>
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
When `data-sync` is enabled:
|
|
278
|
+
|
|
279
|
+
- Host attribute changes are mirrored to the control
|
|
280
|
+
- `value` is mirrored as a property (not an attribute)
|
|
281
|
+
- Removing attributes removes them from the control
|
|
282
|
+
|
|
283
|
+
This is particularly useful in reactive environments such as Livewire.
|
|
284
|
+
|
|
285
|
+
## HTMLElementSugarSelect
|
|
286
|
+
|
|
287
|
+
HTMLElementSugarSelect extends HTMLElementSugarInput and provides a
|
|
288
|
+
structured way to author `<select>`-based components.
|
|
289
|
+
|
|
290
|
+
It supports:
|
|
291
|
+
* Static option lists (via JSON)
|
|
292
|
+
* Remote option loading (via endpoint)
|
|
293
|
+
* Flexible data mapping via jpath
|
|
294
|
+
* Label/value templates
|
|
295
|
+
* Optional search input
|
|
296
|
+
* Attribute forwarding to the underlying `<select>`
|
|
297
|
+
|
|
298
|
+
It always renders a real `<select>` in the light DOM.
|
|
299
|
+
|
|
300
|
+
### Contract
|
|
301
|
+
|
|
302
|
+
A subclass must:
|
|
303
|
+
* Extend HTMLElementSugarSelect
|
|
304
|
+
* Render exactly one `<select wc-control>`
|
|
305
|
+
* Optionally include a search input if searchable is enabled
|
|
306
|
+
|
|
307
|
+
Example:
|
|
308
|
+
|
|
309
|
+
```javascript
|
|
310
|
+
class MySelect extends HTMLElementSugarSelect {
|
|
311
|
+
static tag = 'my-select';
|
|
312
|
+
|
|
313
|
+
static HtmlTemplate = this.tpl(`
|
|
314
|
+
<select wc-control></select>
|
|
315
|
+
`);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
MySelect.register();
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
### Static Options
|
|
322
|
+
|
|
323
|
+
You may provide options via the options attribute as a JSON array:
|
|
324
|
+
|
|
325
|
+
```html
|
|
326
|
+
<my-select
|
|
327
|
+
options='[
|
|
328
|
+
{ "id": 1, "name": "Alice" },
|
|
329
|
+
{ "id": 2, "name": "Bob" }
|
|
330
|
+
]'
|
|
331
|
+
jpath-label="name"
|
|
332
|
+
jpath-value="id"
|
|
333
|
+
></my-select>
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
### Mapping Options
|
|
337
|
+
|
|
338
|
+
Data mapping is controlled via:
|
|
339
|
+
* `jpath`: path to the array in a nested JSON response
|
|
340
|
+
* `jpath-label`: path for the option label
|
|
341
|
+
* `jpath-value`: path for the option value
|
|
342
|
+
* `jpath-label-template`: template string for label
|
|
343
|
+
* `jpath-value-template`: template string for value
|
|
344
|
+
|
|
345
|
+
Templates take precedence over path mappings.
|
|
346
|
+
|
|
347
|
+
Example:
|
|
348
|
+
|
|
349
|
+
```html
|
|
350
|
+
<my-select
|
|
351
|
+
options='[
|
|
352
|
+
{ "id": 1, "name": "Alice", "email": "a@example.com" }
|
|
353
|
+
]'
|
|
354
|
+
jpath-label-template="{name} ({email})"
|
|
355
|
+
jpath-value-template="user:{id}"
|
|
356
|
+
></my-select>
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
Missing template fields resolve to empty strings.
|
|
360
|
+
|
|
361
|
+
### Remote Options
|
|
362
|
+
|
|
363
|
+
Options can be loaded from a remote endpoint:
|
|
364
|
+
|
|
365
|
+
```html
|
|
366
|
+
<my-select
|
|
367
|
+
endpoint="/api/users"
|
|
368
|
+
method="GET"
|
|
369
|
+
param="q"
|
|
370
|
+
searchable
|
|
371
|
+
min-chars="2"
|
|
372
|
+
debounce="300"
|
|
373
|
+
nosearch-initial
|
|
374
|
+
></my-select>
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
Behavior:
|
|
378
|
+
* Fetches JSON from endpoint
|
|
379
|
+
* Adds search query via param
|
|
380
|
+
* Applies mapping rules
|
|
381
|
+
* Replaces options on each fetch
|
|
382
|
+
|
|
383
|
+
If `searchable` is enabled:
|
|
384
|
+
* A search input is rendered
|
|
385
|
+
* Input is debounced
|
|
386
|
+
* Fetch is triggered after min-chars is reached
|
|
387
|
+
* By default, a remote endpoint triggers an initial fetch with an empty query
|
|
388
|
+
on connect. If `nosearch-initial` is enabled this behavior is negated.
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
* No initial searches are
|
|
392
|
+
|
|
393
|
+
### Search Behavior (Static Mode)
|
|
394
|
+
|
|
395
|
+
When using static options, enabling searchable:
|
|
396
|
+
* Filters `<option>` elements in place
|
|
397
|
+
* Uses case-insensitive substring matching
|
|
398
|
+
* Does not modify underlying data
|
|
399
|
+
|
|
400
|
+
### Value Handling
|
|
401
|
+
|
|
402
|
+
The value attribute behaves consistently with `HTMLElementSugarInput`:
|
|
403
|
+
* Initial value is applied after options render
|
|
404
|
+
* With data-sync, updates are mirrored to the `<select>` element
|
|
405
|
+
* Native input and change events are re-emitted from the host
|
|
406
|
+
|
|
407
|
+
### Framework Compatibility
|
|
408
|
+
|
|
409
|
+
Because it renders a real `<select>` in the light DOM, it works naturally with:
|
|
410
|
+
* Livewire (`wire:*`)
|
|
411
|
+
* Alpine (`x-*`)
|
|
412
|
+
* HTMX (`hx-*`)
|
|
413
|
+
* `aria-*`
|
|
414
|
+
* `data-*`
|
|
415
|
+
|
|
416
|
+
Attributes not defined in attributeDefs are forwarded to the `<select>`
|
|
417
|
+
element.
|
|
418
|
+
|
|
419
|
+
### Why This Works
|
|
420
|
+
* No shadow DOM
|
|
421
|
+
* No custom dropdown UI
|
|
422
|
+
* No CSS lock-in
|
|
423
|
+
* Real `<option>` elements
|
|
424
|
+
* Progressive enhancement friendly
|
|
425
|
+
|
|
426
|
+
## Code of Conduct
|
|
427
|
+
|
|
428
|
+
Be human.
|
|
429
|
+
|
|
430
|
+
## Developer notes about this package
|
|
431
|
+
|
|
432
|
+
### Versioning
|
|
433
|
+
|
|
434
|
+
This project does not follow semver. It follows a Perl-style release philosophy
|
|
435
|
+
centered on backward compatibility. This translates to the following hard
|
|
436
|
+
guarantee: We do not intentionally break working code. If a release causes
|
|
437
|
+
breakage, it will be addressed accordingly.
|
|
438
|
+
|
|
439
|
+
The `x.y.z` version number should not be used to infer stability.
|
|
440
|
+
Consult the Changes file for important updates, deprecations,
|
|
441
|
+
and breaking changes.
|
|
442
|
+
|
|
443
|
+
The current `0.x.z` range does not imply alpha, beta, or instability.
|
|
444
|
+
It is simply the starting point of the project.
|
|
445
|
+
|
|
446
|
+
In case we foresee breaking changes we'll add deprecation warnings. Giving you
|
|
447
|
+
ample time to fix things before a breaking change will be introduced. When a
|
|
448
|
+
change will be introduced is communicated in the Changes file. Security fixes
|
|
449
|
+
may cause breakage at any given time without notice.
|
|
450
|
+
|
|
451
|
+
This package is released by `@opndev/rzilla`, changes to `package.json` will be
|
|
452
|
+
overridden. In addition to a little bit of promotion, this also means that
|
|
453
|
+
version numbers are autoincremented at release time and bumped in all relevant
|
|
454
|
+
files: Versioning for humans, not machines.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Wesley Schwengle <wesleys@opperschaap.net>
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: MIT
|
|
4
|
+
|
|
5
|
+
import { applyDevAliases } from './aliases.mjs';
|
|
6
|
+
|
|
7
|
+
export function registerAliases(root = document) {
|
|
8
|
+
applyDevAliases(root);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Run immediately if DOM is ready, otherwise on DOMContentLoaded.
|
|
12
|
+
if (typeof document !== 'undefined') {
|
|
13
|
+
if (document.readyState === 'loading') {
|
|
14
|
+
document.addEventListener('DOMContentLoaded', () => registerAliases(), {
|
|
15
|
+
once: true,
|
|
16
|
+
});
|
|
17
|
+
} else {
|
|
18
|
+
registerAliases();
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
package/lib/aliases.mjs
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Wesley Schwengle <wesleys@opperschaap.net>
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: MIT
|
|
4
|
+
|
|
5
|
+
const KEY = '__skirbi_dev_aliases__';
|
|
6
|
+
|
|
7
|
+
function getMap() {
|
|
8
|
+
globalThis[KEY] ??= new Map();
|
|
9
|
+
return globalThis[KEY];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Register a dev alias (authoring sugar): <from> -> <to>
|
|
13
|
+
export function registerDevAlias(from, to, defaultAttributes = {}) {
|
|
14
|
+
if (!from || !to) throw new Error('registerDevAlias(from, to): missing args');
|
|
15
|
+
|
|
16
|
+
getMap().set(String(from).toLowerCase(), {
|
|
17
|
+
to: String(to).toLowerCase(),
|
|
18
|
+
defaultAttributes: defaultAttributes ?? {},
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Rewrite all dev aliases under root.
|
|
23
|
+
export function applyDevAliases(root = document) {
|
|
24
|
+
const map = getMap();
|
|
25
|
+
|
|
26
|
+
for (const [from, spec] of map.entries()) {
|
|
27
|
+
const nodes = root.querySelectorAll(from);
|
|
28
|
+
|
|
29
|
+
for (const oldEl of nodes) {
|
|
30
|
+
const neu = document.createElement(spec.to);
|
|
31
|
+
|
|
32
|
+
// Copy attributes
|
|
33
|
+
for (const { name, value } of Array.from(oldEl.attributes)) {
|
|
34
|
+
neu.setAttribute(name, value);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Apply default attributes if missing
|
|
38
|
+
for (const [k, v] of Object.entries(spec.defaultAttributes)) {
|
|
39
|
+
if (!neu.hasAttribute(k)) neu.setAttribute(k, v);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Move children
|
|
43
|
+
while (oldEl.firstChild) neu.appendChild(oldEl.firstChild);
|
|
44
|
+
|
|
45
|
+
oldEl.replaceWith(neu);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
package/lib/boolean.mjs
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2025 Wesley Schwengle <wesleys@opperschaap.net>
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: MIT
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Normalize an HTML attribute value to a boolean.
|
|
7
|
+
*
|
|
8
|
+
* This function interprets a variety of string-like inputs as either `true` or `false`:
|
|
9
|
+
* - Empty string ("") becomes true (e.g. <input disabled>)
|
|
10
|
+
* - "false" and "0" become false
|
|
11
|
+
* - All other defined values become true
|
|
12
|
+
*
|
|
13
|
+
* @param {string | null | undefined} value - The attribute value to parse
|
|
14
|
+
* @returns {boolean} The normalized boolean result
|
|
15
|
+
*/
|
|
16
|
+
export function parseBoolean(value) {
|
|
17
|
+
if (value === null || value === undefined) return false;
|
|
18
|
+
|
|
19
|
+
const val = String(value).toLowerCase().trim();
|
|
20
|
+
|
|
21
|
+
return val !== 'false' && val !== '0';
|
|
22
|
+
}
|