Kaynağa Gözat

Allow $model to be used inside x-data

Caleb Porzio 1 yıl önce
ebeveyn
işleme
3e765c9452

+ 11 - 0
packages/alpinejs/src/interceptor.js

@@ -13,6 +13,11 @@ export function initInterceptors(data) {
 
             if (typeof value === 'object' && value !== null && value._x_interceptor) {
                 obj[key] = value.initialize(data, path, key)
+            } else if ((typeof value === 'object' || typeof value === 'function') && value !== null && value._x_accessor) {
+                Object.defineProperty(obj, key, {
+                    get() { return value.get() },
+                    set(val) { return value.set(val) },
+                })
             } else {
                 if (isObject(value) && value !== obj && ! (value instanceof Element)) {
                     recurse(value, path)
@@ -24,6 +29,12 @@ export function initInterceptors(data) {
     return recurse(data)
 }
 
+export function accessor(obj) {
+    obj._x_accessor = true
+
+    return obj
+}
+
 export function interceptor(callback, mutateObj = () => {}) {
     let obj = {
         initialValue: undefined,

+ 11 - 5
packages/alpinejs/src/magics/$model.js

@@ -1,14 +1,16 @@
+import { accessor } from '../interceptor'
 import { findClosest } from '../lifecycle'
 import { magic } from '../magics'
+import { reactive } from '../reactivity'
 
 magic('model', (el, { cleanup }) => {
-    let accessor = generateModelAccessor(el.parentElement, cleanup)
+    let func = generateModelAccessor(el.parentElement, cleanup)
 
-    Object.defineProperty(accessor, 'self', { get() {
-        return generateModelAccessor(el, cleanup)
+    Object.defineProperty(func, 'self', { get() {
+        return accessor(generateModelAccessor(el, cleanup))
     }, })
 
-    return accessor
+    return accessor(func)
 })
 
 function generateModelAccessor(el, cleanup) {
@@ -19,7 +21,11 @@ function generateModelAccessor(el, cleanup) {
 
     // Instead of simply returning the get/set object, we'll create a wrapping function
     // so that we have the option to add additional APIs without breaking anything...
-    let accessor = function () {}
+    let accessor = function (fallbackStateInitialValue) {
+        if (closestModelEl) return this
+
+        return fallbackStateInitialValue
+    }
 
     accessor.exists = () => {
         return !! closestModelEl

+ 36 - 0
packages/docs/src/en/magics/model.md

@@ -96,3 +96,39 @@ Although Alpine provides other methods to watch reactive values for changes, `$m
 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
+
+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`.
+
+For example:
+
+```alpine
+<div x-model="count">
+    <div x-data="{ value: $model }">
+        <button @click="value = value + 1">Increment</button>
+
+        Count: <span x-text="value"></span>
+    </div>
+</div>
+```
+
+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.
+
+### 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.
+
+The following example will use the provided fallback value as the state if no `x-model` is present:
+
+```alpine
+<div>
+    <div x-data="{ value: $model(1) }">
+        <button @click="value = value + 1">Increment</button>
+
+        Count: <span x-text="value"></span>
+    </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.

+ 32 - 0
tests/cypress/integration/magics/$model.spec.js

@@ -211,3 +211,35 @@ test('$model can watch for changing values and watcher gets cleaned up on elemen
         get('h1').should(haveText('bob'))
     }
 )
+
+test('$model can be used as a getter/setter pair in x-data',
+    html`
+        <div x-data="{ foo: 'bar' }" x-model="foo">
+            <div x-data="{ value: $model }">
+                <button @click="value = 'baz'">click me</button>
+                <h2 x-text="value"></h2>
+            </div>
+        </div>
+    `,
+    ({ get }) => {
+        get('h2').should(haveText('bar'))
+        get('button').click()
+        get('h2').should(haveText('baz'))
+    }
+)
+
+test('$model can be used as a getter/setter pair in x-data with an initial value that makes x-model optional',
+    html`
+        <div x-data="{ foo: 'bar' }">
+            <div x-data="{ value: $model('bar') }">
+                <button @click="value = 'baz'">click me</button>
+                <h2 x-text="value"></h2>
+            </div>
+        </div>
+    `,
+    ({ get }) => {
+        get('h2').should(haveText('bar'))
+        get('button').click()
+        get('h2').should(haveText('baz'))
+    }
+)