Sfoglia il codice sorgente

Fix x-transition and x-for when used with x-bind

Caleb Porzio 5 anni fa
parent
commit
46b1979bc0
4 ha cambiato i file con 147 aggiunte e 25 eliminazioni
  1. 18 14
      dist/alpine.js
  2. 4 2
      src/directives/for.js
  3. 17 8
      src/utils.js
  4. 108 1
      test/bind.spec.js

+ 18 - 14
dist/alpine.js

@@ -106,7 +106,7 @@
   }
   function saferEvalNoReturn(expression, dataContext, additionalHelperVariables = {}) {
     if (typeof expression === 'function') {
-      expression.call(dataContext);
+      return expression.call(dataContext);
     } // For the cases when users pass only a function reference to the caller: `x-on:click="foo"`
     // Where "foo" is a function. Also, we'll pass the function the event instance when we call it.
 
@@ -190,7 +190,7 @@
       modifiers = settingBothSidesOfTransition ? modifiers.filter((i, index) => index < modifiers.indexOf('out')) : modifiers;
       transitionHelperIn(el, modifiers, show); // Otherwise, we can assume x-transition:enter.
     } else if (attrs.filter(attr => ['enter', 'enter-start', 'enter-end'].includes(attr.value)).length > 0) {
-      transitionClassesIn(el, attrs, show);
+      transitionClassesIn(el, component, attrs, show);
     } else {
       // If neither, just show that damn thing.
       show();
@@ -208,7 +208,7 @@
       modifiers = settingBothSidesOfTransition ? modifiers.filter((i, index) => index > modifiers.indexOf('out')) : modifiers;
       transitionHelperOut(el, modifiers, settingBothSidesOfTransition, hide);
     } else if (attrs.filter(attr => ['leave', 'leave-start', 'leave-end'].includes(attr.value)).length > 0) {
-      transitionClassesOut(el, attrs, hide);
+      transitionClassesOut(el, component, attrs, hide);
     } else {
       hide();
     }
@@ -329,19 +329,23 @@
     };
     transition(el, stages);
   }
-  function transitionClassesIn(el, directives, showCallback) {
-    const enter = (directives.find(i => i.value === 'enter') || {
+  function transitionClassesIn(el, component, directives, showCallback) {
+    let ensureStringExpression = expression => {
+      return typeof expression === 'function' ? component.evaluateReturnExpression(el, expression) : expression;
+    };
+
+    const enter = ensureStringExpression((directives.find(i => i.value === 'enter') || {
       expression: ''
-    }).expression.split(' ').filter(i => i !== '');
-    const enterStart = (directives.find(i => i.value === 'enter-start') || {
+    }).expression).split(' ').filter(i => i !== '');
+    const enterStart = ensureStringExpression((directives.find(i => i.value === 'enter-start') || {
       expression: ''
-    }).expression.split(' ').filter(i => i !== '');
-    const enterEnd = (directives.find(i => i.value === 'enter-end') || {
+    }).expression).split(' ').filter(i => i !== '');
+    const enterEnd = ensureStringExpression((directives.find(i => i.value === 'enter-end') || {
       expression: ''
-    }).expression.split(' ').filter(i => i !== '');
+    }).expression).split(' ').filter(i => i !== '');
     transitionClasses(el, enter, enterStart, enterEnd, showCallback, () => {});
   }
-  function transitionClassesOut(el, directives, hideCallback) {
+  function transitionClassesOut(el, component, directives, hideCallback) {
     const leave = (directives.find(i => i.value === 'leave') || {
       expression: ''
     }).expression.split(' ').filter(i => i !== '');
@@ -418,7 +422,7 @@
 
   function handleForDirective(component, templateEl, expression, initialUpdate, extraVars) {
     warnIfNotTemplateTag(templateEl);
-    let iteratorNames = parseForExpression(expression);
+    let iteratorNames = typeof expression === 'function' ? parseForExpression(component.evaluateReturnExpression(templateEl, expression)) : parseForExpression(expression);
     let items = evaluateItemsAndReturnEmptyIfXIfIsPresentAndFalseOnElement(component, templateEl, iteratorNames, extraVars); // As we walk the array, we'll also walk the DOM (updating/creating as we go).
 
     let currentEl = templateEl;
@@ -443,7 +447,7 @@
       currentEl = nextEl;
       currentEl.__x_for_key = currentKey;
     });
-    removeAnyLeftOverElementsFromPreviousUpdate(currentEl);
+    removeAnyLeftOverElementsFromPreviousUpdate(currentEl, component);
   } // This was taken from VueJS 2.* core. Thanks Vue!
 
   function parseForExpression(expression) {
@@ -525,7 +529,7 @@
     }
   }
 
-  function removeAnyLeftOverElementsFromPreviousUpdate(currentEl) {
+  function removeAnyLeftOverElementsFromPreviousUpdate(currentEl, component) {
     var nextElementFromOldLoop = currentEl.nextElementSibling && currentEl.nextElementSibling.__x_for_key !== undefined ? currentEl.nextElementSibling : false;
 
     while (nextElementFromOldLoop) {

+ 4 - 2
src/directives/for.js

@@ -1,9 +1,11 @@
-import { transitionIn, transitionOut, getXAttrs } from '../utils'
+import { transitionIn, transitionOut, getXAttrs, saferEval } from '../utils'
 
 export function handleForDirective(component, templateEl, expression, initialUpdate, extraVars) {
     warnIfNotTemplateTag(templateEl)
 
-    let iteratorNames = parseForExpression(expression)
+    let iteratorNames = typeof expression === 'function'
+        ? parseForExpression(component.evaluateReturnExpression(templateEl, expression))
+        : parseForExpression(expression)
 
     let items = evaluateItemsAndReturnEmptyIfXIfIsPresentAndFalseOnElement(component, templateEl, iteratorNames, extraVars)
 

+ 17 - 8
src/utils.js

@@ -61,7 +61,7 @@ export function saferEval(expression, dataContext, additionalHelperVariables = {
 
 export function saferEvalNoReturn(expression, dataContext, additionalHelperVariables = {}) {
     if (typeof expression === 'function') {
-        expression.call(dataContext)
+        return expression.call(dataContext)
     }
 
     // For the cases when users pass only a function reference to the caller: `x-on:click="foo"`
@@ -171,7 +171,7 @@ export function transitionIn(el, show, component, forceSkip = false) {
         transitionHelperIn(el, modifiers, show)
     // Otherwise, we can assume x-transition:enter.
     } else if (attrs.filter(attr => ['enter', 'enter-start', 'enter-end'].includes(attr.value)).length > 0) {
-        transitionClassesIn(el, attrs, show)
+        transitionClassesIn(el, component, attrs, show)
     } else {
     // If neither, just show that damn thing.
         show()
@@ -196,7 +196,7 @@ export function transitionOut(el, hide, component, forceSkip = false) {
 
         transitionHelperOut(el, modifiers, settingBothSidesOfTransition, hide)
     } else if (attrs.filter(attr => ['leave', 'leave-start', 'leave-end'].includes(attr.value)).length > 0) {
-        transitionClassesOut(el, attrs, hide)
+        transitionClassesOut(el, component, attrs, hide)
     } else {
         hide()
     }
@@ -324,15 +324,24 @@ export function transitionHelper(el, modifiers, hook1, hook2, styleValues) {
     transition(el, stages)
 }
 
-export function transitionClassesIn(el, directives, showCallback) {
-    const enter = (directives.find(i => i.value === 'enter') || { expression: '' }).expression.split(' ').filter(i => i !== '')
-    const enterStart = (directives.find(i => i.value === 'enter-start') || { expression: '' }).expression.split(' ').filter(i => i !== '')
-    const enterEnd = (directives.find(i => i.value === 'enter-end') || { expression: '' }).expression.split(' ').filter(i => i !== '')
+export function transitionClassesIn(el, component, directives, showCallback) {
+    let ensureStringExpression = (expression) => {
+        return typeof expression === 'function'
+            ? component.evaluateReturnExpression(el, expression)
+            : expression
+    }
+
+    const enter = ensureStringExpression((directives.find(i => i.value === 'enter') || { expression: '' }).expression)
+        .split(' ').filter(i => i !== '')
+    const enterStart = ensureStringExpression((directives.find(i => i.value === 'enter-start') || { expression: '' }).expression)
+        .split(' ').filter(i => i !== '')
+    const enterEnd = ensureStringExpression((directives.find(i => i.value === 'enter-end') || { expression: '' }).expression)
+        .split(' ').filter(i => i !== '')
 
     transitionClasses(el, enter, enterStart, enterEnd, showCallback, () => {})
 }
 
-export function transitionClassesOut(el, directives, hideCallback) {
+export function transitionClassesOut(el, component, directives, hideCallback) {
     const leave = (directives.find(i => i.value === 'leave') || { expression: '' }).expression.split(' ').filter(i => i !== '')
     const leaveStart = (directives.find(i => i.value === 'leave-start') || { expression: '' }).expression.split(' ').filter(i => i !== '')
     const leaveEnd = (directives.find(i => i.value === 'leave-end') || { expression: '' }).expression.split(' ').filter(i => i !== '')

+ 108 - 1
test/bind.spec.js

@@ -449,7 +449,7 @@ test('can bind an object of directives', async () => {
         return {
             show: false,
             trigger: {
-                '@click': function () { this.show = ! this.show }
+                ['x-on:click']() { this.show = ! this.show }
             },
             dialogue: {
                 ['x-show']() { return this.show }
@@ -477,3 +477,110 @@ test('can bind an object of directives', async () => {
 
     await wait(() => { expect(document.querySelector('span').getAttribute('style')).toEqual('display: none;') })
 })
+
+test('x-bind object spread syntax supports x-for', async () => {
+    window.todos = () => {
+        return {
+            todos: ['one', 'two', 'three'],
+            outputForExpression: {
+                ['x-for']() { return 'todo in todos' }
+            },
+        }
+    }
+
+    document.body.innerHTML = `
+        <div x-data="window.todos()">
+            <ul>
+                <template x-bind="outputForExpression">
+                    <li x-text="todo"></li>
+                </template>
+            </ul>
+        </div>
+    `
+
+    Alpine.start()
+
+    expect(document.querySelectorAll('li')[0].innerText).toEqual('one')
+    expect(document.querySelectorAll('li')[1].innerText).toEqual('two')
+    expect(document.querySelectorAll('li')[2].innerText).toEqual('three')
+})
+
+test('x-bind object spread syntax supports x-transition', async () => {
+    // Hijack "requestAnimationFrame" for finer-tuned control in this test.
+    var frameStack = []
+
+    jest.spyOn(window, 'requestAnimationFrame').mockImplementation((callback) => {
+        frameStack.push(callback)
+    });
+
+    // Hijack "getComputeStyle" because js-dom is weird with it.
+    // (hardcoding 10ms transition time for later assertions)
+    jest.spyOn(window, 'getComputedStyle').mockImplementation(el => {
+        return { transitionDuration: '.01s' }
+    });
+
+    window.transitions = () => {
+        return {
+            show: false,
+            outputClickExpression: {
+                ['x-on:click']() { this.show = ! this.show },
+            },
+            outputTransitionExpression: {
+                ['x-show']() { return this.show },
+                ['x-transition:enter']() { return 'enter' },
+                ['x-transition:enter-start']() { return 'enter-start' },
+                ['x-transition:enter-end']() { return 'enter-end' },
+            },
+        }
+    }
+
+    document.body.innerHTML = `
+        <div x-data="transitions()">
+            <button x-bind="outputClickExpression"></button>
+
+            <span x-bind="outputTransitionExpression"></span>
+        </div>
+    `
+
+    Alpine.start()
+
+    await wait(() => { expect(document.querySelector('span').getAttribute('style')).toEqual('display: none;') })
+
+    document.querySelector('button').click()
+
+    // Wait out the intial Alpine refresh debounce.
+    await new Promise((resolve) =>
+        setTimeout(() => {
+            resolve();
+        }, 5)
+    )
+
+    expect(document.querySelector('span').classList.contains('enter')).toEqual(true)
+    expect(document.querySelector('span').classList.contains('enter-start')).toEqual(true)
+    expect(document.querySelector('span').classList.contains('enter-end')).toEqual(false)
+    expect(document.querySelector('span').getAttribute('style')).toEqual('display: none;')
+
+    frameStack.pop()()
+
+    expect(document.querySelector('span').classList.contains('enter')).toEqual(true)
+    expect(document.querySelector('span').classList.contains('enter-start')).toEqual(true)
+    expect(document.querySelector('span').classList.contains('enter-end')).toEqual(false)
+    expect(document.querySelector('span').getAttribute('style')).toEqual(null)
+
+    frameStack.pop()()
+
+    expect(document.querySelector('span').classList.contains('enter')).toEqual(true)
+    expect(document.querySelector('span').classList.contains('enter-start')).toEqual(false)
+    expect(document.querySelector('span').classList.contains('enter-end')).toEqual(true)
+    expect(document.querySelector('span').getAttribute('style')).toEqual(null)
+
+    await new Promise((resolve) =>
+        setTimeout(() => {
+            expect(document.querySelector('span').classList.contains('enter')).toEqual(false)
+            expect(document.querySelector('span').classList.contains('enter-start')).toEqual(false)
+            expect(document.querySelector('span').classList.contains('enter-end')).toEqual(false)
+            expect(document.querySelector('span').getAttribute('style')).toEqual(null)
+            resolve();
+        }, 10)
+    )
+})