Browse Source

Refactory x-model

Caleb Porzio 1 year ago
parent
commit
dd5e5460c0
1 changed files with 105 additions and 84 deletions
  1. 105 84
      packages/alpinejs/src/directives/x-model.js

+ 105 - 84
packages/alpinejs/src/directives/x-model.js

@@ -10,90 +10,84 @@ import { isCloning } from '../clone'
 directive('model', (el, { modifiers, expression }, { effect, cleanup }) => {
     let scopeTarget = el
 
-    if (modifiers.includes('parent')) {
-        scopeTarget = el.parentNode
-    }
+    if (modifiers.includes('parent')) scopeTarget = el.parentNode
 
-    let evaluateGet = evaluateLater(scopeTarget, expression)
-    let evaluateSet
+    let [ getValue, setValue ] = generateGetAndSet(evaluateLater, scopeTarget, expression)
 
-    if (typeof expression === 'string') {
-        evaluateSet = evaluateLater(scopeTarget, `${expression} = __placeholder`)
-    } else if (typeof expression === 'function' && typeof expression() === 'string') {
-        evaluateSet = evaluateLater(scopeTarget, `${expression()} = __placeholder`)
-    } else {
-        evaluateSet = () => {}
-    }
+    // Allow programmatic overriding of x-model.
+    el._x_model = { get() { return getValue() }, set(value) { setValue(value) } }
 
-    let getResult = () => {
-        let result
+    initializeRadioInput(expression, el)
 
-        evaluateGet(value => result = value)
+    let event = determineListenerEvent(el, modifiers)
 
-        // The following code prevents an infinite loop when using:
-        // x-model="$model" by retreiving an x-model higher in the tree...
-        if (typeof result === 'object' && result !== null && result._x_modelAccessor) {
-            return result._x_modelAccessor.closest
-        }
+    let removeListener = registerInputEventListener(el, event, modifiers, setValue, getValue)
 
-        return result
-    }
+    modifiers.includes('fill') && initializeFilledInput(el, event,getValue)
 
-    let getValue = () => {
-        let result = getResult()
+    // Register the listener removal callback on the element, so that
+    // in addition to the cleanup function, x-modelable may call it.
+    // Also, make this a keyed object if we decide to reintroduce
+    // "named modelables" some time in a future Alpine version.
+    if (! el._x_removeModelListeners) el._x_removeModelListeners = {}
+    el._x_removeModelListeners['default'] = removeListener
 
-        return isGetterSetter(result) ? result.get() : result
-    }
+    cleanup(() => el._x_removeModelListeners['default']())
 
-    let setValue = value => {
-        let result = getResult()
+    handleFormResets(el, cleanup)
 
-        if (isGetterSetter(result)) {
-            result.set(value)
-        } else {
-            evaluateSet(() => {}, {
-                scope: { '__placeholder': value }
-            })
-        }
-    }
+    el._x_forceModelUpdate = (value) => {
+        // If nested model key is undefined, set the default value to empty string.
+        if (value === undefined && typeof expression === 'string' && expression.match(/\./)) 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)
-        })
+        // @todo: This is nasty
+        window.fromModel = true
+        mutateDom(() => bind(el, 'value', value))
+        delete window.fromModel
     }
 
+    effect(() => {
+        // We need to make sure we're always "getting" the value up front,
+        // so that we don't run into a situation where because of the early
+        // the reactive value isn't gotten and therefore disables future reactions.
+        let value = getValue()
+
+        // Don't modify the value of the input if it's focused.
+        if (modifiers.includes('unintrusive') && document.activeElement.isSameNode(el)) return
+
+        el._x_forceModelUpdate(value)
+    })
+})
+
+function initializeFilledInput(el, event, getValue) {
+    if ([null, ''].includes(getValue())
+        || (el.type === 'checkbox' && Array.isArray(getValue()))) {
+            el.dispatchEvent(new Event(event, {}));
+        }
+}
+
+function determineListenerEvent(el, modifiers) {
     // 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.
-    var event = (el.tagName.toLowerCase() === 'select')
+
+    return (el.tagName.toLowerCase() === 'select')
         || ['checkbox', 'radio'].includes(el.type)
         || modifiers.includes('lazy')
-            ? 'change' : 'input'
+        ? 'change' : 'input'
+}
 
+function registerInputEventListener(el, event, modifiers, setValue, getValue) {
     // We only want to register the event listener when we're not cloning, since the
     // mutation observer handles initializing the x-model directive already when
     // the element is inserted into the DOM. Otherwise we register it twice.
-    let removeListener = isCloning ? () => {} : on(el, event, modifiers, (e) => {
+    let removeListener = isCloning ? () => { } : on(el, event, modifiers, (e) => {
         setValue(getInputValue(el, modifiers, e, getValue()))
     })
 
-    if (modifiers.includes('fill'))
-        if ([null, ''].includes(getValue())
-            || (el.type === 'checkbox' && Array.isArray(getValue()))) {
-            el.dispatchEvent(new Event(event, {}));
-    }
-    // Register the listener removal callback on the element, so that
-    // in addition to the cleanup function, x-modelable may call it.
-    // Also, make this a keyed object if we decide to reintroduce
-    // "named modelables" some time in a future Alpine version.
-    if (! el._x_removeModelListeners) el._x_removeModelListeners = {}
-    el._x_removeModelListeners['default'] = removeListener
-
-    cleanup(() => el._x_removeModelListeners['default']())
+    return removeListener
+}
 
+function handleFormResets(el, cleanup) {
     // If the input/select/textarea element is linked to a form
     // we listen for the reset event on the parent form (the event
     // does not trigger on the single inputs) and update
@@ -102,41 +96,68 @@ directive('model', (el, { modifiers, expression }, { effect, cleanup }) => {
         let removeResetListener = on(el.form, 'reset', [], (e) => {
             nextTick(() => el._x_model && el._x_model.set(el.value))
         })
+
         cleanup(() => removeResetListener())
     }
+}
 
-    // Allow programmatic overriding of x-model.
-    el._x_model = {
-        get() {
-            return getValue()
-        },
-        set(value) {
-            setValue(value)
-        },
+function initializeRadioInput(expression, el) {
+    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)
+        })
     }
+}
 
-    el._x_forceModelUpdate = (value) => {
-        // If nested model key is undefined, set the default value to empty string.
-        if (value === undefined && typeof expression === 'string' && expression.match(/\./)) value = ''
+function generateGetAndSet(evaluateLater, scopeTarget, expression) {
+    let evaluateGet = evaluateLater(scopeTarget, expression)
+    let evaluateSet
 
-        // @todo: This is nasty
-        window.fromModel = true
-        mutateDom(() => bind(el, 'value', value))
-        delete window.fromModel
+    if (typeof expression === 'string') {
+        evaluateSet = evaluateLater(scopeTarget, `${expression} = __placeholder`)
+    } else if (typeof expression === 'function' && typeof expression() === 'string') {
+        evaluateSet = evaluateLater(scopeTarget, `${expression()} = __placeholder`)
+    } else {
+        evaluateSet = () => { }
     }
 
-    effect(() => {
-        // We need to make sure we're always "getting" the value up front,
-        // so that we don't run into a situation where because of the early
-        // the reactive value isn't gotten and therefore disables future reactions.
-        let value = getValue()
+    let getResult = () => {
+        let result
 
-        // Don't modify the value of the input if it's focused.
-        if (modifiers.includes('unintrusive') && document.activeElement.isSameNode(el)) return
+        evaluateGet(value => result = value)
 
-        el._x_forceModelUpdate(value)
-    })
-})
+        // The following code prevents an infinite loop when using:
+        // x-model="$model" by retreiving an x-model higher in the tree...
+        if (typeof result === 'object' && result !== null && result._x_modelAccessor) {
+            return result._x_modelAccessor.closest
+        }
+
+        return result
+    }
+
+    let getValue = () => {
+        let result = getResult()
+
+        return isGetterSetter(result) ? result.get() : result
+    }
+
+    let setValue = value => {
+        let result = getResult()
+
+        if (isGetterSetter(result)) {
+            result.set(value)
+        } else {
+            evaluateSet(() => { }, {
+                scope: { '__placeholder': value }
+            })
+        }
+    }
+
+    return [ getValue, setValue ]
+}
 
 function getInputValue(el, modifiers, event, currentValue) {
     return mutateDom(() => {