ソースを参照

Feature complete listbox (needs a little more cleanup)

Caleb Porzio 2 年 前
コミット
7925f3cdad
2 ファイル変更130 行追加5 行削除
  1. 109 0
      packages/ui/src/list-context.js
  2. 21 5
      packages/ui/src/listbox.js

+ 109 - 0
packages/ui/src/list-context.js

@@ -1,3 +1,4 @@
+import Alpine from "../../alpinejs/src/alpine"
 
 export function generateContext(multiple) {
     return {
@@ -165,6 +166,38 @@ export function generateContext(multiple) {
             return this.selectedKeys[0] ? this.values[this.selectedKeys[0]] : null
         },
 
+        selectValue(value, by) {
+            if (! by) by = (a, b) => a === b
+
+            if (typeof by === 'string') {
+                let property = by
+                by = (a, b) => a[property] === b[property]
+            }
+
+            if (multiple) {
+                // debugger
+                let keys = []
+
+                value.forEach(i => {
+                    for (let key in this.values) {
+                        if (by(this.values[key], i)) {
+                            if (! keys.includes(key)) {
+                                keys.push(key)
+                            }
+                        }
+                    }
+                })
+
+                this.selectExclusive(keys)
+            } else {
+                for (let key in this.values) {
+                    if (by(this.values[key], value)) {
+                        this.selectKey(key)
+                    }
+                }
+            }
+        },
+
         /**
          * Handle disabled keys...
          */
@@ -200,6 +233,28 @@ export function generateContext(multiple) {
             this.selectedKeys.push(key)
         },
 
+        selectExclusive(keys) {
+            // We can't just do this.selectedKeys = keys,
+            // because we need to preserve reactivity...
+
+            let toAdd = [...keys]
+
+            for (let i = 0; i < this.selectedKeys.length; i++) {
+                if (keys.includes(this.selectedKeys[i])) {
+                    delete toAdd[toAdd.indexOf(this.selectedKeys[i])]
+                    continue;
+                }
+
+                if (! keys.includes(this.selectedKeys[i])) {
+                    delete this.selectedKeys[i]
+                }
+            }
+
+            toAdd.forEach(i => {
+                this.selectedKeys.push(i)
+            })
+        },
+
         selectActive(key) {
             if (! this.activeKey) return
 
@@ -347,5 +402,59 @@ function keyByValue(object, value) {
     return Object.keys(object).find(key => object[key] === value)
 }
 
+export function renderHiddenInputs(el, name, value) {
+    // Create input elements...
+    let newInputs = generateInputs(name, value)
+
+    // Mark them for later tracking...
+    newInputs.forEach(i => i._x_hiddenInput = true)
+
+    // Mark them for Alpine ignoring...
+    newInputs.forEach(i => i._x_ignore = true)
+
+    // Gather old elements for removal...
+    let children = el.children
+
+    let oldInputs = []
+
+    for (let i = 0; i < children.length; i++) {
+        let child = children[i];
+
+        if (child._x_hiddenInput) oldInputs.push(child)
+        else break
+    }
+
+    // Remove old, and insert new ones into the DOM...
+    Alpine.mutateDom(() => {
+        oldInputs.forEach(i => i.remove())
+
+        newInputs.reverse().forEach(i => el.prepend(i))
+    })
+}
+
+function generateInputs(name, value, carry = []) {
+    if (isObjectOrArray(value)) {
+        for (let key in value) {
+            carry = carry.concat(
+                generateInputs(`${name}[${key}]`, value[key])
+            )
+        }
+    } else {
+        let el = document.createElement('input')
+        el.setAttribute('type', 'hidden')
+        el.setAttribute('name', name)
+        el.setAttribute('value', '' + value)
+
+        return [el]
+    }
+
+
+    return carry
+}
+
+function isObjectOrArray(subject) {
+    return typeof subject === 'object' && subject !== null
+}
+
 
 

+ 21 - 5
packages/ui/src/listbox.js

@@ -1,4 +1,4 @@
-import { generateContext } from './list-context'
+import { generateContext, renderHiddenInputs } from './list-context'
 
 export default function (Alpine) {
     Alpine.directive('listbox', (el, directive) => {
@@ -69,15 +69,31 @@ function handleRoot(el, Alpine) {
                 init() {
                     this.__isMultiple = Alpine.bound(el, 'multiple', false)
                     this.__isDisabled = Alpine.bound(el, 'disabled', false)
+                    this.__inputName = Alpine.bound(el, 'name', null)
+                    this.__compareBy = Alpine.bound(el, 'by')
 
                     this.__context = generateContext(this.__isMultiple)
 
-                    Alpine.effect(() => {
-                        this.__value = this.__context.selectedValueOrValues()
-                    })
-
                     queueMicrotask(() => {
                         this.__ready = true
+
+                        queueMicrotask(() => {
+                            let lastValueFingerprint = false
+
+                            Alpine.effect(() => {
+                                if (lastValueFingerprint !== false && lastValueFingerprint !== JSON.stringify(this.__value)) {
+                                    console.log('changed value, select keys')
+                                    this.__context.selectValue(this.__value, this.__compareBy)
+                                } else {
+                                    console.log('changed keys, select value')
+                                    this.__value = this.__context.selectedValueOrValues()
+                                }
+
+                                this.__inputName && renderHiddenInputs(this.$el, this.__inputName, this.__value)
+
+                                lastValueFingerprint = JSON.stringify(this.__value)
+                            })
+                        })
                     })
                 },
                 __open() {