Caleb Porzio 1 年之前
父节点
当前提交
cc2bdcc1eb

+ 14 - 1
index.html

@@ -10,6 +10,16 @@
     <!-- <script src="//cdn.tailwindcss.com"></script> -->
     <!-- <script src="//cdn.tailwindcss.com"></script> -->
 
+    <div x-data="{ foo: 'bar' }" x-model="foo">
+        <select x-model="$model">
+            <option>bar</option>
+            <option>baz</option>
+        </select>
+
+        <h1 x-text="foo"></h1>
+        <h2 x-text="$model.get()"></h2>
+    </div>
+
     <div x-data="{ count: 1 }">
         <div>
             <span x-text="count"></span>
@@ -17,7 +27,10 @@
             <main x-data="{ value: $model }" x-model="count">
                 Count: <h1 x-text="value"></h1>
 
-                <input type="text" x-model="value">
+                <div x-data="{ foo: 'bar' }">
+                    <input type="text" x-model="foo">
+                    Inner input: <span x-text="foo"></span>
+                </div>
             </main>
         </div>
     </div>

+ 1 - 5
packages/alpinejs/src/directives/x-data.js

@@ -22,11 +22,7 @@ directive('data', ((el, { expression }, { cleanup }) => {
     let dataProviderContext = {}
     injectDataProviders(dataProviderContext, magicContext)
 
-    let data
-
-    eagerlyRunXModelIfMagicModelIsUsedInsideThisExpression(() => {
-        data = evaluate(el, expression, { scope: dataProviderContext })
-    })
+    let data = evaluate(el, expression, { scope: dataProviderContext })
 
     if (data === undefined || data === true) data = {}
 

+ 0 - 6
packages/alpinejs/src/directives/x-model.js

@@ -133,12 +133,6 @@ function generateGetAndSet(evaluateLater, scopeTarget, expression) {
 
         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 (typeof result === 'object' && result !== null && result._x_modelAccessor) {
-            return result._x_modelAccessor.closest
-        }
-
         return result
     }
 

+ 21 - 21
packages/alpinejs/src/magics/$model.js

@@ -9,16 +9,6 @@ magic('model', (el, { cleanup }) => {
 
     let func = 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)
 })
 
@@ -28,6 +18,8 @@ function generateModelAccessor(el, cleanup) {
         if (i._x_model) return true
     })
 
+    closestModelEl && destroyModelListeners(closestModelEl)
+
     // 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 (fallbackStateInitialValue) {
@@ -63,19 +55,11 @@ function generateModelAccessor(el, cleanup) {
     return accessor
 }
 
-let isInsideXData = false
-
-export function eagerlyRunXModelIfMagicModelIsUsedInsideThisExpression(callback) {
-    isInsideXData = true
-
-    callback()
-
-    isInsideXData = false
-}
-
 function eagerlyRunXModelIfNeeded(el) {
-    if (! isInsideXData) return
+    // Looks like x-model has already been run so we're good...
+    if (el._x_model) return
 
+    // We only care to run x-model on elements WITH x-model...
     if (! el.hasAttribute('x-model')) return
 
     let attribute = { name: 'x-model', value: el.getAttribute('x-model') }
@@ -84,3 +68,19 @@ function eagerlyRunXModelIfNeeded(el) {
         handle()
     })
 }
+
+function destroyModelListeners(modelEl) {
+    if (! modelEl._x_removeModelListeners) return
+
+    if (isInputyElement(modelEl)) return
+
+    modelEl._x_removeModelListeners['default']()
+}
+
+function isInputyElement(el) {
+    let tag = el.tagName.toLowerCase()
+
+    inputTags = ['input', 'textarea', 'select']
+
+    return inputTags.includes(tag)
+}

+ 38 - 2
tests/cypress/integration/magics/$model.spec.js

@@ -1,4 +1,4 @@
-import { beSelected, haveText, html, notBeSelected, test } from '../../utils'
+import { beSelected, haveText, haveValue, html, notBeSelected, test } from '../../utils'
 
 test('$model allows you to interact with parent x-model bindings explicitly',
     html`
@@ -63,6 +63,7 @@ test('$model can be used with a getter and setter',
     }
 )
 
+// @todo: this is failing...
 test('$model can be used with another x-model',
     html`
         <div x-data="{ foo: 'bar' }" x-model="foo">
@@ -89,7 +90,7 @@ test('$model can be used with another x-model',
     }
 )
 
-test.only('$model can be used on the same element as the corresponding x-model',
+test('$model can be used on the same element as the corresponding x-model',
     html`
         <div x-data="{ foo: 'bar' }">
             <button @click="foo = 'baz'">click me</button>
@@ -106,6 +107,41 @@ test.only('$model can be used on the same element as the corresponding x-model',
     }
 )
 
+test('$model destroys x-model event listeners on initialization',
+    html`
+        <div x-data="{ foo: 'bar' }">
+            <div x-data="{ value: $model }" x-model="foo">
+                <h1 x-text="value"></h1>
+
+                <input type="text">
+            </div>
+        </div>
+    `,
+    ({ get }) => {
+        get('h1').should(haveText('bar'))
+        get('input').type('baz')
+        get('h1').should(haveText('bar'))
+    }
+)
+
+test('$model doesnt destroy x-model event listeners when on an input element',
+    html`
+        <div x-data="{ foo: 'bar' }">
+            <div>
+                <h1 x-text="foo"></h1>
+
+                <input type="text" x-data="{ value: $model }" x-model="foo">
+            </div>
+        </div>
+    `,
+    ({ get }) => {
+        get('h1').should(haveText('bar'))
+        get('input').should(haveValue('bar'))
+        get('input').type('baz')
+        get('h1').should(haveText('barbaz'))
+    }
+)
+
 test('$model can watch for changing values and watcher gets cleaned up on element removal',
     html`
         <div x-data="{ foo: 'bar' }" x-model="foo">