@ramstack/alpinegear-bound 1.2.3 → 1.3.0

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/README.md CHANGED
@@ -6,6 +6,10 @@
6
6
 
7
7
  This directive allows for two-way binding between input elements and their associated data properties. It works similarly to the binding provided by [Svelte](https://svelte.dev/docs/element-directives#bind-property) and also supports synchronizing values between two `Alpine.js` data properties.
8
8
 
9
+ > [!Note]
10
+ > This package is part of the **[`@ramstack/alpinegear-main`](https://www.npmjs.com/package/@ramstack/alpinegear-main)** bundle.
11
+ > If you are using the main bundle, you don't need to install this package separately.
12
+
9
13
  ## Installation
10
14
 
11
15
  ### Using CDN
@@ -16,7 +20,7 @@ To include the CDN version of this plugin, add the following `<script>` tag befo
16
20
  <script src="https://cdn.jsdelivr.net/npm/@ramstack/alpinegear-bound@1/alpinegear-bound.min.js" defer></script>
17
21
 
18
22
  <!-- alpine.js -->
19
- <script src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
23
+ <script src="https://cdn.jsdelivr.net/npm/alpinejs@3/dist/cdn.min.js" defer></script>
20
24
  ```
21
25
 
22
26
  ### Using NPM
@@ -44,12 +48,13 @@ Let's take the following example:
44
48
 
45
49
  ```html
46
50
  <div x-data="{ name: '' }">
47
- <input x-bound:value="name" />
48
- Hello <span x-text="name"></span>!
51
+ <input x-bound:value="name" />
52
+ Hello <span x-text="name"></span>!
49
53
 
50
- <button @click="name = 'John'">Change Name</button>
54
+ <button @click="name = 'John'">Change Name</button>
51
55
  </div>
52
56
  ```
57
+ 🚀 [Live demo | Alpine.js x-bound: Basic usage](https://jsfiddle.net/rameel/8cw23y7o/)
53
58
 
54
59
  In this example, we bind the `name` property to the `value` property of the `<input>` element. Since `x-bound` provides two-way binding, any changes to `name` will be reflected in the `<input>` element, as will occur when the `button` is clicked.
55
60
 
@@ -76,12 +81,16 @@ In this example, the repetition of the `value` in `x-bound:value="value"` is red
76
81
  More examples:
77
82
 
78
83
  ```html
79
- <div x-data="{ name: '', text: '', yes: true }">
80
- <input &value="name" />
81
- <textarea &value="text"></textarea>
82
- <input &checked="yes" type="checkbox" />
84
+ <div x-data="{ name: '', text: '', yes: true, methods: [] }">
85
+ <input &value="name" />
86
+ <textarea &value="text"></textarea>
87
+ <input &checked="yes" type="checkbox" />
88
+ <select &value="methods">
89
+ ...
90
+ </select>
83
91
  </div>
84
92
  ```
93
+ 🚀 [Live demo | Alpine.js x-bound: Shorthand Syntax](https://jsfiddle.net/rameel/9ys23n4z/)
85
94
 
86
95
  ### Binding Numeric Inputs
87
96
 
@@ -90,6 +99,7 @@ For `<input>` elements with `type="number"` and `type="range"`, values are autom
90
99
  ```html
91
100
  <input &value="number" type="number" />
92
101
  ```
102
+ 🚀 [Live demo | Alpine.js x-bound: Bind Numeric Inputs](https://jsfiddle.net/rameel/e160vsta/)
93
103
 
94
104
  ### Binding `<input type="file">`
95
105
 
@@ -98,46 +108,68 @@ For `<input>` elements with `type="file"`, the binding is applied to the `files`
98
108
  ```html
99
109
  <input &files type="file" accept="image/jpeg" />
100
110
  ```
111
+ 🚀 [Live demo | Alpine.js x-bound: Bind Files](https://jsfiddle.net/rameel/phy2zn0a/)
101
112
 
102
- > [!NOTE]
103
- > The `files` binding is one-way.
104
113
 
105
114
  ### Binding `<select>`
106
115
 
107
116
  To bind the value of a `<select>` element, use the `value` property:
108
117
  ```html
109
- <select &value="pet">
110
- <option value="cat">Cat</option>
111
- <option value="goldfish">Goldfish</option>
112
- <option value="parrot">Parrot</option>
113
- </select>
118
+ <div x-data="{ fruit: '' }">
119
+ <select &value="fruit">
120
+ <option value="" disabled>Select...</option>
121
+ <option>Apple</option>
122
+ <option>Banana</option>
123
+ <option>Orange</option>
124
+ <option>Grape</option>
125
+ <option>Mango</option>
126
+ </select>
127
+
128
+ <p>
129
+ Fruit: <span x-text="fruit"></span>
130
+ </p>
131
+ </div>
114
132
  ```
133
+ 🚀 [Live demo | Alpine.js x-bound: Binding select](https://jsfiddle.net/rameel/fs12bo5m/)
134
+
115
135
 
116
136
  For a `<select multiple>` element, the data property is an array containing the values of the selected options.
117
137
 
118
138
  ```html
119
139
  <div x-data="{ pets: ['goldfish', 'parrot'] }">
120
- <select &value="pets" multiple>
121
- <option value="cat">Cat</option>
122
- <option value="goldfish">Goldfish</option>
123
- <option value="parrot">Parrot</option>
124
- <option value="spider">Spider</option>
125
- </select>
126
-
127
- Pets: <span x-text="pets"></span>
140
+ <select &value="pets" multiple>
141
+ <option value="cat">Cat</option>
142
+ <option value="goldfish">Goldfish</option>
143
+ <option value="parrot">Parrot</option>
144
+ <option value="spider">Spider</option>
145
+ </select>
146
+
147
+ Pets: <span x-text="pets"></span>
128
148
  </div>
129
149
  ```
150
+ 🚀 [Live demo | Alpine.js x-bound: Multiple select](https://jsfiddle.net/rameel/kq0xseo1/)
151
+
130
152
 
131
153
  ### Binding `<details>`
132
154
 
133
155
  The directive also allows binding to the `open` property of `<details>` elements:
134
156
 
135
157
  ```html
136
- <details &open="isOpen">
158
+ <div x-data="{ open: true }">
159
+ <details &open>
137
160
  <summary>Details</summary>
138
161
  <p>Something small enough to escape casual notice.</p>
139
- </details>
162
+ </details>
163
+
164
+ <p>
165
+ <label>
166
+ <input &checked="open" type="checkbox" />
167
+ Open / Close
168
+ </label>
169
+ </p>
170
+ </div>
140
171
  ```
172
+ 🚀 [Live demo | Alpine.js x-bound: Binding details](https://jsfiddle.net/rameel/fw2bkLqv/)
141
173
 
142
174
  ### Binding `<img>` sizes
143
175
 
@@ -146,6 +178,7 @@ You can bind the `naturalWidth` and `naturalHeight` properties of an image after
146
178
  ```html
147
179
  <img src="..." &naturalWidth="width" &naturalHeight="height" />
148
180
  ```
181
+ 🚀 [Live demo | Alpine.js x-bound: Binding image sizes](https://jsfiddle.net/rameel/q4vb1d0w/)
149
182
 
150
183
  > [!TIP]
151
184
  > If you prefer using `kebab-case` for multi-word properties like `naturalWidth`, you can write it as `natural-width`. It will be automatically normalized internally:
@@ -164,6 +197,18 @@ You can bind the `naturalWidth` and `naturalHeight` properties of an image after
164
197
  > The `naturalWidth` and `naturalHeight` properties are read-only and reflect the original image dimensions, available after the image has loaded.
165
198
 
166
199
 
200
+ ### Binding `<video>` sizes
201
+
202
+ You can bind the `videoWidth` and `videoHeight` properties of a video after it loads:
203
+
204
+ ```html
205
+ <video &videoWidth="width" &videoHeight="height">
206
+ <source src="..." type="video/mp4">
207
+ </video>
208
+ ```
209
+ 🚀 [Live demo | Alpine.js x-bound: Binding video sizes](https://jsfiddle.net/rameel/nah2pfcx/)
210
+
211
+
167
212
  ### Binding `contenteditable` elements
168
213
 
169
214
  For `contenteditable` elements, you can bind the following properties:
@@ -172,12 +217,15 @@ For `contenteditable` elements, you can bind the following properties:
172
217
  - [textContent](https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent)
173
218
 
174
219
  ```html
175
- <div &inner-html="html" contenteditable="true"></div>
220
+ <div &innerHtml="html" contenteditable="true"></div>
176
221
  ```
222
+ 🚀 [Live demo | Alpine.js x-bound: Contenteditable bindings](https://jsfiddle.net/rameel/n5sj0rdz/)
223
+
177
224
 
178
225
  ### Binding block-level element sizes
179
226
 
180
- You can bind to the following properties to get the **width** and **height** of block-level elements. The values will update whenever the element's size changes:
227
+ You can bind to the following properties to get the **width** and **height** of block-level elements,
228
+ measured with a `ResizeObserver`. The values will update whenever the element's size changes:
181
229
 
182
230
  - [clientHeight](https://developer.mozilla.org/en-US/docs/Web/API/Element/clientHeight)
183
231
  - [clientWidth](https://developer.mozilla.org/en-US/docs/Web/API/Element/clientWidth)
@@ -187,36 +235,58 @@ You can bind to the following properties to get the **width** and **height** of
187
235
  ```html
188
236
  <div &client-width="width" &client-height="height"></div>
189
237
  ```
238
+ 🚀 [Live demo | Alpine.js x-bound: Binding element dimensions](https://jsfiddle.net/rameel/jc4eu921/)
190
239
 
191
240
  > [!NOTE]
192
241
  > These properties are read-only.
193
242
 
243
+ > [!IMPORTANT]
244
+ > Elements with `display: inline` don't have an explicit width or height (unless they are intrinsically sized, like `<img>` or `<canvas>`). Therefore, a `ResizeObserver` cannot track their size. If you need to observe their size, change their `display` style to something like `inline-block`.
245
+ >
246
+ > Also keep in mind that CSS transforms do not trigger `ResizeObserver` updates.
247
+
248
+
194
249
  ### Binding group of `<input type="radio">` and `<input type="checkbox">`
195
250
  The group of `<input>` elements that should function together can utilize the `group` bound property.
196
251
 
197
252
  ```html
198
253
  <div x-data="{ pets: ['goldfish', 'parrot'], contact: 'Email' }">
199
254
 
200
- <!-- grouped checkboxes are similar to "select multiple"
201
- and use an array for selected options -->
202
- <input &group="pets" type="checkbox" value="cat" />
203
- <input &group="pets" type="checkbox" value="goldfish" />
204
- <input &group="pets" type="checkbox" value="parrot" />
205
- <input &group="pets" type="checkbox" value="spider" />
255
+ <!-- grouped checkboxes are similar to "select multiple"
256
+ and use an array for selected options -->
257
+ <input &group="pets" type="checkbox" value="cat" />
258
+ <input &group="pets" type="checkbox" value="goldfish" />
259
+ <input &group="pets" type="checkbox" value="parrot" />
260
+ <input &group="pets" type="checkbox" value="spider" />
206
261
 
207
- <!-- grouped radio inputs are mutually exclusive -->
208
- <input &group="contact" type="radio" value="Email" />
209
- <input &group="contact" type="radio" value="Phone" />
210
- <input &group="contact" type="radio" value="Mail" />
262
+ <!-- grouped radio inputs are mutually exclusive -->
263
+ <input &group="contact" type="radio" value="Email" />
264
+ <input &group="contact" type="radio" value="Phone" />
265
+ <input &group="contact" type="radio" value="Mail" />
211
266
 
212
267
  </div>
213
268
  ```
269
+ 🚀 [Live demo | Alpine.js x-bound: Binding element dimensions](https://jsfiddle.net/rameel/f5jpry7b/)
270
+
214
271
 
215
272
  ### Binding `input[type="checkbox"]:indeterminate` property
216
273
  The `x-bound` directive supports binding the `indeterminate` property of `<input type="checkbox">` elements,
217
274
  allowing you to control the checkbox's indeterminate state (a state where the checkbox is neither checked nor unchecked,
218
275
  typically represented visually with a dash or similar indicator).
219
276
 
277
+ ```html
278
+ <div x-data="{ checked: false, indeterminate: true }">
279
+ <input type="checkbox" &checked &indeterminate />
280
+
281
+ <template x-match>
282
+ <span x-case="indeterminate">Waiting...</span>
283
+ <span x-case="checked">Checked</span>
284
+ <span x-default>Unchecked</span>
285
+ </template>
286
+ </div>
287
+ ```
288
+ 🚀 [Live demo | Alpine.js x-bound: Binding indeterminate](https://jsfiddle.net/rameel/o8ubzac0/)
289
+
220
290
  This is useful for scenarios like selecting a subset of items in a list, such as in a table header checkbox:
221
291
  ```html
222
292
  <table>
@@ -233,6 +303,8 @@ This is useful for scenarios like selecting a subset of items in a list, such as
233
303
  ...
234
304
  </table>
235
305
  ```
306
+ 🚀 [Live demo | Alpine.js x-bound: Binding indeterminate (table)](https://jsfiddle.net/rameel/ryvhw3jt/)
307
+
236
308
  In this example, the `indeterminate` property of the checkbox is bound to the `isPartialSelected` data property.
237
309
  When `isPartialSelected` is `true`, the checkbox will be in the indeterminate state.
238
310
 
@@ -252,6 +324,7 @@ The directive also supports synchronizing values between two data properties.
252
324
  Number: <span x-text="number"></span>
253
325
  </div>
254
326
  ```
327
+ 🚀 [Live demo | Alpine.js x-bound: Binding data properties](https://jsfiddle.net/rameel/972qyomn/)
255
328
 
256
329
  In this example, we bind the outer `number` property to the inner `count` property. Since `number` is initially set to `5`, the `count` property is also set to `5` when the binding occurs.
257
330
 
@@ -280,6 +353,46 @@ You can find the source code for this plugin on GitHub:
280
353
 
281
354
  https://github.com/rameel/ramstack.alpinegear.js/tree/main/src/plugins/bound
282
355
 
356
+ ## Related projects
357
+
358
+ **[@ramstack/alpinegear-main](https://www.npmjs.com/package/@ramstack/alpinegear-main)** ([README](https://github.com/rameel/ramstack.alpinegear.js/tree/main/src/plugins/main))<br>
359
+ Provides a combined plugin that includes several useful directives.
360
+ This package aggregates multiple individual plugins, offering a convenient all-in-one bundle.
361
+ Included directives: `x-bound`, `x-format`, `x-fragment`, `x-match`, `x-template`, and `x-when`.
362
+
363
+ **[@ramstack/alpinegear-format](https://www.npmjs.com/package/@ramstack/alpinegear-format)** ([README](https://github.com/rameel/ramstack.alpinegear.js/tree/main/src/plugins/format))<br>
364
+ Provides the `x-format` directive, which allows you to easily interpolate text using a template syntax similar to what's available in `Vue.js`.
365
+
366
+ **[@ramstack/alpinegear-template](https://www.npmjs.com/package/@ramstack/alpinegear-template)** ([README](https://github.com/rameel/ramstack.alpinegear.js/tree/main/src/plugins/template))<br>
367
+ Provides the `x-template` directive, which allows you to define a template once anywhere in the DOM and reference it by its ID.
368
+
369
+ **[@ramstack/alpinegear-fragment](https://www.npmjs.com/package/@ramstack/alpinegear-fragment)** ([README](https://github.com/rameel/ramstack.alpinegear.js/tree/main/src/plugins/fragment))<br>
370
+ Provides the `x-fragment` directive, which allows for fragment-like behavior similar to what's available in frameworks
371
+ like `Vue.js` or `React`, where multiple root elements can be grouped together.
372
+
373
+ **[@ramstack/alpinegear-match](https://www.npmjs.com/package/@ramstack/alpinegear-match)** ([README](https://github.com/rameel/ramstack.alpinegear.js/tree/main/src/plugins/match))<br>
374
+ Provides the `x-match` directive, which functions similarly to the `switch` statement in many programming languages,
375
+ allowing you to conditionally render elements based on matching cases.
376
+
377
+ **[@ramstack/alpinegear-when](https://www.npmjs.com/package/@ramstack/alpinegear-when)** ([README](https://github.com/rameel/ramstack.alpinegear.js/tree/main/src/plugins/when))<br>
378
+ Provides the `x-when` directive, which allows for conditional rendering of elements similar to `x-if`, but supports multiple root elements.
379
+
380
+ **[@ramstack/alpinegear-destroy](https://www.npmjs.com/package/@ramstack/alpinegear-destroy)** ([README](https://github.com/rameel/ramstack.alpinegear.js/tree/main/src/plugins/destroy))<br>
381
+ Provides the `x-destroy` directive, which is the opposite of `x-init` and allows you to hook into the cleanup phase
382
+ of any element, running a callback when the element is removed from the DOM.
383
+
384
+ **[@ramstack/alpinegear-hotkey](https://www.npmjs.com/package/@ramstack/alpinegear-hotkey)** ([README](https://github.com/rameel/ramstack.alpinegear.js/tree/main/src/plugins/hotkey))<br>
385
+ Provides the `x-hotkey` directive, which allows you to easily handle keyboard shortcuts within your Alpine.js components or application.
386
+
387
+ **[@ramstack/alpinegear-router](https://www.npmjs.com/package/@ramstack/alpinegear-router)** ([README](https://github.com/rameel/ramstack.alpinegear.js/tree/main/src/plugins/router))<br>
388
+ Provides the `x-router` and `x-route` directives, which enable client-side navigation and routing functionality within your Alpine.js application.
389
+
390
+ **[@ramstack/alpinegear-dialog](https://www.npmjs.com/package/@ramstack/alpinegear-dialog)** ([README](https://github.com/rameel/ramstack.alpinegear.js/tree/main/src/plugins/dialog))<br>
391
+ Provides a headless dialog directive for Alpine.js based on the native HTML `<dialog>` element.
392
+ It supports declarative composition, value-based close semantics, and both modal and non-modal dialogs,
393
+ with optional Promise-based imperative control.
394
+
395
+
283
396
  ## Contributions
284
397
  Bug reports and contributions are welcome.
285
398
 
@@ -122,7 +122,7 @@ function watch(get_value, callback, options = null) {
122
122
  }
123
123
 
124
124
  if (initialized || (options?.immediate ?? true)) {
125
-
125
+ // Prevent the watcher from detecting its own dependencies.
126
126
  setTimeout(() => {
127
127
  callback(new_value, old_value);
128
128
  old_value = new_value;
@@ -146,10 +146,10 @@ const canonical_names = create_map(
146
146
  "group");
147
147
 
148
148
  function plugin({ directive, entangle, evaluateLater, mapAttributes, mutateDom, prefixed }) {
149
-
150
-
151
-
152
-
149
+ // creating a shortcut for the directive,
150
+ // when an attribute name starting with & will refer to our directive,
151
+ // allowing us to write like this: &value="prop",
152
+ // which is equivalent to x-bound:value="prop"
153
153
  mapAttributes(attr => ({
154
154
  name: attr.name.replace(/^&/, prefixed("bound:")),
155
155
  value: attr.value
@@ -165,8 +165,8 @@ function plugin({ directive, entangle, evaluateLater, mapAttributes, mutateDom,
165
165
 
166
166
  expression = expression?.trim();
167
167
 
168
-
169
-
168
+ // since attributes come in a lowercase,
169
+ // we need to convert the bound property name to its canonical form
170
170
  const property_name = canonical_names.get(value.trim().replace("-", "").toLowerCase());
171
171
 
172
172
  // if the expression is omitted, then we assume it corresponds
@@ -223,7 +223,7 @@ function plugin({ directive, entangle, evaluateLater, mapAttributes, mutateDom,
223
223
  break;
224
224
 
225
225
  case "open":
226
- process_details();
226
+ process_open_attribute();
227
227
  break;
228
228
 
229
229
  case "group":
@@ -277,8 +277,8 @@ function plugin({ directive, entangle, evaluateLater, mapAttributes, mutateDom,
277
277
  switch (tag_name) {
278
278
  case "INPUT":
279
279
  case "TEXTAREA":
280
-
281
-
280
+ // if the value of the bound property is "null" or "undefined",
281
+ // we initialize it with the value from the element.
282
282
  is_nullish(get_value()) && update_variable();
283
283
 
284
284
  effect(update_property);
@@ -288,16 +288,16 @@ function plugin({ directive, entangle, evaluateLater, mapAttributes, mutateDom,
288
288
  break;
289
289
 
290
290
  case "SELECT":
291
-
292
-
293
-
294
-
295
-
296
-
297
-
291
+ // WORKAROUND:
292
+ // For the "select" element, there might be a situation
293
+ // where options are generated dynamically using the "x-for" directive,
294
+ // and in this case, attempting to set the "value" property
295
+ // will have no effect since there are no options yet.
296
+ // Therefore, we use a small trick to set the value a bit later
297
+ // when the "x-for" directive has finished its work.
298
298
  setTimeout(() => {
299
-
300
-
299
+ // if the value of the bound property is "null" or "undefined",
300
+ // we initialize it with the value from the element.
301
301
  is_nullish(get_value()) && update_variable();
302
302
 
303
303
  effect(() => apply_select_values(el, as_array(get_value() ?? [])));
@@ -359,13 +359,29 @@ function plugin({ directive, entangle, evaluateLater, mapAttributes, mutateDom,
359
359
  processed = true;
360
360
  }
361
361
 
362
- function process_details() {
363
- if (tag_name === "DETAILS") {
364
-
365
-
366
- is_nullish(get_value()) && update_variable();
367
-
368
- effect(update_property);
362
+ function process_open_attribute() {
363
+ const [is_details, is_dialog] = [tag_name === "DETAILS", tag_name === "DIALOG"];
364
+
365
+ if (is_details || is_dialog) {
366
+ //
367
+ // <details>:
368
+ // Supports safe two-way binding via the "open" attribute,
369
+ // so we initialize from the element only if the bound value
370
+ // is null or undefined.
371
+ //
372
+ // <dialog>:
373
+ // Directly setting element.open is discouraged by the spec,
374
+ // as it breaks native dialog behavior and the "close" event.
375
+ // Therefore, we always initialize state from the element
376
+ // and treat it as a one-way source of truth.
377
+ // https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/open#value
378
+ //
379
+ (is_dialog || is_nullish(get_value())) && update_variable();
380
+
381
+ //
382
+ // Enable two-way binding only for "<details>"
383
+ //
384
+ is_details && effect(update_property);
369
385
  cleanup(listen(el, "toggle", update_variable));
370
386
  processed = true;
371
387
  }
@@ -428,4 +444,4 @@ function collect_group_values(el, values) {
428
444
  return values;
429
445
  }
430
446
 
431
- export { plugin as bound };
447
+ export { plugin as bound };
@@ -1 +1 @@
1
- function e(e,...t){const n=e(...t);return()=>{let e;return n(t=>e=t),t=e,"function"==typeof t?.get?e.get():e;var t}}function t(e,...t){const n=e(...t);t[t.length-1]=`${t.at(-1)} = __val`;const i=e(...t);return e=>{let t;n(e=>t=e),function(e){return"function"==typeof e?.set}(t)?t.set(e):i(()=>{},{scope:{__val:e}})}}const n=Symbol();let i;const a=(...e)=>console.warn("alpinegear.js:",...e),r=Array.isArray,o=e=>null==e,c=e=>"checkbox"===e.type||"radio"===e.type,s=e=>r(e)?e:[e],u=(e,t)=>e==t,l=(e,t)=>e.findIndex(e=>e==t),d=(e,t)=>e.includes(t),f=(e,t,n,i)=>(e.addEventListener(t,n,i),()=>e.removeEventListener(t,n,i)),p=e=>"object"==typeof e?JSON.parse(JSON.stringify(e)):e;function h(e,t,n=null){const{effect:i,release:a}=Alpine;let r,o,c=!1;const s=i(()=>{r=e(),c||(n?.deep&&JSON.stringify(r),o=r),(c||(n?.immediate??1))&&setTimeout(()=>{t(r,o),o=r},0),c=!0});return()=>a(s)}const v=new Map("value,checked,files,innerHTML,innerText,textContent,videoHeight,videoWidth,naturalHeight,naturalWidth,clientHeight,clientWidth,offsetHeight,offsetWidth,indeterminate,open,group".split(",").map(e=>[e.trim().toLowerCase(),e.trim()]));function b({directive:b,entangle:g,evaluateLater:m,mapAttributes:k,mutateDom:x,prefixed:y}){k(e=>({name:e.name.replace(/^&/,y("bound:")),value:e.value})),b("bound",(b,{expression:k,value:y,modifiers:T},{effect:w,cleanup:E})=>{if(!y)return void a("x-bound directive expects the presence of a bound property name");const H=b.tagName.toUpperCase();k=k?.trim();const L=v.get(y.trim().replace("-","").toLowerCase());k||=L;const _=e(m,b,k),S=t(m,b,k),W=()=>u(b[L],_())||x(()=>b[L]=_()),A=()=>S((e=>"number"===e.type||"range"===e.type)(b)?function(e){return""===e?null:+e}(b[L]):b[L]);let C;switch(L){case"value":!function(){switch(H){case"INPUT":case"TEXTAREA":o(_())&&A(),w(W),E(f(b,"input",A)),C=!0;break;case"SELECT":setTimeout(()=>{o(_())&&A(),w(()=>function(e,t){for(const n of e.options)n.selected=l(t,n.value)>=0}(b,s(_()??[]))),E(f(b,"change",()=>S(function(e){return e.multiple?[...e.selectedOptions].map(e=>e.value):e.value}(b))))},0),C=!0}}();break;case"checked":c(b)&&(w(W),E(f(b,"change",A)),C=!0);break;case"files":"file"===b.type&&(_()instanceof FileList||A(),w(W),E(f(b,"input",A)),C=!0);break;case"innerHTML":case"innerText":case"textContent":"true"===b.contentEditable&&(o(_())&&A(),w(W),E(f(b,"input",A)),C=!0);break;case"videoHeight":case"videoWidth":N("VIDEO","resize");break;case"naturalHeight":case"naturalWidth":N("IMG","load");break;case"clientHeight":case"clientWidth":case"offsetHeight":case"offsetWidth":E(function(e,t){return i??=new ResizeObserver(e=>{for(const t of e)for(const e of t.target[n]?.values()??[])e(t)}),e[n]??=new Set,e[n].add(t),i.observe(e),()=>{e[n].delete(t),e[n].size||(i.unobserve(e),e[n]=null)}}(b,A)),C=!0;break;case"indeterminate":"checkbox"===b.type&&(o(_())&&A(),w(W),E(f(b,"change",A)),C=!0);break;case"open":"DETAILS"===H&&(o(_())&&A(),w(W),E(f(b,"toggle",A)),C=!0);break;case"group":c(b)&&(b.name||x(()=>b.name=k),w(()=>x(()=>function(e,t){e.checked=r(t)?l(t,e.value)>=0:u(e.value,t)}(b,_()??[]))),E(f(b,"input",()=>S(function(e,t){if("radio"===e.type)return e.value;t=s(t);const n=l(t,e.value);return e.checked?n>=0||t.push(e.value):n>=0&&t.splice(n,1),t}(b,_())))),C=!0)}if(!C){const n=d(T,"in")?"in":d(T,"out")?"out":"inout",i=k===y?((e,t)=>{for(;e&&!t(e);)e=(e._x_teleportBack??e).parentElement;return e})(b.parentNode,e=>e._x_dataStack):b;if(!b._x_dataStack)return void a("x-bound directive requires the presence of the x-data directive to bind component properties");if(!i)return void a(`x-bound directive cannot find the parent scope where the '${y}' property is defined`);const r={get:e(m,i,k),set:t(m,i,k)},o={get:e(m,b,y),set:t(m,b,y)};switch(n){case"in":E(h(()=>r.get(),e=>o.set(p(e))));break;case"out":E(h(()=>o.get(),e=>r.set(p(e))));break;default:E(g(r,o))}}function N(e,t){H===e&&(A(),E(f(b,t,A)),C=!0)}})}export{b as bound};
1
+ function e(e,...t){const n=e(...t);return()=>{let e;return n(t=>e=t),t=e,"function"==typeof t?.get?e.get():e;var t}}function t(e,...t){const n=e(...t);t[t.length-1]=`${t.at(-1)} = __val`;const i=e(...t);return e=>{let t;n(e=>t=e),function(e){return"function"==typeof e?.set}(t)?t.set(e):i(()=>{},{scope:{__val:e}})}}const n=Symbol();let i;const a=(...e)=>console.warn("alpinegear.js:",...e),r=Array.isArray,o=e=>null==e,c=e=>"checkbox"===e.type||"radio"===e.type,s=e=>r(e)?e:[e],u=(e,t)=>e==t,l=(e,t)=>e.findIndex(e=>e==t),d=(e,t)=>e.includes(t),f=(e,t,n,i)=>(e.addEventListener(t,n,i),()=>e.removeEventListener(t,n,i)),p=e=>"object"==typeof e?JSON.parse(JSON.stringify(e)):e;function h(e,t,n=null){const{effect:i,release:a}=Alpine;let r,o,c=!1;const s=i(()=>{r=e(),c||(n?.deep&&JSON.stringify(r),o=r),(c||(n?.immediate??1))&&setTimeout(()=>{t(r,o),o=r},0),c=!0});return()=>a(s)}const v=new Map("value,checked,files,innerHTML,innerText,textContent,videoHeight,videoWidth,naturalHeight,naturalWidth,clientHeight,clientWidth,offsetHeight,offsetWidth,indeterminate,open,group".split(",").map(e=>[e.trim().toLowerCase(),e.trim()]));function b({directive:b,entangle:g,evaluateLater:m,mapAttributes:k,mutateDom:x,prefixed:y}){k(e=>({name:e.name.replace(/^&/,y("bound:")),value:e.value})),b("bound",(b,{expression:k,value:y,modifiers:L},{effect:T,cleanup:w})=>{if(!y)return void a("x-bound directive expects the presence of a bound property name");const E=b.tagName.toUpperCase();k=k?.trim();const H=v.get(y.trim().replace("-","").toLowerCase());k||=H;const _=e(m,b,k),S=t(m,b,k),A=()=>u(b[H],_())||x(()=>b[H]=_()),W=()=>S((e=>"number"===e.type||"range"===e.type)(b)?function(e){return""===e?null:+e}(b[H]):b[H]);let O;switch(H){case"value":!function(){switch(E){case"INPUT":case"TEXTAREA":o(_())&&W(),T(A),w(f(b,"input",W)),O=!0;break;case"SELECT":setTimeout(()=>{o(_())&&W(),T(()=>function(e,t){for(const n of e.options)n.selected=l(t,n.value)>=0}(b,s(_()??[]))),w(f(b,"change",()=>S(function(e){return e.multiple?[...e.selectedOptions].map(e=>e.value):e.value}(b))))},0),O=!0}}();break;case"checked":c(b)&&(T(A),w(f(b,"change",W)),O=!0);break;case"files":"file"===b.type&&(_()instanceof FileList||W(),T(A),w(f(b,"input",W)),O=!0);break;case"innerHTML":case"innerText":case"textContent":"true"===b.contentEditable&&(o(_())&&W(),T(A),w(f(b,"input",W)),O=!0);break;case"videoHeight":case"videoWidth":C("VIDEO","resize");break;case"naturalHeight":case"naturalWidth":C("IMG","load");break;case"clientHeight":case"clientWidth":case"offsetHeight":case"offsetWidth":w(function(e,t){return i??=new ResizeObserver(e=>{for(const t of e)for(const e of t.target[n]?.values()??[])e(t)}),e[n]??=new Set,e[n].add(t),i.observe(e),()=>{e[n].delete(t),e[n].size||(i.unobserve(e),e[n]=null)}}(b,W)),O=!0;break;case"indeterminate":"checkbox"===b.type&&(o(_())&&W(),T(A),w(f(b,"change",W)),O=!0);break;case"open":!function(){const[e,t]=["DETAILS"===E,"DIALOG"===E];(e||t)&&((t||o(_()))&&W(),e&&T(A),w(f(b,"toggle",W)),O=!0)}();break;case"group":c(b)&&(b.name||x(()=>b.name=k),T(()=>x(()=>function(e,t){e.checked=r(t)?l(t,e.value)>=0:u(e.value,t)}(b,_()??[]))),w(f(b,"input",()=>S(function(e,t){if("radio"===e.type)return e.value;t=s(t);const n=l(t,e.value);return e.checked?n>=0||t.push(e.value):n>=0&&t.splice(n,1),t}(b,_())))),O=!0)}if(!O){const n=d(L,"in")?"in":d(L,"out")?"out":"inout",i=k===y?((e,t)=>{for(;e&&!t(e);)e=(e._x_teleportBack??e).parentElement;return e})(b.parentNode,e=>e._x_dataStack):b;if(!b._x_dataStack)return void a("x-bound directive requires the presence of the x-data directive to bind component properties");if(!i)return void a(`x-bound directive cannot find the parent scope where the '${y}' property is defined`);const r={get:e(m,i,k),set:t(m,i,k)},o={get:e(m,b,y),set:t(m,b,y)};switch(n){case"in":w(h(()=>r.get(),e=>o.set(p(e))));break;case"out":w(h(()=>o.get(),e=>r.set(p(e))));break;default:w(g(r,o))}}function C(e,t){E===e&&(W(),w(f(b,t,W)),O=!0)}})}export{b as bound};
@@ -125,7 +125,7 @@
125
125
  }
126
126
 
127
127
  if (initialized || (options?.immediate ?? true)) {
128
-
128
+ // Prevent the watcher from detecting its own dependencies.
129
129
  setTimeout(() => {
130
130
  callback(new_value, old_value);
131
131
  old_value = new_value;
@@ -149,10 +149,10 @@
149
149
  "group");
150
150
 
151
151
  function plugin({ directive, entangle, evaluateLater, mapAttributes, mutateDom, prefixed }) {
152
-
153
-
154
-
155
-
152
+ // creating a shortcut for the directive,
153
+ // when an attribute name starting with & will refer to our directive,
154
+ // allowing us to write like this: &value="prop",
155
+ // which is equivalent to x-bound:value="prop"
156
156
  mapAttributes(attr => ({
157
157
  name: attr.name.replace(/^&/, prefixed("bound:")),
158
158
  value: attr.value
@@ -168,8 +168,8 @@
168
168
 
169
169
  expression = expression?.trim();
170
170
 
171
-
172
-
171
+ // since attributes come in a lowercase,
172
+ // we need to convert the bound property name to its canonical form
173
173
  const property_name = canonical_names.get(value.trim().replace("-", "").toLowerCase());
174
174
 
175
175
  // if the expression is omitted, then we assume it corresponds
@@ -226,7 +226,7 @@
226
226
  break;
227
227
 
228
228
  case "open":
229
- process_details();
229
+ process_open_attribute();
230
230
  break;
231
231
 
232
232
  case "group":
@@ -280,8 +280,8 @@
280
280
  switch (tag_name) {
281
281
  case "INPUT":
282
282
  case "TEXTAREA":
283
-
284
-
283
+ // if the value of the bound property is "null" or "undefined",
284
+ // we initialize it with the value from the element.
285
285
  is_nullish(get_value()) && update_variable();
286
286
 
287
287
  effect(update_property);
@@ -291,16 +291,16 @@
291
291
  break;
292
292
 
293
293
  case "SELECT":
294
-
295
-
296
-
297
-
298
-
299
-
300
-
294
+ // WORKAROUND:
295
+ // For the "select" element, there might be a situation
296
+ // where options are generated dynamically using the "x-for" directive,
297
+ // and in this case, attempting to set the "value" property
298
+ // will have no effect since there are no options yet.
299
+ // Therefore, we use a small trick to set the value a bit later
300
+ // when the "x-for" directive has finished its work.
301
301
  setTimeout(() => {
302
-
303
-
302
+ // if the value of the bound property is "null" or "undefined",
303
+ // we initialize it with the value from the element.
304
304
  is_nullish(get_value()) && update_variable();
305
305
 
306
306
  effect(() => apply_select_values(el, as_array(get_value() ?? [])));
@@ -362,13 +362,29 @@
362
362
  processed = true;
363
363
  }
364
364
 
365
- function process_details() {
366
- if (tag_name === "DETAILS") {
367
-
368
-
369
- is_nullish(get_value()) && update_variable();
370
-
371
- effect(update_property);
365
+ function process_open_attribute() {
366
+ const [is_details, is_dialog] = [tag_name === "DETAILS", tag_name === "DIALOG"];
367
+
368
+ if (is_details || is_dialog) {
369
+ //
370
+ // <details>:
371
+ // Supports safe two-way binding via the "open" attribute,
372
+ // so we initialize from the element only if the bound value
373
+ // is null or undefined.
374
+ //
375
+ // <dialog>:
376
+ // Directly setting element.open is discouraged by the spec,
377
+ // as it breaks native dialog behavior and the "close" event.
378
+ // Therefore, we always initialize state from the element
379
+ // and treat it as a one-way source of truth.
380
+ // https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/open#value
381
+ //
382
+ (is_dialog || is_nullish(get_value())) && update_variable();
383
+
384
+ //
385
+ // Enable two-way binding only for "<details>"
386
+ //
387
+ is_details && effect(update_property);
372
388
  cleanup(listen(el, "toggle", update_variable));
373
389
  processed = true;
374
390
  }
@@ -433,4 +449,4 @@
433
449
 
434
450
  document.addEventListener("alpine:init", () => { Alpine.plugin(plugin); });
435
451
 
436
- })();
452
+ })();
@@ -1 +1 @@
1
- !function(){"use strict";function e(e,...t){const n=e(...t);return()=>{let e;return n(t=>e=t),t=e,"function"==typeof t?.get?e.get():e;var t}}function t(e,...t){const n=e(...t);t[t.length-1]=`${t.at(-1)} = __val`;const i=e(...t);return e=>{let t;n(e=>t=e),function(e){return"function"==typeof e?.set}(t)?t.set(e):i(()=>{},{scope:{__val:e}})}}const n=Symbol();let i;const a=(...e)=>console.warn("alpinegear.js:",...e),r=Array.isArray,o=e=>null==e,c=e=>"checkbox"===e.type||"radio"===e.type,s=e=>r(e)?e:[e],u=(e,t)=>e==t,l=(e,t)=>e.findIndex(e=>e==t),d=(e,t)=>e.includes(t),p=(e,t,n,i)=>(e.addEventListener(t,n,i),()=>e.removeEventListener(t,n,i)),f=e=>"object"==typeof e?JSON.parse(JSON.stringify(e)):e;function v(e,t,n=null){const{effect:i,release:a}=Alpine;let r,o,c=!1;const s=i(()=>{r=e(),c||(n?.deep&&JSON.stringify(r),o=r),(c||(n?.immediate??1))&&setTimeout(()=>{t(r,o),o=r},0),c=!0});return()=>a(s)}const h=new Map("value,checked,files,innerHTML,innerText,textContent,videoHeight,videoWidth,naturalHeight,naturalWidth,clientHeight,clientWidth,offsetHeight,offsetWidth,indeterminate,open,group".split(",").map(e=>[e.trim().toLowerCase(),e.trim()]));function g({directive:g,entangle:b,evaluateLater:m,mapAttributes:k,mutateDom:x,prefixed:y}){k(e=>({name:e.name.replace(/^&/,y("bound:")),value:e.value})),g("bound",(g,{expression:k,value:y,modifiers:E},{effect:L,cleanup:T})=>{if(!y)return void a("x-bound directive expects the presence of a bound property name");const w=g.tagName.toUpperCase();k=k?.trim();const H=h.get(y.trim().replace("-","").toLowerCase());k||=H;const _=e(m,g,k),S=t(m,g,k),A=()=>u(g[H],_())||x(()=>g[H]=_()),W=()=>S((e=>"number"===e.type||"range"===e.type)(g)?function(e){return""===e?null:+e}(g[H]):g[H]);let C;switch(H){case"value":!function(){switch(w){case"INPUT":case"TEXTAREA":o(_())&&W(),L(A),T(p(g,"input",W)),C=!0;break;case"SELECT":setTimeout(()=>{o(_())&&W(),L(()=>function(e,t){for(const n of e.options)n.selected=l(t,n.value)>=0}(g,s(_()??[]))),T(p(g,"change",()=>S(function(e){return e.multiple?[...e.selectedOptions].map(e=>e.value):e.value}(g))))},0),C=!0}}();break;case"checked":c(g)&&(L(A),T(p(g,"change",W)),C=!0);break;case"files":"file"===g.type&&(_()instanceof FileList||W(),L(A),T(p(g,"input",W)),C=!0);break;case"innerHTML":case"innerText":case"textContent":"true"===g.contentEditable&&(o(_())&&W(),L(A),T(p(g,"input",W)),C=!0);break;case"videoHeight":case"videoWidth":N("VIDEO","resize");break;case"naturalHeight":case"naturalWidth":N("IMG","load");break;case"clientHeight":case"clientWidth":case"offsetHeight":case"offsetWidth":T(function(e,t){return i??=new ResizeObserver(e=>{for(const t of e)for(const e of t.target[n]?.values()??[])e(t)}),e[n]??=new Set,e[n].add(t),i.observe(e),()=>{e[n].delete(t),e[n].size||(i.unobserve(e),e[n]=null)}}(g,W)),C=!0;break;case"indeterminate":"checkbox"===g.type&&(o(_())&&W(),L(A),T(p(g,"change",W)),C=!0);break;case"open":"DETAILS"===w&&(o(_())&&W(),L(A),T(p(g,"toggle",W)),C=!0);break;case"group":c(g)&&(g.name||x(()=>g.name=k),L(()=>x(()=>function(e,t){e.checked=r(t)?l(t,e.value)>=0:u(e.value,t)}(g,_()??[]))),T(p(g,"input",()=>S(function(e,t){if("radio"===e.type)return e.value;t=s(t);const n=l(t,e.value);return e.checked?n>=0||t.push(e.value):n>=0&&t.splice(n,1),t}(g,_())))),C=!0)}if(!C){const n=d(E,"in")?"in":d(E,"out")?"out":"inout",i=k===y?((e,t)=>{for(;e&&!t(e);)e=(e._x_teleportBack??e).parentElement;return e})(g.parentNode,e=>e._x_dataStack):g;if(!g._x_dataStack)return void a("x-bound directive requires the presence of the x-data directive to bind component properties");if(!i)return void a(`x-bound directive cannot find the parent scope where the '${y}' property is defined`);const r={get:e(m,i,k),set:t(m,i,k)},o={get:e(m,g,y),set:t(m,g,y)};switch(n){case"in":T(v(()=>r.get(),e=>o.set(f(e))));break;case"out":T(v(()=>o.get(),e=>r.set(f(e))));break;default:T(b(r,o))}}function N(e,t){w===e&&(W(),T(p(g,t,W)),C=!0)}})}document.addEventListener("alpine:init",()=>{Alpine.plugin(g)})}();
1
+ !function(){"use strict";function e(e,...t){const n=e(...t);return()=>{let e;return n(t=>e=t),t=e,"function"==typeof t?.get?e.get():e;var t}}function t(e,...t){const n=e(...t);t[t.length-1]=`${t.at(-1)} = __val`;const i=e(...t);return e=>{let t;n(e=>t=e),function(e){return"function"==typeof e?.set}(t)?t.set(e):i(()=>{},{scope:{__val:e}})}}const n=Symbol();let i;const a=(...e)=>console.warn("alpinegear.js:",...e),r=Array.isArray,o=e=>null==e,c=e=>"checkbox"===e.type||"radio"===e.type,s=e=>r(e)?e:[e],u=(e,t)=>e==t,l=(e,t)=>e.findIndex(e=>e==t),d=(e,t)=>e.includes(t),f=(e,t,n,i)=>(e.addEventListener(t,n,i),()=>e.removeEventListener(t,n,i)),p=e=>"object"==typeof e?JSON.parse(JSON.stringify(e)):e;function v(e,t,n=null){const{effect:i,release:a}=Alpine;let r,o,c=!1;const s=i(()=>{r=e(),c||(n?.deep&&JSON.stringify(r),o=r),(c||(n?.immediate??1))&&setTimeout(()=>{t(r,o),o=r},0),c=!0});return()=>a(s)}const h=new Map("value,checked,files,innerHTML,innerText,textContent,videoHeight,videoWidth,naturalHeight,naturalWidth,clientHeight,clientWidth,offsetHeight,offsetWidth,indeterminate,open,group".split(",").map(e=>[e.trim().toLowerCase(),e.trim()]));function g({directive:g,entangle:b,evaluateLater:m,mapAttributes:k,mutateDom:x,prefixed:y}){k(e=>({name:e.name.replace(/^&/,y("bound:")),value:e.value})),g("bound",(g,{expression:k,value:y,modifiers:L},{effect:E,cleanup:T})=>{if(!y)return void a("x-bound directive expects the presence of a bound property name");const w=g.tagName.toUpperCase();k=k?.trim();const H=h.get(y.trim().replace("-","").toLowerCase());k||=H;const _=e(m,g,k),A=t(m,g,k),S=()=>u(g[H],_())||x(()=>g[H]=_()),W=()=>A((e=>"number"===e.type||"range"===e.type)(g)?function(e){return""===e?null:+e}(g[H]):g[H]);let O;switch(H){case"value":!function(){switch(w){case"INPUT":case"TEXTAREA":o(_())&&W(),E(S),T(f(g,"input",W)),O=!0;break;case"SELECT":setTimeout(()=>{o(_())&&W(),E(()=>function(e,t){for(const n of e.options)n.selected=l(t,n.value)>=0}(g,s(_()??[]))),T(f(g,"change",()=>A(function(e){return e.multiple?[...e.selectedOptions].map(e=>e.value):e.value}(g))))},0),O=!0}}();break;case"checked":c(g)&&(E(S),T(f(g,"change",W)),O=!0);break;case"files":"file"===g.type&&(_()instanceof FileList||W(),E(S),T(f(g,"input",W)),O=!0);break;case"innerHTML":case"innerText":case"textContent":"true"===g.contentEditable&&(o(_())&&W(),E(S),T(f(g,"input",W)),O=!0);break;case"videoHeight":case"videoWidth":C("VIDEO","resize");break;case"naturalHeight":case"naturalWidth":C("IMG","load");break;case"clientHeight":case"clientWidth":case"offsetHeight":case"offsetWidth":T(function(e,t){return i??=new ResizeObserver(e=>{for(const t of e)for(const e of t.target[n]?.values()??[])e(t)}),e[n]??=new Set,e[n].add(t),i.observe(e),()=>{e[n].delete(t),e[n].size||(i.unobserve(e),e[n]=null)}}(g,W)),O=!0;break;case"indeterminate":"checkbox"===g.type&&(o(_())&&W(),E(S),T(f(g,"change",W)),O=!0);break;case"open":!function(){const[e,t]=["DETAILS"===w,"DIALOG"===w];(e||t)&&((t||o(_()))&&W(),e&&E(S),T(f(g,"toggle",W)),O=!0)}();break;case"group":c(g)&&(g.name||x(()=>g.name=k),E(()=>x(()=>function(e,t){e.checked=r(t)?l(t,e.value)>=0:u(e.value,t)}(g,_()??[]))),T(f(g,"input",()=>A(function(e,t){if("radio"===e.type)return e.value;t=s(t);const n=l(t,e.value);return e.checked?n>=0||t.push(e.value):n>=0&&t.splice(n,1),t}(g,_())))),O=!0)}if(!O){const n=d(L,"in")?"in":d(L,"out")?"out":"inout",i=k===y?((e,t)=>{for(;e&&!t(e);)e=(e._x_teleportBack??e).parentElement;return e})(g.parentNode,e=>e._x_dataStack):g;if(!g._x_dataStack)return void a("x-bound directive requires the presence of the x-data directive to bind component properties");if(!i)return void a(`x-bound directive cannot find the parent scope where the '${y}' property is defined`);const r={get:e(m,i,k),set:t(m,i,k)},o={get:e(m,g,y),set:t(m,g,y)};switch(n){case"in":T(v(()=>r.get(),e=>o.set(p(e))));break;case"out":T(v(()=>o.get(),e=>r.set(p(e))));break;default:T(b(r,o))}}function C(e,t){w===e&&(W(),T(f(g,t,W)),O=!0)}})}document.addEventListener("alpine:init",()=>{Alpine.plugin(g)})}();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ramstack/alpinegear-bound",
3
- "version": "1.2.3",
3
+ "version": "1.3.0",
4
4
  "description": "@ramstack/alpinegear-bound provides the 'x-bound' Alpine.js directive, which allows for two-way binding of input elements and their associated data properties.",
5
5
  "author": "Rameel Burhan",
6
6
  "license": "MIT",
@@ -11,7 +11,10 @@
11
11
  },
12
12
  "keywords": [
13
13
  "alpine.js",
14
- "alpinejs"
14
+ "alpinejs",
15
+ "alpinejs-binding",
16
+ "alpinejs-directive",
17
+ "alpinejs-plugin"
15
18
  ],
16
19
  "main": "alpinegear-bound.js",
17
20
  "module": "alpinegear-bound.esm.js"