|
@@ -10,90 +10,84 @@ import { isCloning } from '../clone'
|
|
directive('model', (el, { modifiers, expression }, { effect, cleanup }) => {
|
|
directive('model', (el, { modifiers, expression }, { effect, cleanup }) => {
|
|
let scopeTarget = el
|
|
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
|
|
// 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.
|
|
// 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)
|
|
|| ['checkbox', 'radio'].includes(el.type)
|
|
|| modifiers.includes('lazy')
|
|
|| 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
|
|
// 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
|
|
// mutation observer handles initializing the x-model directive already when
|
|
// the element is inserted into the DOM. Otherwise we register it twice.
|
|
// 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()))
|
|
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
|
|
// If the input/select/textarea element is linked to a form
|
|
// we listen for the reset event on the parent form (the event
|
|
// we listen for the reset event on the parent form (the event
|
|
// does not trigger on the single inputs) and update
|
|
// 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) => {
|
|
let removeResetListener = on(el.form, 'reset', [], (e) => {
|
|
nextTick(() => el._x_model && el._x_model.set(el.value))
|
|
nextTick(() => el._x_model && el._x_model.set(el.value))
|
|
})
|
|
})
|
|
|
|
+
|
|
cleanup(() => removeResetListener())
|
|
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) {
|
|
function getInputValue(el, modifiers, event, currentValue) {
|
|
return mutateDom(() => {
|
|
return mutateDom(() => {
|