@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 +150 -37
- package/alpinegear-bound.esm.js +43 -27
- package/alpinegear-bound.esm.min.js +1 -1
- package/alpinegear-bound.js +43 -27
- package/alpinegear-bound.min.js +1 -1
- package/package.json +5 -2
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
|
|
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
|
-
|
|
48
|
-
|
|
51
|
+
<input x-bound:value="name" />
|
|
52
|
+
Hello <span x-text="name"></span>!
|
|
49
53
|
|
|
50
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
<
|
|
110
|
-
<
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
</
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
<
|
|
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 &
|
|
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
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
|
package/alpinegear-bound.esm.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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:
|
|
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};
|
package/alpinegear-bound.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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
|
+
})();
|
package/alpinegear-bound.min.js
CHANGED
|
@@ -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),
|
|
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.
|
|
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"
|