|
@@ -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'
|
|
|
+}
|