x-model.js 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. import { evaluateLater } from '../evaluator'
  2. import { directive } from '../directives'
  3. import { mutateDom } from '../mutation'
  4. import { nextTick } from '../nextTick'
  5. import bind, { safeParseBoolean } from '../utils/bind'
  6. import on from '../utils/on'
  7. import { isCloning } from '../clone'
  8. directive('model', (el, { modifiers, expression }, { effect, cleanup }) => {
  9. let scopeTarget = el
  10. if (modifiers.includes('parent')) {
  11. scopeTarget = el.parentNode
  12. }
  13. let evaluateGet = evaluateLater(scopeTarget, expression)
  14. let evaluateSet
  15. if (typeof expression === 'string') {
  16. evaluateSet = evaluateLater(scopeTarget, `${expression} = __placeholder`)
  17. } else if (typeof expression === 'function' && typeof expression() === 'string') {
  18. evaluateSet = evaluateLater(scopeTarget, `${expression()} = __placeholder`)
  19. } else {
  20. evaluateSet = () => {}
  21. }
  22. let getValue = () => {
  23. let result
  24. evaluateGet(value => result = value)
  25. return isGetterSetter(result) ? result.get() : result
  26. }
  27. let setValue = value => {
  28. let result
  29. evaluateGet(value => result = value)
  30. if (isGetterSetter(result)) {
  31. result.set(value)
  32. } else {
  33. evaluateSet(() => {}, {
  34. scope: { '__placeholder': value }
  35. })
  36. }
  37. }
  38. if (typeof expression === 'string' && el.type === 'radio') {
  39. // Radio buttons only work properly when they share a name attribute.
  40. // People might assume we take care of that for them, because
  41. // they already set a shared "x-model" attribute.
  42. mutateDom(() => {
  43. if (! el.hasAttribute('name')) el.setAttribute('name', expression)
  44. })
  45. }
  46. // If the element we are binding to is a select, a radio, or checkbox
  47. // we'll listen for the change event instead of the "input" event.
  48. var event = (el.tagName.toLowerCase() === 'select')
  49. || ['checkbox', 'radio'].includes(el.type)
  50. || modifiers.includes('lazy')
  51. ? 'change' : 'input'
  52. // We only want to register the event listener when we're not cloning, since the
  53. // mutation observer handles initializing the x-model directive already when
  54. // the element is inserted into the DOM. Otherwise we register it twice.
  55. let removeListener = isCloning ? () => {} : on(el, event, modifiers, (e) => {
  56. setValue(getInputValue(el, modifiers, e, getValue()))
  57. })
  58. if (modifiers.includes('fill'))
  59. if ([undefined, null, ''].includes(getValue())
  60. || (el.type === 'checkbox' && Array.isArray(getValue()))
  61. || (el.tagName.toLowerCase() === 'select' && el.multiple)) {
  62. setValue(
  63. getInputValue(el, modifiers, { target: el }, getValue())
  64. );
  65. }
  66. // Register the listener removal callback on the element, so that
  67. // in addition to the cleanup function, x-modelable may call it.
  68. // Also, make this a keyed object if we decide to reintroduce
  69. // "named modelables" some time in a future Alpine version.
  70. if (! el._x_removeModelListeners) el._x_removeModelListeners = {}
  71. el._x_removeModelListeners['default'] = removeListener
  72. cleanup(() => el._x_removeModelListeners['default']())
  73. // If the input/select/textarea element is linked to a form
  74. // we listen for the reset event on the parent form (the event
  75. // does not trigger on the single inputs) and update
  76. // on nextTick so the page doesn't end up out of sync
  77. if (el.form) {
  78. let removeResetListener = on(el.form, 'reset', [], (e) => {
  79. nextTick(() => el._x_model && el._x_model.set(getInputValue(el, modifiers, { target: el }, getValue())))
  80. })
  81. cleanup(() => removeResetListener())
  82. }
  83. // Allow programmatic overriding of x-model.
  84. el._x_model = {
  85. get() {
  86. return getValue()
  87. },
  88. set(value) {
  89. setValue(value)
  90. },
  91. }
  92. el._x_forceModelUpdate = (value) => {
  93. // If nested model key is undefined, set the default value to empty string.
  94. if (value === undefined && typeof expression === 'string' && expression.match(/\./)) value = ''
  95. // @todo: This is nasty
  96. window.fromModel = true
  97. mutateDom(() => bind(el, 'value', value))
  98. delete window.fromModel
  99. }
  100. effect(() => {
  101. // We need to make sure we're always "getting" the value up front,
  102. // so that we don't run into a situation where because of the early
  103. // the reactive value isn't gotten and therefore disables future reactions.
  104. let value = getValue()
  105. // Don't modify the value of the input if it's focused.
  106. if (modifiers.includes('unintrusive') && document.activeElement.isSameNode(el)) return
  107. el._x_forceModelUpdate(value)
  108. })
  109. })
  110. function getInputValue(el, modifiers, event, currentValue) {
  111. return mutateDom(() => {
  112. // Check for event.detail due to an issue where IE11 handles other events as a CustomEvent.
  113. // Safari autofill triggers event as CustomEvent and assigns value to target
  114. // so we return event.target.value instead of event.detail
  115. if (event instanceof CustomEvent && event.detail !== undefined)
  116. return event.detail !== null && event.detail !== undefined ? event.detail : event.target.value
  117. else if (el.type === 'checkbox') {
  118. // If the data we are binding to is an array, toggle its value inside the array.
  119. if (Array.isArray(currentValue)) {
  120. let newValue = null;
  121. if (modifiers.includes('number')) {
  122. newValue = safeParseNumber(event.target.value)
  123. } else if (modifiers.includes('boolean')) {
  124. newValue = safeParseBoolean(event.target.value)
  125. } else {
  126. newValue = event.target.value
  127. }
  128. return event.target.checked
  129. ? (currentValue.includes(newValue) ? currentValue : currentValue.concat([newValue]))
  130. : currentValue.filter(el => ! checkedAttrLooseCompare(el, newValue));
  131. } else {
  132. return event.target.checked
  133. }
  134. } else if (el.tagName.toLowerCase() === 'select' && el.multiple) {
  135. if (modifiers.includes('number')) {
  136. return Array.from(event.target.selectedOptions).map(option => {
  137. let rawValue = option.value || option.text
  138. return safeParseNumber(rawValue)
  139. })
  140. } else if (modifiers.includes('boolean')) {
  141. return Array.from(event.target.selectedOptions).map(option => {
  142. let rawValue = option.value || option.text
  143. return safeParseBoolean(rawValue)
  144. })
  145. }
  146. return Array.from(event.target.selectedOptions).map(option => {
  147. return option.value || option.text
  148. })
  149. } else {
  150. let newValue
  151. if (el.type === 'radio') {
  152. if (event.target.checked) {
  153. newValue = event.target.value
  154. } else {
  155. newValue = currentValue
  156. }
  157. } else {
  158. newValue = event.target.value
  159. }
  160. if (modifiers.includes('number')) {
  161. return safeParseNumber(newValue)
  162. } else if (modifiers.includes('boolean')) {
  163. return safeParseBoolean(newValue)
  164. } else if (modifiers.includes('trim')) {
  165. return newValue.trim()
  166. } else {
  167. return newValue
  168. }
  169. }
  170. })
  171. }
  172. function safeParseNumber(rawValue) {
  173. let number = rawValue ? parseFloat(rawValue) : null
  174. return isNumeric(number) ? number : rawValue
  175. }
  176. function checkedAttrLooseCompare(valueA, valueB) {
  177. return valueA == valueB
  178. }
  179. function isNumeric(subject){
  180. return ! Array.isArray(subject) && ! isNaN(subject)
  181. }
  182. function isGetterSetter(value) {
  183. return value !== null && typeof value === 'object' && typeof value.get === 'function' && typeof value.set === 'function'
  184. }