瀏覽代碼

Fix lazy iteration evaluation inside x-for loop

Caleb Porzio 5 年之前
父節點
當前提交
6c932f8d6e
共有 9 個文件被更改,包括 153 次插入27 次删除
  1. 0 0
      dist/alpine.js
  2. 0 0
      dist/alpine.js.map
  3. 0 0
      examples/index.html
  4. 89 0
      examples/tags.html
  5. 11 11
      src/component.js
  6. 15 8
      src/directives/for.js
  7. 2 2
      src/directives/model.js
  8. 6 6
      src/directives/on.js
  9. 30 0
      test/for.spec.js

File diff suppressed because it is too large
+ 0 - 0
dist/alpine.js


File diff suppressed because it is too large
+ 0 - 0
dist/alpine.js.map


+ 0 - 0
index.html → examples/index.html


+ 89 - 0
examples/tags.html

@@ -0,0 +1,89 @@
+<html>
+    <head>
+        <style>
+        .tags-input {
+        display: flex;
+        flex-wrap: wrap;
+        background-color: #fff;
+        border-width: 1px;
+        border-radius: .25rem;
+        padding-left: .5rem;
+        padding-right: 1rem;
+        padding-top: .5rem;
+        padding-bottom: .25rem;
+        }
+
+        .tags-input-tag {
+        display: inline-flex;
+        line-height: 1;
+        align-items: center;
+        font-size: .875rem;
+        background-color: #bcdefa;
+        color: #1c3d5a;
+        border-radius: .25rem;
+        user-select: none;
+        padding: .25rem;
+        margin-right: .5rem;
+        margin-bottom: .25rem;
+        }
+
+        .tags-input-tag:last-of-type {
+        margin-right: 0;
+        }
+
+        .tags-input-remove {
+        color: #2779bd;
+        font-size: 1.125rem;
+        line-height: 1;
+        }
+
+        .tags-input-remove:first-child {
+        margin-right: .25rem;
+        }
+
+        .tags-input-remove:last-child {
+        margin-left: .25rem;
+        }
+
+        .tags-input-remove:focus {
+        outline: 0;
+        }
+
+        .tags-input-text {
+        flex: 1;
+        outline: 0;
+        padding-top: .25rem;
+        padding-bottom: .25rem;
+        margin-left: .5rem;
+        margin-bottom: .25rem;
+        min-width: 10rem;
+        }
+
+        .py-16 {
+        padding-top: 4rem;
+        padding-bottom: 4rem;
+    }
+    </style>
+        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/0.5.1/tailwind.css">
+        <script src="/dist/alpine.js" defer></script>
+    </head>
+    <body>
+        <div x-data="{tags: ['hey'], newTag: '' }" class="bg-grey-lighter px-8 py-16 min-h-screen">
+            <input type="hidden" name="tags" :value="JSON.stringify(tags)">
+            <div class="max-w-sm w-full mx-auto">
+              <div class="tags-input">
+                <template x-for="tag in tags" :key="tag">
+                  <span class="tags-input-tag">
+                    <span x-text="tag"></span>
+                    <button type="button" class="tags-input-remove" @click="tags = tags.filter(i => i !== tag)">&times;</button>
+                  </span>
+                </template>
+                <input class="tags-input-text" placeholder="Add tag..."
+                  @keydown.enter="if (newTag.trim() !== '') tags.push(newTag.trim()); newTag = ''"
+                  x-model="newTag"
+                >
+              </div>
+            </div>
+          </div>
+    </body>
+</html>

+ 11 - 11
src/component.js

@@ -129,7 +129,7 @@ export default class Component {
         })
     }
 
-    initializeElements(rootEl, extraVars = {}) {
+    initializeElements(rootEl, extraVars = () => {}) {
         this.walkAndSkipNestedComponents(rootEl, el => {
             // Don't touch spawns from for loop
             if (el.__x_for_key !== undefined) return false
@@ -145,7 +145,7 @@ export default class Component {
         }
     }
 
-    initializeElement(el, extraVars = {}) {
+    initializeElement(el, extraVars) {
         // To support class attribute merging, we have to know what the element's
         // original class attribute looked like for reference.
         if (el.hasAttribute('class') && getXAttrs(el).length > 0) {
@@ -156,7 +156,7 @@ export default class Component {
         this.resolveBoundAttributes(el, true, extraVars)
     }
 
-    updateElements(rootEl, extraVars = {}) {
+    updateElements(rootEl, extraVars = () => {}) {
         this.walkAndSkipNestedComponents(rootEl, el => {
             // Don't touch spawns from for loop (and check if the root is actually a for loop in a parent, don't skip it.)
             if (el.__x_for_key !== undefined && ! el.isSameNode(this.$el)) return false
@@ -167,19 +167,19 @@ export default class Component {
         })
     }
 
-    updateElement(el, extraVars = {}) {
+    updateElement(el, extraVars) {
         this.resolveBoundAttributes(el, false, extraVars)
     }
 
-    registerListeners(el, extraVars = {}) {
+    registerListeners(el, extraVars) {
         getXAttrs(el).forEach(({ type, value, modifiers, expression }) => {
             switch (type) {
                 case 'on':
-                    registerListener(this, el, value, modifiers, expression)
+                    registerListener(this, el, value, modifiers, expression, extraVars)
                     break;
 
                 case 'model':
-                    registerModelListener(this, el, modifiers, expression)
+                    registerModelListener(this, el, modifiers, expression, extraVars)
                     break;
                 default:
                     break;
@@ -235,12 +235,12 @@ export default class Component {
         })
     }
 
-    evaluateReturnExpression(expression, extraData) {
-        return saferEval(expression, this.$data, extraData)
+    evaluateReturnExpression(expression, extraVars = () => {}) {
+        return saferEval(expression, this.$data, extraVars())
     }
 
-    evaluateCommandExpression(expression, extraData) {
-        saferEvalNoReturn(expression, this.$data, extraData)
+    evaluateCommandExpression(expression, extraVars = () => {}) {
+        saferEvalNoReturn(expression, this.$data, extraVars())
     }
 
     listenForNewElementsToInitialize() {

+ 15 - 8
src/directives/for.js

@@ -34,10 +34,11 @@ export function handleForDirective(component, el, expression, initialUpdate) {
             // Temporarily remove the key indicator to allow the normal "updateElements" to work
             delete currentEl.__x_for_key
 
-            component.updateElements(currentEl, {'item': i})
-
-            // Reset it for next time around.
-            currentEl.__x_for_key = currentKey
+            currentEl.__x_for_alias = single
+            currentEl.__x_for_value = i
+            component.updateElements(currentEl, () => {
+                return {[currentEl.__x_for_alias]: currentEl.__x_for_value}
+            })
         } else {
             // There are no more .__x_for_key elements, meaning the page is first loading, OR, there are
             // extra items in the array that need to be added as new elements.
@@ -55,11 +56,17 @@ export function handleForDirective(component, el, expression, initialUpdate) {
 
             // Now, let's walk the new DOM node and initialize everything,
             // including new nested components.
-            component.initializeElements(currentEl, {[single]: i})
-
-            currentEl.__x_for_key = currentKey
+            // Note we are resolving the "extraData" alias stuff from the dom element value so that it's
+            // always up to date for listener handlers that don't get re-registered.
+            currentEl.__x_for_alias = single
+            currentEl.__x_for_value = i
+            component.initializeElements(currentEl, () => {
+                return {[currentEl.__x_for_alias]: currentEl.__x_for_value}
+            })
         }
 
+        currentEl.__x_for_key = currentKey
+
         previousEl = currentEl
     })
 
@@ -110,6 +117,6 @@ function getThisIterationsKeyFromTemplateTag(component, el, single, iterator1, i
     if (iterator2) keyAliases[iterator2] = group
 
     return keyAttr
-        ? component.evaluateReturnExpression(keyAttr.expression, keyAliases)
+        ? component.evaluateReturnExpression(keyAttr.expression, () => keyAliases)
         : index
 }

+ 2 - 2
src/directives/model.js

@@ -1,6 +1,6 @@
 import { registerListener } from './on'
 
-export function registerModelListener(component, el, modifiers, expression) {
+export function registerModelListener(component, el, modifiers, expression, extraVars = {}) {
     // If the element we are binding to is a select, a radio, or checkbox
     // we'll listen for the change event instead of the "input" event.
     var event = (el.tagName.toLowerCase() === 'select')
@@ -10,7 +10,7 @@ export function registerModelListener(component, el, modifiers, expression) {
 
     const listenerExpression = modelListenerExpression(component, el, modifiers, expression)
 
-    registerListener(component, el, event, modifiers, listenerExpression)
+    registerListener(component, el, event, modifiers, listenerExpression, extraVars)
 }
 
 function modelListenerExpression(component, el, modifiers, dataKey) {

+ 6 - 6
src/directives/on.js

@@ -1,6 +1,6 @@
 import { keyToModifier } from '../utils'
 
-export function registerListener(component, el, event, modifiers, expression) {
+export function registerListener(component, el, event, modifiers, expression, extraVars = {}) {
     if (modifiers.includes('away')) {
         const handler = e => {
             // Don't do anything if the click came form the element or within it.
@@ -11,7 +11,7 @@ export function registerListener(component, el, event, modifiers, expression) {
 
             // Now that we are sure the element is visible, AND the click
             // is from outside it, let's run the expression.
-            runListenerHandler(component, expression, e)
+            runListenerHandler(component, expression, e, extraVars)
 
             if (modifiers.includes('once')) {
                 document.removeEventListener(event, handler)
@@ -51,7 +51,7 @@ export function registerListener(component, el, event, modifiers, expression) {
             if (modifiers.includes('prevent')) e.preventDefault()
             if (modifiers.includes('stop')) e.stopPropagation()
 
-            runListenerHandler(component, expression, e)
+            runListenerHandler(component, expression, e, extraVars)
 
             if (modifiers.includes('once')) {
                 listenerTarget.removeEventListener(event, handler)
@@ -62,8 +62,8 @@ export function registerListener(component, el, event, modifiers, expression) {
     }
 }
 
-function runListenerHandler(component, expression, e) {
-    component.evaluateCommandExpression(expression, {
-        '$event': e,
+function runListenerHandler(component, expression, e, extraVars) {
+    component.evaluateCommandExpression(expression, () => {
+        return {...extraVars(), '$event': e}
     })
 }

+ 30 - 0
test/for.spec.js

@@ -174,3 +174,33 @@ test('can key by index', async () => {
 
     await wait(() => { expect(document.querySelectorAll('span').length).toEqual(3) })
 })
+
+test('listeners in loop get fresh iteration data even though they are only registered initially', async () => {
+    document.body.innerHTML = `
+        <div x-data="{ items: ['foo'], output: '' }">
+            <button x-on:click="items = ['bar']"></button>
+
+            <template x-for="(item, index) in items">
+                <span x-text="item" x-on:click="output = item"></span>
+            </template>
+
+            <h1 x-text="output"></h1>
+        </div>
+    `
+
+    Alpine.start()
+
+    expect(document.querySelectorAll('span').length).toEqual(1)
+
+    document.querySelector('span').click()
+
+    await wait(() => { expect(document.querySelector('h1').innerText).toEqual('foo') })
+
+    document.querySelector('button').click()
+
+    await wait(() => { expect(document.querySelector('span').innerText).toEqual('bar') })
+
+    document.querySelector('span').click()
+
+    await wait(() => { expect(document.querySelector('h1').innerText).toEqual('bar') })
+})

Some files were not shown because too many files changed in this diff