Caleb Porzio пре 3 година
родитељ
комит
ab6b328241

+ 37 - 2
packages/alpinejs/src/binds.js

@@ -1,8 +1,15 @@
+import { attributesOnly, directives } from "./directives"
 
 let binds = {}
 
-export function bind(name, object) {
-    binds[name] = typeof object !== 'function' ? () => object : object
+export function bind(name, bindings) {
+    let getBindings = typeof bindings !== 'function' ? () => bindings : bindings
+
+    if (name instanceof Element) {
+        applyBindingsObject(name, getBindings())
+    } else {
+        binds[name] = getBindings
+    }
 }
 
 export function injectBindingProviders(obj) {
@@ -18,3 +25,31 @@ export function injectBindingProviders(obj) {
 
     return obj
 }
+
+export function applyBindingsObject(el, obj, original) {
+    let cleanupRunners = []
+
+    while (cleanupRunners.length) cleanupRunners.pop()()
+
+    let attributes = Object.entries(obj).map(([name, value]) => ({ name, value }))
+
+    let staticAttributes = attributesOnly(attributes)
+
+    // Handle binding normal HTML attributes (non-Alpine directives).
+    attributes = attributes.map(attribute => {
+        if (staticAttributes.find(attr => attr.name === attribute.name)) {
+            return {
+                name: `x-bind:${attribute.name}`,
+                value: `"${attribute.value}"`,
+            }
+        }
+
+        return attribute
+    })
+
+    directives(el, attributes, original).map(handle => {
+        cleanupRunners.push(handle.runCleanups)
+
+        handle()
+    })
+}

+ 2 - 2
packages/alpinejs/src/directives/x-bind.js

@@ -26,7 +26,7 @@ directive('bind', (el, { value, modifiers, expression, original }, { effect }) =
 function applyBindingsObject(el, expression, original, effect) {
     let bindingProviders = {}
     injectBindingProviders(bindingProviders)
-   
+
     let getBindings = evaluateLater(el, expression)
 
     let cleanupRunners = []
@@ -37,7 +37,7 @@ function applyBindingsObject(el, expression, original, effect) {
         let attributes = Object.entries(bindings).map(([name, value]) => ({ name, value }))
 
         let staticAttributes = attributesOnly(attributes)
-        
+
         // Handle binding normal HTML attributes (non-Alpine directives).
         attributes = attributes.map(attribute => {
             if (staticAttributes.find(attr => attr.name === attribute.name)) {

+ 87 - 64
packages/alpinejs/src/directives/x-model.js

@@ -3,11 +3,50 @@ import { directive } from '../directives'
 import { mutateDom } from '../mutation'
 import bind from '../utils/bind'
 import on from '../utils/on'
+import { warn } from '../utils/warn'
 
 directive('model', (el, { modifiers, expression }, { effect, cleanup }) => {
-    let evaluate = evaluateLater(el, expression)
-    let assignmentExpression = `${expression} = rightSideOfExpression($event, ${expression})`
-    let evaluateAssignment = evaluateLater(el, assignmentExpression)
+    let evaluateGet = evaluateLater(el, expression)
+    let evaluateSet
+
+    if (typeof expression === 'string') {
+        evaluateSet = evaluateLater(el, `${expression} = __placeholder`)
+    } else if (typeof expression === 'function' && typeof expression() === 'string') {
+        evaluateSet = evaluateLater(el, `${expression()} = __placeholder`)
+    } else {
+        evaluateSet = () => {}
+    }
+
+    let getValue = () => {
+        let result
+
+        evaluateGet(value => result = value)
+
+        return isGetterSetter(result) ? result.get() : result
+    }
+
+    let setValue = value => {
+        let result
+
+        evaluateGet(value => result = value)
+
+        if (isGetterSetter(result)) {
+            result.set(value)
+        } else {
+            evaluateSet(() => {}, {
+                scope: { '__placeholder': value }
+            })
+        }
+    }
+
+    if (typeof expression === 'string' && el.type === 'radio') {
+        // Radio buttons only work properly when they share a name attribute.
+        // People might assume we take care of that for them, because
+        // they already set a shared "x-model" attribute.
+        mutateDom(() => {
+            if (! el.hasAttribute('name')) el.setAttribute('name', expression)
+        })
+    }
 
     // If the element we are binding to is a select, a radio, or checkbox
     // we'll listen for the change event instead of the "input" event.
@@ -16,13 +55,8 @@ directive('model', (el, { modifiers, expression }, { effect, cleanup }) => {
         || modifiers.includes('lazy')
             ? 'change' : 'input'
 
-    let assigmentFunction = generateAssignmentFunction(el, modifiers, expression)
-
     let removeListener = on(el, event, modifiers, (e) => {
-        evaluateAssignment(() => {}, { scope: {
-            '$event': e,
-            rightSideOfExpression: assigmentFunction
-        }})
+        setValue(getInputValue(el, modifiers, e, getValue()))
     })
 
     // Register the listener removal callback on the element, so that
@@ -35,28 +69,24 @@ directive('model', (el, { modifiers, expression }, { effect, cleanup }) => {
     cleanup(() => el._x_removeModelListeners['default']())
 
     // Allow programmatic overiding of x-model.
-    let evaluateSetModel = evaluateLater(el, `${expression} = __placeholder`)
     el._x_model = {
         get() {
-            let result
-            evaluate(value => result = value)
-            return result
+            return getValue()
         },
         set(value) {
-            evaluateSetModel(() => {}, { scope: { '__placeholder': value }})
+            setValue(value)
         },
     }
 
     el._x_forceModelUpdate = () => {
-        evaluate(value => {
-            // If nested model key is undefined, set the default value to empty string.
-            if (value === undefined && expression.match(/\./)) value = ''
-
-            // @todo: This is nasty
-            window.fromModel = true
-            mutateDom(() => bind(el, 'value', value))
-            delete window.fromModel
-        })
+        let value = getValue()
+        // If nested model key is undefined, set the default value to empty string.
+        if (value === undefined && typeof expression === 'string' && expression.match(/\./)) value = ''
+
+        // @todo: This is nasty
+        window.fromModel = true
+        mutateDom(() => bind(el, 'value', value))
+        delete window.fromModel
     }
 
     effect(() => {
@@ -67,49 +97,38 @@ directive('model', (el, { modifiers, expression }, { effect, cleanup }) => {
     })
 })
 
-function generateAssignmentFunction(el, modifiers, expression) {
-    if (el.type === 'radio') {
-        // Radio buttons only work properly when they share a name attribute.
-        // People might assume we take care of that for them, because
-        // they already set a shared "x-model" attribute.
-        mutateDom(() => {
-            if (! el.hasAttribute('name')) el.setAttribute('name', expression)
-        })
-    }
-
-    return (event, currentValue) => {
-        return mutateDom(() => {
-            // Check for event.detail due to an issue where IE11 handles other events as a CustomEvent.
-            // Safari autofill triggers event as CustomEvent and assigns value to target
-            // so we return event.target.value instead of event.detail
-            if (event instanceof CustomEvent && event.detail !== undefined) {
-                return event.detail || event.target.value
-            } else if (el.type === 'checkbox') {
-                // If the data we are binding to is an array, toggle its value inside the array.
-                if (Array.isArray(currentValue)) {
-                    let newValue = modifiers.includes('number') ? safeParseNumber(event.target.value) : event.target.value
-
-                    return event.target.checked ? currentValue.concat([newValue]) : currentValue.filter(el => ! checkedAttrLooseCompare(el, newValue))
-                } else {
-                    return event.target.checked
-                }
-            } else if (el.tagName.toLowerCase() === 'select' && el.multiple) {
-                return modifiers.includes('number')
-                    ? Array.from(event.target.selectedOptions).map(option => {
-                        let rawValue = option.value || option.text
-                        return safeParseNumber(rawValue)
-                    })
-                    : Array.from(event.target.selectedOptions).map(option => {
-                        return option.value || option.text
-                    })
+function getInputValue(el, modifiers, event, currentValue) {
+    return mutateDom(() => {
+        // Check for event.detail due to an issue where IE11 handles other events as a CustomEvent.
+        // Safari autofill triggers event as CustomEvent and assigns value to target
+        // so we return event.target.value instead of event.detail
+        if (event instanceof CustomEvent && event.detail !== undefined) {
+            return event.detail || event.target.value
+        } else if (el.type === 'checkbox') {
+            // If the data we are binding to is an array, toggle its value inside the array.
+            if (Array.isArray(currentValue)) {
+                let newValue = modifiers.includes('number') ? safeParseNumber(event.target.value) : event.target.value
+
+                return event.target.checked ? currentValue.concat([newValue]) : currentValue.filter(el => ! checkedAttrLooseCompare(el, newValue))
             } else {
-                let rawValue = event.target.value
-                return modifiers.includes('number')
-                    ? safeParseNumber(rawValue)
-                    : (modifiers.includes('trim') ? rawValue.trim() : rawValue)
+                return event.target.checked
             }
-        })
-    }
+        } else if (el.tagName.toLowerCase() === 'select' && el.multiple) {
+            return modifiers.includes('number')
+                ? Array.from(event.target.selectedOptions).map(option => {
+                    let rawValue = option.value || option.text
+                    return safeParseNumber(rawValue)
+                })
+                : Array.from(event.target.selectedOptions).map(option => {
+                    return option.value || option.text
+                })
+        } else {
+            let rawValue = event.target.value
+            return modifiers.includes('number')
+                ? safeParseNumber(rawValue)
+                : (modifiers.includes('trim') ? rawValue.trim() : rawValue)
+        }
+    })
 }
 
 function safeParseNumber(rawValue) {
@@ -125,3 +144,7 @@ function checkedAttrLooseCompare(valueA, valueB) {
 function isNumeric(subject){
     return ! Array.isArray(subject) && ! isNaN(subject)
 }
+
+function isGetterSetter(value) {
+    return typeof value === 'object' && typeof value.get === 'function' && typeof value.set === 'function'
+}