Преглед изворни кода

Merge branch 'master' into bind-value-if-changed

Caleb Porzio пре 5 година
родитељ
комит
5057f4b7d8
8 измењених фајлова са 122 додато и 22 уклоњено
  1. 4 2
      README.md
  2. 1 1
      package.json
  3. 3 2
      src/component.js
  4. 6 6
      src/directives/bind.js
  5. 7 11
      src/directives/for.js
  6. 24 0
      test/bind.spec.js
  7. 48 0
      test/constructor.spec.js
  8. 29 0
      test/for.spec.js

+ 4 - 2
README.md

@@ -217,7 +217,7 @@ If you wish to run code AFTER Alpine has made its initial updates to the DOM (so
 | `x-show.transition.origin.top.right` | Customize the CSS transform origin `transform-origin: top right`. |
 | `x-show.transition.in.duration.200ms.out.duration.50ms` | Different durations for "in" and "out". |
 
-> Note: All of these transition modifiers can be used in conjunction with each other. This is possible (although rediculous lol): `x-show.transition.in.duration.100ms.origin.top.right.opacity.scale.85.out.duration.200ms.origin.bottom.left.opacity.scale.95`
+> Note: All of these transition modifiers can be used in conjunction with each other. This is possible (although ridiculous lol): `x-show.transition.in.duration.100ms.origin.top.right.opacity.scale.85.out.duration.200ms.origin.bottom.left.opacity.scale.95`
 
 > Note: `x-show` will wait for any children to finish transitioning out. If you want to bypass this behavior, add the `.immediate` modifer:
 ```html
@@ -243,7 +243,7 @@ If you wish to run code AFTER Alpine has made its initial updates to the DOM (so
 
 `x-bind` behaves a little differently when binding to the `class` attribute.
 
-For classes, you pass in an object who's keys are class names, and values are boolean expressions to determine if those class names are applied or not.
+For classes, you pass in an object whose keys are class names, and values are boolean expressions to determine if those class names are applied or not.
 
 For example:
 `<div x-bind:class="{ 'hidden': foo }"></div>`
@@ -609,6 +609,8 @@ Since a policy applies to all scripts in your page, it's important that other ex
 * Remove "object" (and array) syntax from `x-bind:class="{ 'foo': true }"` ([#236](https://github.com/alpinejs/alpine/pull/236) to add support for object syntax for the `style` attribute)
 * Improve `x-for` mutation reactivity ([#165](https://github.com/alpinejs/alpine/pull/165))
 * Add "deep watching" support in V3 ([#294](https://github.com/alpinejs/alpine/pull/294))
+* Add `$el` shortcut
+* Change `@click.away` to `@click.outside`?
 
 ## License
 

+ 1 - 1
package.json

@@ -1,7 +1,7 @@
 {
     "main": "dist/alpine.js",
     "name": "alpinejs",
-    "version": "2.3.1",
+    "version": "2.3.3",
     "repository": {
         "type": "git",
         "url": "git://github.com/alpinejs/alpine.git"

+ 3 - 2
src/component.js

@@ -333,10 +333,11 @@ export default class Component {
         }
 
         const observer = new MutationObserver((mutations) => {
-            for (let i=0; i < mutations.length; i++){
+            for (let i=0; i < mutations.length; i++) {
                 // Filter out mutations triggered from child components.
                 const closestParentComponent = mutations[i].target.closest('[x-data]')
-                if (! (closestParentComponent && closestParentComponent.isSameNode(this.$el))) return
+
+                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'), {})

+ 6 - 6
src/directives/bind.js

@@ -43,9 +43,9 @@ export function handleAttributeBindingDirective(component, el, attrName, express
         } else if (el.tagName === 'SELECT') {
             updateSelect(el, value)
         } else {
-            if(el.value !== value) {
-                el.value = value
-            }
+            if (el.value === value) return
+
+            el.value = value
         }
     } else if (attrName === 'class') {
         if (Array.isArray(value)) {
@@ -58,14 +58,14 @@ export function handleAttributeBindingDirective(component, el, attrName, express
 
             keysSortedByBooleanValue.forEach(classNames => {
                 if (value[classNames]) {
-                    classNames.split(' ').forEach(className => el.classList.add(className))
+                    classNames.split(' ').filter(Boolean).forEach(className => el.classList.add(className))
                 } else {
-                    classNames.split(' ').forEach(className => el.classList.remove(className))
+                    classNames.split(' ').filter(Boolean).forEach(className => el.classList.remove(className))
                 }
             })
         } else {
             const originalClasses = el.__x_original_classes || []
-            const newClasses = value.split(' ')
+            const newClasses = value.split(' ').filter(Boolean)
             el.setAttribute('class', arrayUnique(originalClasses.concat(newClasses)).join(' '))
         }
     } else if (isBooleanAttr(attrName)) {

+ 7 - 11
src/directives/for.js

@@ -12,10 +12,10 @@ export function handleForDirective(component, templateEl, expression, initialUpd
     items.forEach((item, index) => {
         let iterationScopeVariables = getIterationScopeVariables(iteratorNames, item, index, items, extraVars())
         let currentKey = generateKeyForIteration(component, templateEl, index, iterationScopeVariables)
-        let nextEl = currentEl.nextElementSibling
+        let nextEl = lookAheadForMatchingKeyedElementAndMoveItIfFound(currentEl.nextElementSibling, currentKey)
 
-        // If there's no previously x-for processed element ahead, add one.
-        if (! nextEl || nextEl.__x_for_key === undefined) {
+        // If we haven't found a matching key, insert the element at the current position.
+        if (! nextEl) {
             nextEl = addElementInLoopAfterCurrentEl(templateEl, currentEl)
 
             // And transition it in if it's not the first page load.
@@ -23,15 +23,9 @@ export function handleForDirective(component, templateEl, expression, initialUpd
 
             nextEl.__x_for = iterationScopeVariables
             component.initializeElements(nextEl, () => nextEl.__x_for)
+        // Otherwise update the element we found.
         } else {
-            nextEl = lookAheadForMatchingKeyedElementAndMoveItIfFound(nextEl, currentKey)
-
-            // If we haven't found a matching key, just insert the element at the current position
-            if (! nextEl) {
-                nextEl = addElementInLoopAfterCurrentEl(templateEl, currentEl)
-            }
-
-            // Temporarily remove the key indicator to allow the normal "updateElements" to work
+            // Temporarily remove the key indicator to allow the normal "updateElements" to work.
             delete nextEl.__x_for_key
 
             nextEl.__x_for = iterationScopeVariables
@@ -114,6 +108,8 @@ function addElementInLoopAfterCurrentEl(templateEl, currentEl) {
 }
 
 function lookAheadForMatchingKeyedElementAndMoveItIfFound(nextEl, currentKey) {
+    if (! nextEl) return
+
     // If the the key's DO match, no need to look ahead.
     if (nextEl.__x_for_key === currentKey) return nextEl
 

+ 24 - 0
test/bind.spec.js

@@ -391,3 +391,27 @@ test('value bindings to hidden inputs', async () => {
         expect(document.querySelector('input').value).toEqual('baz')
     })
 })
+
+test('extra whitespace in class binding object syntax is ignored', async () => {
+    document.body.innerHTML = `
+        <div x-data>
+            <span x-bind:class="{ '  foo  bar  ': true }"></span>
+        </div>
+    `
+    Alpine.start()
+
+    expect(document.querySelector('span').classList.contains('foo')).toBeTruthy()
+    expect(document.querySelector('span').classList.contains('bar')).toBeTruthy()
+})
+
+test('extra whitespace in class binding string syntax is ignored', async () => {
+    document.body.innerHTML = `
+        <div x-data>
+            <span x-bind:class="'  foo  bar  '"></span>
+        </div>
+    `
+    Alpine.start()
+
+    expect(document.querySelector('span').classList.contains('foo')).toBeTruthy()
+    expect(document.querySelector('span').classList.contains('bar')).toBeTruthy()
+})

+ 48 - 0
test/constructor.spec.js

@@ -1,5 +1,6 @@
 import Alpine from 'alpinejs'
 import { fireEvent, wait } from '@testing-library/dom'
+const timeout = ms => new Promise(resolve => setTimeout(resolve, ms))
 
 test('auto-detect new components at the top level', async () => {
     var runObservers = []
@@ -340,3 +341,50 @@ test('x-attributes are matched exactly', async () => {
     expect(document.getElementById('el2').style.display).not.toEqual('none')
     await wait(() => { expect(document.getElementById('el3').style.display).not.toEqual('none') })
 })
+
+
+test('a mutation from another part of the HTML doesnt prevent a different alpine component from initializing', async () => {
+    document.body.innerHTML = `
+        <div x-data x-init="registerInit()">
+        </div>
+    `
+
+    var initCount = 0
+    window.registerInit = function () {
+        initCount = initCount + 1
+    }
+
+    var runObservers = []
+
+    global.MutationObserver = class {
+        constructor(callback) { runObservers.push(callback) }
+        observe() {}
+    }
+
+    Alpine.start()
+
+    await wait(() => { expect(initCount).toEqual(1) })
+
+    document.querySelector('div').innerHTML = `
+        <h1 x-data x-init="registerInit()"></h1>
+    `
+    let h2 = document.createElement('h2')
+    document.querySelector('div').parentElement.appendChild(h2)
+
+    await timeout(5)
+
+    runObservers[0]([
+        {
+            target: document.querySelector('h2'),
+            type: 'attributes',
+            addedNodes: [],
+        },
+        {
+            target: document.querySelector('div'),
+            type: 'childList',
+            addedNodes: [ document.querySelector('h1') ],
+        }
+    ])
+
+    await wait(() => { expect(initCount).toEqual(2) })
+})

+ 29 - 0
test/for.spec.js

@@ -448,3 +448,32 @@ test('nested x-for event listeners', async () => {
         expect(document._alerts[2]).toEqual('foo: bob = 2')
     })
 })
+
+test('make sure new elements with different keys added to the beginning of a loop are initialized instead of just updated', async () => {
+    let clickCount = 0
+    window.registerClick = () => {
+        clickCount++
+    }
+
+    document.body.innerHTML = `
+        <div x-data="{ items: ['foo'] }">
+            <button @click="items = ['bar']">Change</button>
+
+            <template x-for="item in items" :key="item">
+                <h1 @click="registerClick()"></h1>
+            </template>
+        </div>
+    `
+
+    Alpine.start()
+
+    document.querySelector('h1').click()
+
+    expect(clickCount).toEqual(1)
+
+    document.querySelector('button').click()
+
+    document.querySelector('h1').click()
+
+    expect(clickCount).toEqual(2)
+})