prop.ts 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. import {
  2. type NormalizedStyle,
  3. canSetValueDirectly,
  4. isOn,
  5. isString,
  6. normalizeClass,
  7. normalizeStyle,
  8. parseStringStyle,
  9. toDisplayString,
  10. } from '@vue/shared'
  11. import { on } from './event'
  12. import {
  13. mergeProps,
  14. patchStyle as setStyle,
  15. shouldSetAsProp,
  16. warn,
  17. } from '@vue/runtime-dom'
  18. type TargetElement = Element & {
  19. $html?: string
  20. $cls?: string
  21. $clsi?: string
  22. $sty?: NormalizedStyle
  23. $styi?: NormalizedStyle
  24. $dprops?: Record<string, any>
  25. }
  26. export function setText(el: Node & { $txt?: string }, ...values: any[]): void {
  27. const value = values.map(v => toDisplayString(v)).join('')
  28. if (el.$txt !== value) {
  29. el.textContent = el.$txt = value
  30. }
  31. }
  32. export function setHtml(el: TargetElement, value: any): void {
  33. value = value == null ? '' : value
  34. if (el.$html !== value) {
  35. el.innerHTML = el.$html = value
  36. }
  37. }
  38. export function setClass(el: TargetElement, value: any): void {
  39. if ((value = normalizeClass(value)) !== el.$cls) {
  40. el.className = el.$cls = value
  41. }
  42. }
  43. /**
  44. * A version of setClass that does not overwrite pre-existing classes.
  45. * Used on single root elements so it can patch class independent of fallthrough
  46. * attributes.
  47. */
  48. export function setClassIncremental(el: TargetElement, value: any): void {
  49. const prev = el.$clsi
  50. if ((value = normalizeClass(value)) !== prev) {
  51. el.$clsi = value
  52. const nextList = value.split(/\s+/)
  53. el.classList.add(...nextList)
  54. if (prev) {
  55. for (const cls of prev.split(/\s+/)) {
  56. if (!nextList.includes(cls)) el.classList.remove(cls)
  57. }
  58. }
  59. }
  60. }
  61. /**
  62. * Reuse from runtime-dom
  63. */
  64. export { setStyle }
  65. /**
  66. * A version of setStyle that does not overwrite pre-existing styles.
  67. * Used on single root elements so it can patch class independent of fallthrough
  68. * attributes.
  69. */
  70. export function setStyleIncremental(el: TargetElement, value: any): void {
  71. const prev = el.$styi
  72. value = el.$styi = isString(value)
  73. ? parseStringStyle(value)
  74. : ((normalizeStyle(value) || {}) as NormalizedStyle)
  75. setStyle(el, prev, value)
  76. }
  77. export function setAttr(el: any, key: string, value: any): void {
  78. if (value !== el[`$${key}`]) {
  79. el[`$${key}`] = value
  80. if (value != null) {
  81. el.setAttribute(key, value)
  82. } else {
  83. el.removeAttribute(key)
  84. }
  85. }
  86. }
  87. export function setValue(
  88. el: Element & { value?: string; _value?: any },
  89. value: any,
  90. ): void {
  91. // store value as _value as well since
  92. // non-string values will be stringified.
  93. el._value = value
  94. // #4956: <option> value will fallback to its text content so we need to
  95. // compare against its attribute value instead.
  96. const oldValue = el.tagName === 'OPTION' ? el.getAttribute('value') : el.value
  97. const newValue = value == null ? '' : value
  98. if (oldValue !== newValue) {
  99. el.value = newValue
  100. }
  101. if (value == null) {
  102. el.removeAttribute('value')
  103. }
  104. }
  105. export function setDOMProp(el: any, key: string, value: any): void {
  106. const prev = el[key]
  107. if (value === prev) {
  108. return
  109. }
  110. let needRemove = false
  111. if (value === '' || value == null) {
  112. const type = typeof prev
  113. if (value == null && type === 'string') {
  114. // e.g. <div :id="null">
  115. value = ''
  116. needRemove = true
  117. } else if (type === 'number') {
  118. // e.g. <img :width="null">
  119. value = 0
  120. needRemove = true
  121. }
  122. }
  123. // some properties perform value validation and throw,
  124. // some properties has getter, no setter, will error in 'use strict'
  125. // eg. <select :type="null"></select> <select :willValidate="null"></select>
  126. try {
  127. el[key] = value
  128. } catch (e: any) {
  129. // do not warn if value is auto-coerced from nullish values
  130. if (__DEV__ && !needRemove) {
  131. warn(
  132. `Failed setting prop "${key}" on <${el.tagName.toLowerCase()}>: ` +
  133. `value ${value} is invalid.`,
  134. e,
  135. )
  136. }
  137. }
  138. needRemove && el.removeAttribute(key)
  139. }
  140. export function setDynamicProps(
  141. el: TargetElement,
  142. args: any[],
  143. root = false,
  144. ): void {
  145. const props = args.length > 1 ? mergeProps(...args) : args[0]
  146. const oldProps = el.$dprops
  147. if (oldProps) {
  148. for (const key in oldProps) {
  149. // TODO should these keys be allowed as dynamic keys? The current logic of the runtime-core will throw an error
  150. if (key === 'textContent' || key === 'innerHTML') {
  151. continue
  152. }
  153. const oldValue = oldProps[key]
  154. const hasNewValue = props[key] || props['.' + key] || props['^' + key]
  155. if (oldValue && !hasNewValue) {
  156. setDynamicProp(el, key, oldValue, null, root)
  157. }
  158. }
  159. }
  160. const prev = (el.$dprops = Object.create(null))
  161. for (const key in props) {
  162. setDynamicProp(
  163. el,
  164. key,
  165. oldProps ? oldProps[key] : undefined,
  166. (prev[key] = props[key]),
  167. root,
  168. )
  169. }
  170. }
  171. /**
  172. * @internal
  173. */
  174. export function setDynamicProp(
  175. el: TargetElement,
  176. key: string,
  177. prev: any,
  178. value: any,
  179. root?: boolean,
  180. ): void {
  181. // TODO
  182. const isSVG = false
  183. if (key === 'class') {
  184. if (root) {
  185. setClassIncremental(el, value)
  186. } else {
  187. setClass(el, value)
  188. }
  189. } else if (key === 'style') {
  190. if (root) {
  191. setStyleIncremental(el, value)
  192. } else {
  193. setStyle(el, prev, value)
  194. }
  195. } else if (isOn(key)) {
  196. on(el, key[2].toLowerCase() + key.slice(3), () => value, { effect: true })
  197. } else if (
  198. key[0] === '.'
  199. ? ((key = key.slice(1)), true)
  200. : key[0] === '^'
  201. ? ((key = key.slice(1)), false)
  202. : shouldSetAsProp(el, key, value, isSVG)
  203. ) {
  204. if (key === 'innerHTML') {
  205. setHtml(el, value)
  206. } else if (key === 'textContent') {
  207. setText(el, value)
  208. } else if (key === 'value' && canSetValueDirectly(el.tagName)) {
  209. setValue(el, value)
  210. } else {
  211. setDOMProp(el, key, value)
  212. }
  213. } else {
  214. // TODO special case for <input v-model type="checkbox">
  215. setAttr(el, key, value)
  216. }
  217. }