Browse Source

Merge pull request #75 from alpinejs/cp/fix-class-string-binding

Add support for string and array class binding (with merging)
Caleb Porzio 5 years ago
parent
commit
f57af75d38
5 changed files with 98 additions and 19 deletions
  1. 0 0
      dist/alpine.js
  2. 0 0
      dist/alpine.js.map
  3. 15 7
      src/component.js
  4. 12 0
      src/utils.js
  5. 71 12
      test/bind.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


+ 15 - 7
src/component.js

@@ -1,4 +1,4 @@
-import { walkSkippingNestedComponents, keyToModifier, saferEval, saferEvalNoReturn, getXAttrs, debounce, transitionIn, transitionOut } from './utils'
+import { arrayUnique, walkSkippingNestedComponents, keyToModifier, saferEval, saferEvalNoReturn, getXAttrs, debounce, transitionIn, transitionOut } from './utils'
 
 export default class Component {
     constructor(el) {
@@ -112,6 +112,12 @@ export default class Component {
     }
 
     initializeElement(el) {
+        // 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) {
+            el.__originalClasses = el.getAttribute('class').split(' ')
+        }
+
         this.registerListeners(el)
         this.resolveBoundAttributes(el, true)
     }
@@ -391,12 +397,10 @@ export default class Component {
                 el.value = value
             }
         } else if (attrName === 'class') {
-            if (typeof value === 'string') {
-                el.setAttribute('class', value)
-            } else if (Array.isArray(value)) {
-                el.setAttribute('class', value.join(' '))
-            } else {
-                // Use the class object syntax that vue uses to toggle them.
+            if (Array.isArray(value)) {
+                const originalClasses = el.__originalClasses || []
+                el.setAttribute('class', arrayUnique(originalClasses.concat(value)).join(' '))
+            } else if (typeof value === 'object') {
                 Object.keys(value).forEach(classNames => {
                     if (value[classNames]) {
                         classNames.split(' ').forEach(className => el.classList.add(className))
@@ -404,6 +408,10 @@ export default class Component {
                         classNames.split(' ').forEach(className => el.classList.remove(className))
                     }
                 })
+            } else {
+                const originalClasses = el.__originalClasses || []
+                const newClasses = value.split(' ')
+                el.setAttribute('class', arrayUnique(originalClasses.concat(newClasses)).join(' '))
             }
         } else if (['disabled', 'readonly', 'required', 'checked', 'hidden'].includes(attrName)) {
             // Boolean attributes have to be explicitly added and removed, not just set.

+ 12 - 0
src/utils.js

@@ -11,6 +11,18 @@ export function domReady() {
     })
 }
 
+export function arrayUnique(array) {
+    var a = array.concat();
+    for(var i=0; i<a.length; ++i) {
+        for(var j=i+1; j<a.length; ++j) {
+            if(a[i] === a[j])
+                a.splice(j--, 1);
+        }
+    }
+
+    return a;
+}
+
 export function isTesting() {
     return navigator.userAgent, navigator.userAgent.includes("Node.js")
         || navigator.userAgent.includes("jsdom")

+ 71 - 12
test/bind.spec.js

@@ -17,6 +17,65 @@ test('attribute bindings are set on initialize', async () => {
     expect(document.querySelector('span').getAttribute('foo')).toEqual('bar')
 })
 
+test('class attribute bindings are merged by string syntax', async () => {
+    document.body.innerHTML = `
+        <div x-data="{ isOn: false }">
+            <span class="foo" x-bind:class="isOn ? 'bar': ''"></span>
+
+            <button @click="isOn = ! isOn"></button>
+        </div>
+    `
+    Alpine.start()
+
+    expect(document.querySelector('span').classList.contains('foo')).toBeTruthy()
+    expect(document.querySelector('span').classList.contains('bar')).toBeFalsy()
+
+    document.querySelector('button').click()
+
+    await wait(() => {
+        expect(document.querySelector('span').classList.contains('foo')).toBeTruthy()
+        expect(document.querySelector('span').classList.contains('bar')).toBeTruthy()
+    })
+
+    document.querySelector('button').click()
+
+    await wait(() => {
+        expect(document.querySelector('span').classList.contains('foo')).toBeTruthy()
+        expect(document.querySelector('span').classList.contains('bar')).toBeFalsy()
+    })
+})
+
+test('class attribute bindings are merged by array syntax', async () => {
+    document.body.innerHTML = `
+        <div x-data="{ isOn: false }">
+            <span class="foo" x-bind:class="isOn ? ['bar', 'baz']: ['bar']"></span>
+
+            <button @click="isOn = ! isOn"></button>
+        </div>
+    `
+    Alpine.start()
+
+    expect(document.querySelector('span').classList.contains('foo')).toBeTruthy()
+    expect(document.querySelector('span').classList.contains('bar')).toBeTruthy()
+    expect(document.querySelector('span').classList.contains('baz')).toBeFalsy()
+
+    document.querySelector('button').click()
+
+    await wait(() => {
+        expect(document.querySelector('span').classList.contains('foo')).toBeTruthy()
+        expect(document.querySelector('span').classList.contains('bar')).toBeTruthy()
+        expect(document.querySelector('span').classList.contains('baz')).toBeTruthy()
+    })
+
+    document.querySelector('button').click()
+
+    await wait(() => {
+        expect(document.querySelector('span').classList.contains('foo')).toBeTruthy()
+        expect(document.querySelector('span').classList.contains('bar')).toBeTruthy()
+        expect(document.querySelector('span').classList.contains('baz')).toBeFalsy()
+    })
+})
+
 test('class attribute bindings are removed by object syntax', async () => {
     document.body.innerHTML = `
         <div x-data="{ isOn: false }">
@@ -29,6 +88,18 @@ test('class attribute bindings are removed by object syntax', async () => {
     expect(document.querySelector('span').classList.contains('foo')).toBeFalsy()
 })
 
+test('class attribute bindings are added by string syntax', async () => {
+    document.body.innerHTML = `
+        <div x-data="{ initialClass: 'foo' }">
+            <span x-bind:class="initialClass"></span>
+        </div>
+    `
+
+    Alpine.start()
+
+    expect(document.querySelector('span').classList.contains('foo')).toBeTruthy()
+})
+
 test('class attribute bindings are added by object syntax', async () => {
     document.body.innerHTML = `
         <div x-data="{ isOn: true }">
@@ -79,18 +150,6 @@ test('class attribute bindings are added by nested object syntax', async () => {
     expect(document.querySelector('span').classList.contains('foo')).toBeTruthy()
 })
 
-test('class attribute bindings are removed by array syntax', async () => {
-    document.body.innerHTML = `
-        <div x-data="{}">
-            <span class="foo" x-bind:class="[]"></span>
-        </div>
-    `
-
-    Alpine.start()
-
-    expect(document.querySelector('span').classList.contains('foo')).toBeFalsy()
-})
-
 test('class attribute bindings are added by array syntax', async () => {
     document.body.innerHTML = `
         <div x-data="{}">

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