Browse Source

Make x-model="$model" work by default and tweak docs

Caleb Porzio 1 năm trước cách đây
mục cha
commit
fa9b39a753

+ 14 - 4
packages/alpinejs/src/directives/x-model.js

@@ -25,18 +25,28 @@ directive('model', (el, { modifiers, expression }, { effect, cleanup }) => {
         evaluateSet = () => {}
         evaluateSet = () => {}
     }
     }
 
 
-    let getValue = () => {
+    let getResult = () => {
         let result
         let result
 
 
         evaluateGet(value => result = value)
         evaluateGet(value => result = value)
 
 
+        // The following code prevents an infinite loop when using:
+        // x-model="$model" by retreiving an x-model higher in the tree...
+        if (result._x_modelAccessor) {
+            return result._x_modelAccessor.closest
+        }
+
+        return result
+    }
+
+    let getValue = () => {
+        let result = getResult()
+
         return isGetterSetter(result) ? result.get() : result
         return isGetterSetter(result) ? result.get() : result
     }
     }
 
 
     let setValue = value => {
     let setValue = value => {
-        let result
-
-        evaluateGet(value => result = value)
+        let result = getResult()
 
 
         if (isGetterSetter(result)) {
         if (isGetterSetter(result)) {
             result.set(value)
             result.set(value)

+ 18 - 8
packages/alpinejs/src/magics/$model.js

@@ -4,12 +4,18 @@ import { magic } from '../magics'
 import { reactive } from '../reactivity'
 import { reactive } from '../reactivity'
 
 
 magic('model', (el, { cleanup }) => {
 magic('model', (el, { cleanup }) => {
-    let func = generateModelAccessor(el.parentElement, cleanup)
+    let func = generateModelAccessor(el, cleanup)
 
 
-    Object.defineProperty(func, 'self', { get() {
-        return accessor(generateModelAccessor(el, cleanup))
+    Object.defineProperty(func, 'closest', { get() {
+        let func = generateModelAccessor(el.parentElement, cleanup)
+
+        func._x_modelAccessor = true
+
+        return accessor(func)
     }, })
     }, })
 
 
+    func._x_modelAccessor = true
+
     return accessor(func)
     return accessor(func)
 })
 })
 
 
@@ -27,19 +33,23 @@ function generateModelAccessor(el, cleanup) {
         return fallbackStateInitialValue
         return fallbackStateInitialValue
     }
     }
 
 
-    accessor.exists = () => {
-        return !! closestModelEl
+    let model = () => {
+        if (! closestModelEl) {
+            throw 'Cannot find an available x-model directive to reference from $model.'
+        }
+
+        return closestModelEl._x_model
     }
     }
 
 
     accessor.get = () => {
     accessor.get = () => {
-        return closestModelEl._x_model.get()
+        return model().get()
     }
     }
 
 
     accessor.set = (value) => {
     accessor.set = (value) => {
         if (typeof value === 'function') {
         if (typeof value === 'function') {
-            closestModelEl._x_model.set(value(accessor.get()))
+            model().set(value(accessor.get()))
         } else {
         } else {
-            closestModelEl._x_model.set(value)
+            model().set(value)
         }
         }
     }
     }
 
 

+ 30 - 61
packages/docs/src/en/magics/model.md

@@ -8,7 +8,11 @@ title: model
 
 
 `$model` is a magic property that can be used to interact with the closest `x-model` binding programmatically.
 `$model` is a magic property that can be used to interact with the closest `x-model` binding programmatically.
 
 
-Here's a simple example of using `$model` to access and control the `count` property bound using `x-model`.
+Typically this feature would be used in conjunction with a backend templating framework like Blade in Laravel. It's useful for abstracting away Alpine components into backend templates and exposing control to the outside scope through `x-model`.
+
+## Using getters and setters
+
+Here's a simple example of using `$model` to access and control the `count` property using `$model.get()` and `$model.set(...)`:
 
 
 ```alpine
 ```alpine
 <div x-data="{ count: 0 }">
 <div x-data="{ count: 0 }">
@@ -34,70 +38,15 @@ Here's a simple example of using `$model` to access and control the `count` prop
 
 
 As you can see, `$model.get()` and `$model.set()` can be used to programmatically control the `count` property bound using `x-model`.
 As you can see, `$model.get()` and `$model.set()` can be used to programmatically control the `count` property bound using `x-model`.
 
 
-Typically this feature would be used in conjunction with a backend templating framework like Blade in Laravel. It's useful for abstracting away Alpine components into backend templates and exposing control to the outside scope through `x-model`.
-
-## Using $model and x-model on the same element.
-
-It's important to note that by default, `$model` can only be used within children of `x-model`, not on the `x-model` element itself.
-
-For example, the following code won't work because `x-model` and `$model` are both used on the same element:
-
-```alpine
-<!-- The following code will throw an error... -->
-<div x-data="{ count: 0 }">
-    Count: <span x-model="count" x-text="$model.get()"></span>
-</div>
-```
-
-To remedy this, ensure that `x-model` is declared on a parent element of `$model` in the HTML tree:
-
-```alpine
-<div x-data="{ count: 0 }" x-model="count">
-    Count: <span x-text="$model.get()"></span>
-</div>
-```
-
-Alternatively, you can use the `.self` modifier to reference the `x-model` directive declared on the same element:
-
-```alpine
-<div x-data="{ count: 0 }">
-    Count: <span x-model="count" x-text="$model.self.get()"></span>
-</div>
-```
-
-## Setting values using a callback
+### Setting values using a callback
 
 
 If you prefer, `$model` offers an alternate syntax that allows you to pass a callback to `.set()` that receives the current value and returns the next value:
 If you prefer, `$model` offers an alternate syntax that allows you to pass a callback to `.set()` that receives the current value and returns the next value:
 
 
 ```alpine
 ```alpine
-<div x-model="count">
-    <button @click="$model.set(count => count + 1)">Increment</button>
-
-    Count: <span x-text="$model.get()"></span>
-</div>
+<button @click="$model.set(count => count + 1)">Increment</button>
 ```
 ```
 
 
-## Registering watchers
-
-Although Alpine provides other methods to watch reactive values for changes, `$model.watch()` exposes a convenient way to register a watcher for the `x-model` property directly:
-
-```alpine
-<div x-model="count">
-    <button @click="$model.set(count => count + 1)">Increment</button>
-
-    <div x-init="
-        $model.watch(count => {
-            console.log('The new count is: ' + count)
-        })
-    "></div>
-</div>
-```
-
-Now everytime `count` changes, the newest count value will be logged to the console.
-
-Watchers registered using `$watch` will be automatically destroyed when the element they are declared on is removed from the DOM.
-
-## Using $model within x-data
+## Binding to x-data properties
 
 
 Rather than manually controlling the `x-model` value using `$model.get()` and `$model.set()`, you can alternatively use `$model` as an entirely new value inside `x-data`.
 Rather than manually controlling the `x-model` value using `$model.get()` and `$model.set()`, you can alternatively use `$model` as an entirely new value inside `x-data`.
 
 
@@ -115,6 +64,8 @@ For example:
 
 
 This way you can freely use and modify the newly defined property `value` property within the nested component and `.get()` and `.set()` will be called internally.
 This way you can freely use and modify the newly defined property `value` property within the nested component and `.get()` and `.set()` will be called internally.
 
 
+> You may run into errors when using `$model` within `x-data` on the same element as the `x-model` you are trying to reference. This is because `x-data` is evaluated by Alpine before `x-model` is. In these cases, you must either ensure `x-model` is on a parent element of `x-data`, or you are deffering evaluation with `this.$nextTick` (or a similar strategy).
+
 ### Passing fallback state to $model
 ### Passing fallback state to $model
 
 
 In scenarios where you aren't sure if a parent `x-model` exists or you want to make `x-model` optional, you can pass initial state to `$model` as a function parameter.
 In scenarios where you aren't sure if a parent `x-model` exists or you want to make `x-model` optional, you can pass initial state to `$model` as a function parameter.
@@ -123,7 +74,7 @@ The following example will use the provided fallback value as the state if no `x
 
 
 ```alpine
 ```alpine
 <div>
 <div>
-    <div x-data="{ value: $model(1) }">
+    <div x-data="{ value: $model(0) }">
         <button @click="value = value + 1">Increment</button>
         <button @click="value = value + 1">Increment</button>
 
 
         Count: <span x-text="value"></span>
         Count: <span x-text="value"></span>
@@ -131,4 +82,22 @@ The following example will use the provided fallback value as the state if no `x
 </div>
 </div>
 ```
 ```
 
 
-In the above example you can see that there is no `x-model` defined in the parent HTML heirarchy. When `$model(1)` is called, it will recognize this and instead pass through the initial state as a reactive value.
+In the above example you can see that there is no `x-model` defined in the parent HTML heirarchy. When `$model(0)` is called, it will recognize this and instead pass through the initial state as a reactive value.
+
+## Registering watchers
+
+Although Alpine provides other methods to watch reactive values for changes, `$model.watch()` exposes a convenient way to register a watcher for the `x-model` property directly:
+
+```alpine
+<div x-model="count">
+    <div x-init="
+        $model.watch(count => {
+            console.log('The new count is: ' + count)
+        })
+    "></div>
+</div>
+```
+
+Now everytime `count` changes, the newest count value will be logged to the console.
+
+> Watchers registered using `$watch` will be automatically destroyed when the element they are declared on is removed from the DOM.

+ 4 - 66
tests/cypress/integration/magics/$model.spec.js

@@ -63,68 +63,6 @@ test('$model can be used with a getter and setter',
     }
     }
 )
 )
 
 
-test('$model can be used with optional internal state: with outer',
-    html`
-        <div x-data="{ foo: 'bar' }" x-model="foo">
-            <button @click="foo = 'baz'">click me</button>
-
-            <div x-data="{
-                internalValue: 'bob',
-                get value() {
-                    if (this.$model.exists()) return this.$model.get()
-
-                    return this.internalValue
-                },
-                set value(value) {
-                    if (this.$model.exists()) {
-                        this.$model.set(value)
-                    } else {
-                        this.internalValue = value
-                    }
-                }
-            }">
-                <h1 x-text="value"></h1>
-            </div>
-        </div>
-    `,
-    ({ get }) => {
-        get('h1').should(haveText('bar'))
-        get('button').click()
-        get('h1').should(haveText('baz'))
-    }
-)
-
-test('$model can be used with optional internal state: without outer',
-    html`
-        <div x-data>
-            <div x-data="{
-                internalValue: 'bar',
-                get value() {
-                    if (this.$model.exists()) return this.$model.get()
-
-                    return this.internalValue
-                },
-                set value(value) {
-                    if (this.$model.exists()) {
-                        this.$model.set(value)
-                    } else {
-                        this.internalValue = value
-                    }
-                }
-            }">
-                <button @click="value = 'baz'">click me</button>
-
-                <h1 x-text="value"></h1>
-            </div>
-        </div>
-    `,
-    ({ get }) => {
-        get('h1').should(haveText('bar'))
-        get('button').click()
-        get('h1').should(haveText('baz'))
-    }
-)
-
 test('$model can be used with another x-model',
 test('$model can be used with another x-model',
     html`
     html`
         <div x-data="{ foo: 'bar' }" x-model="foo">
         <div x-data="{ foo: 'bar' }" x-model="foo">
@@ -168,13 +106,13 @@ test('$model can be used on the same element as the corresponding x-model',
                     return {
                     return {
                         internalValue: 'bob',
                         internalValue: 'bob',
                         get value() {
                         get value() {
-                            if (this.$model.self) return this.$model.self.get()
+                            if (this.$model) return this.$model.get()
 
 
                             return this.internalValue
                             return this.internalValue
                         },
                         },
                         set value(value) {
                         set value(value) {
-                            if (this.$model.self) {
-                                this.$model.self.set(value)
+                            if (this.$model) {
+                                this.$model.set(value)
                             } else {
                             } else {
                                 this.internalValue = value
                                 this.internalValue = value
                             }
                             }
@@ -258,7 +196,7 @@ test('$model can be used as a getter/setter pair in x-data on the same element w
             Alpine.bind(el, {
             Alpine.bind(el, {
                 'x-data'() {
                 'x-data'() {
                     return {
                     return {
-                        value: this.$model.self
+                        value: this.$model
                     }
                     }
                 }
                 }
             })
             })