1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
|
import { evaluateLater } from '../evaluator'
import { directive } from '../directives'
import { mutateDom } from '../mutation'
import bind from '../utils/bind'
import on from '../utils/on'
directive('model', (el, { modifiers, expression }, { effect, cleanup }) => {
let evaluate = evaluateLater(el, expression)
let assignmentExpression = `${expression} = rightSideOfExpression($event, ${expression})`
let evaluateAssignment = evaluateLater(el, assignmentExpression)
// 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')
|| ['checkbox', 'radio'].includes(el.type)
|| modifiers.includes('lazy')
? 'change' : 'input'
let assigmentFunction = generateAssignmentFunction(el, modifiers, expression)
let removeListener = on(el, event, modifiers, (e) => {
evaluateAssignment(() => {}, { scope: {
'$event': e,
rightSideOfExpression: assigmentFunction
}})
})
// 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']())
// Allow programmatic overiding of x-model.
let evaluateSetModel = evaluateLater(el, `${expression} = __placeholder`)
el._x_model = {
get() {
let result
evaluate(value => result = value)
return result
},
set(value) {
evaluateSetModel(() => {}, { scope: { '__placeholder': 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
})
}
effect(() => {
// Don't modify the value of the input if it's focused.
if (modifiers.includes('unintrusive') && document.activeElement.isSameNode(el)) return
el._x_forceModelUpdate()
})
})
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
})
} else {
let rawValue = event.target.value
return modifiers.includes('number')
? safeParseNumber(rawValue)
: (modifiers.includes('trim') ? rawValue.trim() : rawValue)
}
})
}
}
function safeParseNumber(rawValue) {
let number = rawValue ? parseFloat(rawValue) : null
return isNumeric(number) ? number : rawValue
}
function checkedAttrLooseCompare(valueA, valueB) {
return valueA == valueB
}
function isNumeric(subject){
return ! Array.isArray(subject) && ! isNaN(subject)
}
|