Parcourir la source

Change x-bind object spread to x-spread directive

Caleb Porzio il y a 5 ans
Parent
commit
e573d5a070
4 fichiers modifiés avec 168 ajouts et 154 suppressions
  1. 17 11
      dist/alpine.js
  2. 3 2
      src/utils.js
  3. 0 141
      test/bind.spec.js
  4. 148 0
      test/spread.spec.js

+ 17 - 11
dist/alpine.js

@@ -70,6 +70,13 @@
   function isTesting() {
     return navigator.userAgent.includes("Node.js") || navigator.userAgent.includes("jsdom");
   }
+  function warnIfMalformedTemplate(el, directive) {
+    if (el.tagName.toLowerCase() !== 'template') {
+      console.warn(`Alpine: [${directive}] directive should only be added to <template> tags. See https://github.com/alpinejs/alpine#${directive}`);
+    } else if (el.content.childElementCount !== 1) {
+      console.warn(`Alpine: <template> tag with [${directive}] encountered with multiple element roots. Make sure <template> only has a single child node.`);
+    }
+  }
   function kebabCase(subject) {
     return subject.replace(/([a-z])([A-Z])/g, '$1-$2').replace(/[_\s]/, '-').toLowerCase();
   }
@@ -121,14 +128,14 @@
 
     return new Function(['dataContext', ...Object.keys(additionalHelperVariables)], `with(dataContext) { ${expression} }`)(dataContext, ...Object.values(additionalHelperVariables));
   }
-  const xAttrRE = /^x-(on|bind|data|text|html|model|if|for|show|cloak|transition|ref)\b/;
+  const xAttrRE = /^x-(on|bind|data|text|html|model|if|for|show|cloak|transition|ref|spread)\b/;
   function isXAttr(attr) {
     const name = replaceAtAndColonWithStandardSyntax(attr.name);
     return xAttrRE.test(name);
   }
   function getXAttrs(el, component, type) {
     return Array.from(el.attributes).filter(isXAttr).map(parseHtmlAttribute).flatMap(i => {
-      if (i.type === 'bind' && i.value === null) {
+      if (i.type === 'spread') {
         let directiveBindings = saferEval(i.expression, component.$data);
         return Object.entries(directiveBindings).map(([name, value]) => parseHtmlAttribute({
           name,
@@ -421,7 +428,7 @@
   }
 
   function handleForDirective(component, templateEl, expression, initialUpdate, extraVars) {
-    warnIfNotTemplateTag(templateEl);
+    warnIfMalformedTemplate(templateEl, 'x-for');
     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).
 
@@ -491,10 +498,6 @@
     return component.evaluateReturnExpression(el, bindKeyAttribute.expression, () => iterationScopeVariables);
   }
 
-  function warnIfNotTemplateTag(el) {
-    if (el.tagName.toLowerCase() !== 'template') console.warn('Alpine: [x-for] directive should only be added to <template> tags.');
-  }
-
   function evaluateItemsAndReturnEmptyIfXIfIsPresentAndFalseOnElement(component, el, iteratorNames, extraVars) {
     let ifAttribute = getXAttrs(el, component, 'if')[0];
 
@@ -507,7 +510,6 @@
 
   function addElementInLoopAfterCurrentEl(templateEl, currentEl) {
     let clone = document.importNode(templateEl.content, true);
-    if (clone.childElementCount !== 1) console.warn('Alpine: <template> tag with [x-for] encountered with multiple element roots. Make sure <template> only has a single child node.');
     currentEl.parentElement.insertBefore(clone, currentEl.nextElementSibling);
     return currentEl.nextElementSibling;
   }
@@ -702,7 +704,7 @@
   }
 
   function handleIfDirective(component, el, expressionResult, initialUpdate, extraVars) {
-    if (el.nodeName.toLowerCase() !== 'template') console.warn(`Alpine: [x-if] directive should only be added to <template> tags. See https://github.com/alpinejs/alpine#x-if`);
+    warnIfMalformedTemplate(el, 'x-if');
     const elementHasAlreadyBeenAdded = el.nextElementSibling && el.nextElementSibling.__x_inserted_me === true;
 
     if (expressionResult && !elementHasAlreadyBeenAdded) {
@@ -1297,7 +1299,9 @@
       const dataAttr = this.$el.getAttribute('x-data');
       const dataExpression = dataAttr === '' ? '{}' : dataAttr;
       const initExpression = this.$el.getAttribute('x-init');
-      this.unobservedData = seedDataForCloning ? seedDataForCloning : saferEval(dataExpression, {});
+      this.unobservedData = seedDataForCloning ? seedDataForCloning : saferEval(dataExpression, {
+        $el: this.$el
+      });
       // Construct a Proxy-based observable. This will be used to handle reactivity.
 
       let {
@@ -1593,7 +1597,9 @@
           if (!(closestParentComponent && closestParentComponent.isSameNode(this.$el))) continue;
 
           if (mutations[i].type === 'attributes' && mutations[i].attributeName === 'x-data') {
-            const rawData = saferEval(mutations[i].target.getAttribute('x-data'), {});
+            const rawData = saferEval(mutations[i].target.getAttribute('x-data'), {
+              $el: this.$el
+            });
             Object.keys(rawData).forEach(key => {
               if (this.$data[key] !== rawData[key]) {
                 this.$data[key] = rawData[key];

+ 3 - 2
src/utils.js

@@ -89,10 +89,11 @@ export function saferEvalNoReturn(expression, dataContext, additionalHelperVaria
     )
 }
 
-const xAttrRE = /^x-(on|bind|data|text|html|model|if|for|show|cloak|transition|ref)\b/
+const xAttrRE = /^x-(on|bind|data|text|html|model|if|for|show|cloak|transition|ref|spread)\b/
 
 export function isXAttr(attr) {
     const name = replaceAtAndColonWithStandardSyntax(attr.name)
+
     return xAttrRE.test(name)
 }
 
@@ -101,7 +102,7 @@ export function getXAttrs(el, component, type) {
         .filter(isXAttr)
         .map(parseHtmlAttribute)
         .flatMap(i => {
-            if (i.type === 'bind' && i.value === null) {
+            if (i.type === 'spread') {
                 let directiveBindings = saferEval(i.expression, component.$data)
 
                 return Object.entries(directiveBindings).map(([name, value]) => parseHtmlAttribute({ name, value }))

+ 0 - 141
test/bind.spec.js

@@ -483,144 +483,3 @@ test('extra whitespace in class binding string syntax is ignored', async () => {
     expect(document.querySelector('span').classList.contains('foo')).toBeTruthy()
     expect(document.querySelector('span').classList.contains('bar')).toBeTruthy()
 })
-
-test('can bind an object of directives', async () => {
-    window.modal = () => {
-        return {
-            show: false,
-            trigger: {
-                ['x-on:click']() { this.show = ! this.show }
-            },
-            dialogue: {
-                ['x-show']() { return this.show }
-            },
-        }
-    }
-
-    document.body.innerHTML = `
-        <div x-data="window.modal()">
-            <button x-bind="trigger">Toggle</button>
-
-            <span x-bind="dialogue">Modal Body</span>
-        </div>
-    `
-
-    Alpine.start()
-
-    expect(document.querySelector('span').getAttribute('style')).toEqual('display: none;')
-
-    document.querySelector('button').click()
-
-    await wait(() => { expect(document.querySelector('span').getAttribute('style')).toEqual(null) })
-
-    document.querySelector('button').click()
-
-    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)
-    )
-})

+ 148 - 0
test/spread.spec.js

@@ -0,0 +1,148 @@
+import Alpine from 'alpinejs'
+import { wait } from '@testing-library/dom'
+
+global.MutationObserver = class {
+    observe() {}
+}
+
+test('can bind an object of directives', async () => {
+    window.modal = () => {
+        return {
+            show: false,
+            trigger: {
+                ['x-on:click']() { this.show = ! this.show }
+            },
+            dialogue: {
+                ['x-show']() { return this.show }
+            },
+        }
+    }
+
+    document.body.innerHTML = `
+        <div x-data="window.modal()">
+            <button x-spread="trigger">Toggle</button>
+
+            <span x-spread="dialogue">Modal Body</span>
+        </div>
+    `
+
+    Alpine.start()
+
+    expect(document.querySelector('span').getAttribute('style')).toEqual('display: none;')
+
+    document.querySelector('button').click()
+
+    await wait(() => { expect(document.querySelector('span').getAttribute('style')).toEqual(null) })
+
+    document.querySelector('button').click()
+
+    await wait(() => { expect(document.querySelector('span').getAttribute('style')).toEqual('display: none;') })
+})
+
+
+test('x-spread 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-spread="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-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-spread="outputClickExpression"></button>
+
+            <span x-spread="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)
+    )
+})