Caleb Porzio 2 years ago
parent
commit
6f52c8ce3f

+ 1 - 0
packages/alpinejs/src/directives.js

@@ -193,6 +193,7 @@ let directiveOrder = [
     // that I don't have to manually add things like "tabs"
     // to the order list...
     'tabs',
+    'switch',
     'bind',
     'init',
     'for',

+ 3 - 1
packages/alpinejs/src/directives/x-bind.js

@@ -26,7 +26,9 @@ directive('bind', (el, { value, modifiers, expression, original }, { effect }) =
 
     effect(() => evaluate(result => {
         // If nested object key is undefined, set the default value to empty string.
-        if (result === undefined && expression.match(/\./)) result = ''
+        if (result === undefined && typeof expression === 'string' && expression.match(/\./)) {
+            result = ''
+        }
 
         mutateDom(() => bind(el, value, result, modifiers))
     }))

+ 54 - 25
packages/ui/src/switch.js

@@ -31,43 +31,53 @@ function handleGroup(el, Alpine) {
     })
 }
 
-function handleLabel(el, Alpine) {
-    Alpine.bind(el, {
-        'x-init'() { this.$data.__hasLabel = true },
-        ':id'() { return this.$id('alpine-switch-label') },
-        '@click'() {
-            this.$data.__switchEl.click()
-            this.$data.__switchEl.focus({ preventScroll: true })
-        },
-    })
-}
-
-function handleDescription(el, Alpine) {
-    Alpine.bind(el, {
-        'x-init'() { this.$data.__hasDescription = true },
-        ':id'() { return this.$id('alpine-switch-description') },
-    })
-}
-
 function handleRoot(el, Alpine) {
     Alpine.bind(el, {
+        'x-modelable': '__value',
         'x-data'() {
             return {
                 init() {
-                    // Need the "microtask" here so that x-model has a chance to initialize.
                     queueMicrotask(() => {
-                        // Set our internal "selected" every time the x-modeled value changes.
-                        Alpine.effect(() => {
-                            this.__value = this.$el._x_model.get()
-                        })
+                        this.__value = Alpine.bound(this.$el, 'default-checked', false)
+                        this.__inputName = Alpine.bound(this.$el, 'name', false)
+                        this.__inputValue = Alpine.bound(this.$el, 'value', 'on')
+                        this.__inputId = Date.now()
                     })
                 },
                 __value: undefined,
+                __inputName: undefined,
+                __inputValue: undefined,
+                __inputId: undefined,
                 __toggle() {
-                    this.$el._x_model.set(!this.__value)
+                    this.__value = ! this.__value;
                 },
             }
         },
+        'x-effect'() {
+            let value = this.__value
+
+            // Only render a hidden input if the "name" prop is passed...
+            if (! this.__inputName) return
+
+            // First remove a previously appended hidden input (if it exists)...
+            let nextEl = this.$el.nextElementSibling
+            if (nextEl && String(nextEl.id) === String(this.__inputId)) {
+                nextEl.remove()
+            }
+
+            // If the value is true, create the input and append it, otherwise,
+            // we already removed it in the previous step...
+            if (value) {
+                let input = document.createElement('input')
+
+                input.type = 'hidden'
+                input.value = this.__inputValue
+                input.name = this.__inputName
+                input.id = this.__inputId
+
+                this.$el.after(input)
+            }
+        },
         'x-init'() {
             if (this.$el.tagName.toLowerCase() === 'button' && !this.$el.hasAttribute('type')) this.$el.type = 'button'
             this.$data.__switchEl = this.$el
@@ -76,7 +86,7 @@ function handleRoot(el, Alpine) {
         'tabindex': "0",
         ':aria-checked'() { return !!this.__value },
         ':aria-labelledby'() { return this.$data.__hasLabel && this.$id('alpine-switch-label') },
-        ':aria-describedby'() { return this.$data.__hasDescription && this.$id('alpine-switch-description') },
+        ':aria-describedby'() { return this.$data.true__hasDescription && this.$id('alpine-switch-description') },
         '@click.prevent'() { this.__toggle() },
         '@keyup'(e) {
             if (e.key !== 'Tab') e.preventDefault()
@@ -86,3 +96,22 @@ function handleRoot(el, Alpine) {
         '@keypress.prevent'() { },
     })
 }
+
+function handleLabel(el, Alpine) {
+    Alpine.bind(el, {
+        'x-init'() { this.$data.__hasLabel = true },
+        ':id'() { return this.$id('alpine-switch-label') },
+        '@click'() {
+            this.$data.__switchEl.click()
+            this.$data.__switchEl.focus({ preventScroll: true })
+        },
+    })
+}
+
+function handleDescription(el, Alpine) {
+    Alpine.bind(el, {
+        'x-init'() { this.$data.__hasDescription = true },
+        ':id'() { return this.$id('alpine-switch-description') },
+    })
+}
+

+ 13 - 13
tests/cypress/integration/plugins/ui/switch.spec.js

@@ -1,11 +1,11 @@
-import { beVisible, haveAttribute, haveText, html, notBeVisible, notExist, test } from '../../../utils'
+import { beVisible, haveAttribute, haveClasses, haveText, html, notBeVisible, notExist, test } from '../../../utils'
 
 test('has accessibility attributes',
     [html`
         <div x-data="{ checked: false }">
             <article x-switch:group>
                 <label x-switch:label>Enable notifications</label>
-                <span description" x-switch:description>A description of the switch.</span>
+                <span description x-switch:description>A description of the switch.</span>
 
                 <button x-switch x-model="checked">Enable Notifications</button>
             </article>
@@ -13,7 +13,7 @@ test('has accessibility attributes',
     `],
     ({ get }) => {
         get('label').should(haveAttribute('id', 'alpine-switch-label-1'))
-        get('[description"]').should(haveAttribute('id', 'alpine-switch-description-1'))
+        get('[description]').should(haveAttribute('id', 'alpine-switch-description-1'))
         get('button').should(haveAttribute('type', 'button'))
         get('button').should(haveAttribute('aria-labelledby', 'alpine-switch-label-1'))
         get('button').should(haveAttribute('aria-describedby', 'alpine-switch-description-1'))
@@ -44,23 +44,20 @@ test('works with x-model',
     },
 )
 
-
 test('works with internal state/$switch.isChecked',
     [html`
-        <div>
-            <button x-switch>Enable notifications</button>
-
-            <article x-show="$switch.isChecked">
-                Notifications are enabled.
-            </article>
+        <div x-data>
+            <button x-switch x-bind:class="$switch.isChecked ? 'foo' : 'bar'">
+                Enable notifications
+            </button>
         </div>
     `],
     ({ get }) => {
-        get('article').should(notBeVisible())
+        get('button').should(haveClasses(['bar']))
         get('button').click()
-        get('article').should(beVisible())
+        get('button').should(haveClasses(['foo']))
         get('button').click()
-        get('article').should(notBeVisible())
+        get('button').should(haveClasses(['bar']))
     },
 )
 
@@ -85,3 +82,6 @@ test('pressing space toggles the switch',
         get('article').should(notBeVisible())
     },
 )
+
+// @todo: add test for default-checked
+// @todo: add test for hidden input