}"
`;
+exports[`compiler v-bind > MathML global attributes should set as attribute 1`] = `
+"import { setAttr as _setAttr, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<math></math>")
+
+export function render(_ctx) {
+ const n0 = t0()
+ _renderEffect(() => {
+ _setAttr(n0, "autofucus", _ctx.autofucus)
+ _setAttr(n0, "dir", _ctx.dir)
+ _setAttr(n0, "displaystyle", _ctx.displaystyle)
+ _setAttr(n0, "mathcolor", _ctx.mathcolor)
+ _setAttr(n0, "tabindex", _ctx.tabindex)
+ })
+ return n0
+}"
+`;
+
exports[`compiler v-bind > MathML global attributes should set as dom prop 1`] = `
"import { setDOMProp as _setDOMProp, renderEffect as _renderEffect, template as _template } from 'vue';
const t0 = _template("<math></math>")
}"
`;
+exports[`compiler v-bind > SVG global attributes should set as attribute 1`] = `
+"import { setAttr as _setAttr, renderEffect as _renderEffect, template as _template } from 'vue';
+const t0 = _template("<svg></svg>")
+
+export function render(_ctx) {
+ const n0 = t0()
+ _renderEffect(() => {
+ _setAttr(n0, "id", _ctx.id)
+ _setAttr(n0, "lang", _ctx.lang)
+ _setAttr(n0, "tabindex", _ctx.tabindex)
+ })
+ return n0
+}"
+`;
+
exports[`compiler v-bind > SVG global attributes should set as dom prop 1`] = `
"import { setDOMProp as _setDOMProp, renderEffect as _renderEffect, template as _template } from 'vue';
const t0 = _template("<svg></svg>")
expect(code).contains(' _setAttr(n6, "width", _ctx.width)')
})
- test('HTML global attributes should set as dom prop', () => {
- const { code } = compileWithVBind(`
- <div :id="id" :title="title" :lang="lang" :dir="dir" :tabindex="tabindex" />
- `)
-
- expect(code).matchSnapshot()
- expect(code).contains(
- '_id !== _ctx.id && _setDOMProp(n0, "id", (_id = _ctx.id))',
- )
- expect(code).contains(
- '_title !== _ctx.title && _setDOMProp(n0, "title", (_title = _ctx.title))',
- )
- expect(code).contains(
- '_lang !== _ctx.lang && _setDOMProp(n0, "lang", (_lang = _ctx.lang))',
- )
- expect(code).contains(
- '_dir !== _ctx.dir && _setDOMProp(n0, "dir", (_dir = _ctx.dir))',
- )
- expect(code).contains(
- '_tabindex !== _ctx.tabindex && _setDOMProp(n0, "tabindex", (_tabindex = _ctx.tabindex))',
- )
- })
-
- test('SVG global attributes should set as dom prop', () => {
- const { code } = compileWithVBind(`
- <svg :id="id" :lang="lang" :tabindex="tabindex" />
- `)
-
- expect(code).matchSnapshot()
- expect(code).contains(
- '_id !== _ctx.id && _setDOMProp(n0, "id", (_id = _ctx.id))',
- )
- expect(code).contains(
- '_lang !== _ctx.lang && _setDOMProp(n0, "lang", (_lang = _ctx.lang))',
- )
- expect(code).contains(
- '_tabindex !== _ctx.tabindex && _setDOMProp(n0, "tabindex", (_tabindex = _ctx.tabindex))',
- )
- })
-
- test('MathML global attributes should set as dom prop', () => {
- const { code } = compileWithVBind(`
- <math :autofucus :dir :displaystyle :mathcolor :tabindex/>
- `)
-
- expect(code).matchSnapshot()
- expect(code).contains(
- '_autofucus !== _ctx.autofucus && _setDOMProp(n0, "autofucus", (_autofucus = _ctx.autofucus))',
- )
- expect(code).contains(
- '_dir !== _ctx.dir && _setDOMProp(n0, "dir", (_dir = _ctx.dir))',
- )
- expect(code).contains(
- '_displaystyle !== _ctx.displaystyle && _setDOMProp(n0, "displaystyle", (_displaystyle = _ctx.displaystyle))',
- )
- expect(code).contains(
- '_mathcolor !== _ctx.mathcolor && _setDOMProp(n0, "mathcolor", (_mathcolor = _ctx.mathcolor))',
- )
- expect(code).contains(
- '_tabindex !== _ctx.tabindex && _setDOMProp(n0, "tabindex", (_tabindex = _ctx.tabindex))',
- )
- })
-
test(':innerHTML', () => {
const { code } = compileWithVBind(`
<div :innerHTML="foo"/>
},
})
+ console.log(code)
+
expect(code).matchSnapshot()
})
CodegenOptions as BaseCodegenOptions,
BaseCodegenResult,
} from '@vue/compiler-dom'
-import type {
- BlockIRNode,
- CoreHelper,
- IREffect,
- RootIRNode,
- VaporHelper,
-} from './ir'
+import type { BlockIRNode, CoreHelper, RootIRNode, VaporHelper } from './ir'
import { extend, remove } from '@vue/shared'
import { genBlockContent } from './generators/block'
import { genTemplates } from './generators/template'
delegates: Set<string> = new Set<string>()
- processingRenderEffect: IREffect | undefined = undefined
- allRenderEffectSeenNames: Record<string, number> = Object.create(null)
- shouldCacheRenderEffectDeps = (): boolean => {
- // only need to generate effect deps when it's not nested in v-for
- return !!(
- this.processingRenderEffect && !this.processingRenderEffect.inVFor
- )
- }
-
identifiers: Record<string, string[]> = Object.create(null)
block: BlockIRNode
-import { isArray, isGloballyAllowed } from '@vue/shared'
+import { isGloballyAllowed } from '@vue/shared'
import {
BindingTypes,
NewlineType,
)
if (i === ids.length - 1 && end < content.length) {
- const rest = content.slice(end)
- const last = frag[frag.length - 1]
- if (hasMemberExpression && isArray(last)) {
- // merge rest content into the last identifier's generated name
- last[0] += rest
- } else {
- push([rest, NewlineType.Unknown])
- }
+ push([content.slice(end), NewlineType.Unknown])
}
})
import type { SetHtmlIRNode } from '../ir'
import { genExpression } from './expression'
import { type CodeFragment, NEWLINE, genCall } from './utils'
-import { processValues } from './prop'
export function genSetHtml(
oper: SetHtmlIRNode,
context: CodegenContext,
): CodeFragment[] {
- const { helper, shouldCacheRenderEffectDeps } = context
+ const { helper } = context
const { value, element } = oper
- let html = genExpression(value, context)
- if (shouldCacheRenderEffectDeps()) {
- processValues(context, [html])
- }
- return [NEWLINE, ...genCall(helper('setHtml'), `n${element}`, html)]
+ return [
+ NEWLINE,
+ ...genCall(helper('setHtml'), `n${element}`, genExpression(value, context)),
+ ]
}
): CodeFragment[] {
const { helper } = context
const [frag, push, unshift] = buildCodeFragment()
- const declareNames = new Set<string>()
let operationsCount = 0
for (let i = 0; i < effects.length; i++) {
- const effect = (context.processingRenderEffect = effects[i])
+ const effect = effects[i]
operationsCount += effect.operations.length
- const frags = genEffect(effect, context, declareNames)
- const needSemi = frag[frag.length - 1] === ')' && frags[0] === '('
+ const frags = genEffect(effect, context)
i > 0 && push(NEWLINE)
- push(needSemi ? ';' : undefined, ...frags)
+ if (frag[frag.length - 1] === ')' && frags[0] === '(') {
+ push(';')
+ }
+ push(...frags)
}
const newLineCount = frag.filter(frag => frag === NEWLINE).length
push(`)`)
}
- // declare variables: let _foo, _bar
- if (declareNames.size) {
- frag.splice(1, 0, `let ${[...declareNames].join(', ')}`, NEWLINE)
- }
return frag
}
export function genEffect(
{ operations }: IREffect,
context: CodegenContext,
- allDeclareNames: Set<string>,
): CodeFragment[] {
- const { processingRenderEffect } = context
const [frag, push] = buildCodeFragment()
- const { declareNames, earlyCheckExps } = processingRenderEffect!
const operationsExps = genOperations(operations, context)
-
- if (declareNames.size) {
- allDeclareNames.add([...declareNames].join(', '))
- }
-
const newlineCount = operationsExps.filter(frag => frag === NEWLINE).length
+
if (newlineCount > 1) {
- // multiline check expression: if (_foo !== _ctx.foo || _bar !== _ctx.bar) {
- const checkExpsStart: CodeFragment[] =
- earlyCheckExps.length > 0
- ? [`if(`, ...earlyCheckExps.join(' || '), `) {`, INDENT_START]
- : []
- const checkExpsEnd: CodeFragment[] =
- earlyCheckExps.length > 0 ? [INDENT_END, NEWLINE, '}'] : []
- // assignment: _foo = _ctx.foo; _bar = _ctx.bar
- const assignmentExps: CodeFragment[] =
- earlyCheckExps.length > 0
- ? [NEWLINE, ...earlyCheckExps.map(c => c.replace('!==', '=')).join(';')]
- : []
- push(
- ...checkExpsStart,
- ...operationsExps,
- ...assignmentExps,
- ...checkExpsEnd,
- )
+ push(...operationsExps)
} else {
- // single line check expression: (_foo !== _ctx.foo || _bar !== _ctx.bar) &&
- const multiple = earlyCheckExps.length > 1
- const checkExps: CodeFragment[] =
- earlyCheckExps.length > 0
- ? [
- multiple ? `(` : undefined,
- ...earlyCheckExps.join(' || '),
- multiple ? `)` : undefined,
- ' && ',
- ]
- : []
- push(...checkExps, ...operationsExps.filter(frag => frag !== NEWLINE))
+ push(...operationsExps.filter(frag => frag !== NEWLINE))
}
return frag
genMulti,
} from './utils'
import {
- attributeCache,
canSetValueDirectly,
- isArray,
isHTMLGlobalAttr,
- isHTMLTag,
- isMathMLGlobalAttr,
- isMathMLTag,
- isSVGTag,
- isSvgGlobalAttr,
- shouldSetAsAttr,
toHandlerKey,
} from '@vue/shared'
+export type HelperConfig = {
+ name: VaporHelper
+ needKey?: boolean
+ acceptRoot?: boolean
+}
+
+// this should be kept in sync with runtime-vapor/src/dom/prop.ts
+const helpers = {
+ setText: { name: 'setText' },
+ setHtml: { name: 'setHtml' },
+ setClass: { name: 'setClass' },
+ setClassIncremental: { name: 'setClassIncremental' },
+ setStyle: { name: 'setStyle' },
+ setStyleIncremental: { name: 'setStyleIncremental' },
+ setValue: { name: 'setValue' },
+ setAttr: { name: 'setAttr', needKey: true },
+ setDOMProp: { name: 'setDOMProp', needKey: true },
+ setDynamicProps: { name: 'setDynamicProps', acceptRoot: true },
+} as const satisfies Partial<Record<VaporHelper, HelperConfig>>
+
// only the static key prop will reach here
export function genSetProp(
oper: SetPropIRNode,
const {
prop: { key, values, modifier },
tag,
+ root,
} = oper
- const { helperName, omitKey } = getRuntimeHelper(tag, key.content, modifier)
+ const resolvedHelper = getRuntimeHelper(tag, key.content, modifier, root)
const propValue = genPropValue(values, context)
- const { prevValueName, shouldWrapInParentheses } = processPropValues(
- context,
- helperName,
- [propValue],
- )
return [
NEWLINE,
- ...(prevValueName
- ? [shouldWrapInParentheses ? `(` : undefined, `${prevValueName} = `]
- : []),
...genCall(
- [helper(helperName), null],
+ [helper(resolvedHelper.name), null],
`n${oper.element}`,
- omitKey ? false : genExpression(key, context),
- ...(prevValueName ? [`${prevValueName}`] : []),
+ resolvedHelper.needKey ? genExpression(key, context) : false,
propValue,
- // only `setClass` and `setStyle` need merge inherit attr
- oper.root && (helperName === 'setClass' || helperName === 'setStyle')
- ? 'true'
- : undefined,
+ root && resolvedHelper.acceptRoot ? 'true' : undefined,
),
- ...(prevValueName && shouldWrapInParentheses ? [`)`] : []),
]
}
? genLiteralObjectProps([props], context) // dynamic arg props
: genExpression(props.value, context),
) // v-bind=""
- const { prevValueName, shouldWrapInParentheses } = processPropValues(
- context,
- 'setDynamicProps',
- values,
- )
return [
NEWLINE,
- ...(prevValueName
- ? [shouldWrapInParentheses ? `(` : undefined, `${prevValueName} = `]
- : []),
...genCall(
helper('setDynamicProps'),
`n${oper.element}`,
- ...(prevValueName ? [`${prevValueName}`] : []),
genMulti(DELIMITERS_ARRAY, ...values),
oper.root && 'true',
),
- ...(prevValueName && shouldWrapInParentheses ? [`)`] : []),
]
}
)
}
+// TODO
+// - 1. textContent + innerHTML Known base dom properties in https://developer.mozilla.org/en-US/docs/Web/API/Element
+// - 2. special handling (class / style)
+// - 3. SVG: always attribute
+// - 4. Custom Elements
+// - always properties unless known global attr or has hyphen (aria- / data-)
+// - 5. Normal Elements
+// - 1. Known shared dom properties:
+// - https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement
+// - 2. Each element's known dom properties
+// - 3. Fallback to attribute
+
function getRuntimeHelper(
tag: string,
keyName: string,
modifier: '.' | '^' | undefined,
-) {
+ root: boolean,
+): HelperConfig {
const tagName = tag.toUpperCase()
- let helperName: VaporHelper
- let omitKey = false
-
if (modifier) {
if (modifier === '.') {
- const helper = getSpecialHelper(keyName, tagName)
- if (helper) {
- helperName = helper.name
- omitKey = helper.omitKey
- } else {
- helperName = 'setDOMProp'
- omitKey = false
- }
+ return getSpecialHelper(keyName, tagName, root) || helpers.setDOMProp
} else {
- helperName = 'setAttr'
+ return helpers.setAttr
}
} else {
- const attrCacheKey = `${tagName}_${keyName}`
- const helper = getSpecialHelper(keyName, tagName)
+ const helper = getSpecialHelper(keyName, tagName, root)
if (helper) {
- helperName = helper.name
- omitKey = helper.omitKey
- } else if (
- attributeCache[attrCacheKey] === undefined
- ? (attributeCache[attrCacheKey] = shouldSetAsAttr(tagName, keyName))
- : attributeCache[attrCacheKey]
- ) {
- helperName = 'setAttr'
- } else if (
- (isHTMLTag(tag) && isHTMLGlobalAttr(keyName)) ||
- (isSVGTag(tag) && isSvgGlobalAttr(keyName)) ||
- (isMathMLTag(tag) && isMathMLGlobalAttr(keyName))
- ) {
- helperName = 'setDOMProp'
+ return helper
+ } else if (tagName.includes('-')) {
+ // custom element
+ if (isHTMLGlobalAttr(keyName) || keyName.includes('-')) {
+ return helpers.setAttr
+ } else {
+ return helpers.setDOMProp
+ }
+ } else if (/[A-Z]/.test(keyName)) {
+ return helpers.setDOMProp
} else {
- helperName = 'setDynamicProp'
+ return helpers.setAttr
}
}
- return { helperName, omitKey }
}
-const specialHelpers: Record<string, { name: VaporHelper; omitKey: boolean }> =
- {
- class: { name: 'setClass', omitKey: true },
- style: { name: 'setStyle', omitKey: true },
- innerHTML: { name: 'setHtml', omitKey: true },
- textContent: { name: 'setText', omitKey: true },
- }
-
const getSpecialHelper = (
keyName: string,
tagName: string,
-): { name: VaporHelper; omitKey: boolean } | null => {
+ root: boolean,
+): HelperConfig | undefined => {
// special case for 'value' property
if (keyName === 'value' && canSetValueDirectly(tagName)) {
- return { name: 'setValue', omitKey: true }
- }
-
- return specialHelpers[keyName] || null
-}
-
-// those runtime helpers will return the prevValue
-const helpersNeedCachedReturnValue = [
- 'setStyle',
- 'setDynamicProp',
- 'setDynamicProps',
-]
-
-function processPropValues(
- context: CodegenContext,
- helperName: string,
- values: CodeFragment[][],
-): { prevValueName: string | undefined; shouldWrapInParentheses: boolean } {
- const { shouldCacheRenderEffectDeps, processingRenderEffect } = context
- // single-line render effect and the operation needs cache return a value,
- // the expression needs to be wrapped in parentheses.
- // e.g. _foo === _ctx.foo && (_foo = _setStyle(...))
- let shouldWrapInParentheses: boolean = false
- let prevValueName
- if (shouldCacheRenderEffectDeps()) {
- const needReturnValue = helpersNeedCachedReturnValue.includes(helperName)
- processValues(context, values, !needReturnValue)
- const { declareNames } = processingRenderEffect!
- // if the operation needs to cache the return value and has multiple declareNames,
- // combine them into a single name as the return value name.
- if (declareNames.size > 0 && needReturnValue) {
- prevValueName = [...declareNames].join('')
- declareNames.add(prevValueName)
- }
- shouldWrapInParentheses = processingRenderEffect!.operations.length === 1
+ return helpers.setValue
}
- return { prevValueName, shouldWrapInParentheses }
-}
-
-export function processValues(
- context: CodegenContext,
- values: CodeFragment[][],
- needRewrite: boolean = true,
-): string[] {
- const allCheckExps: string[] = []
- values.forEach(value => {
- const checkExps = processValue(context, value, needRewrite)
- if (checkExps) allCheckExps.push(...checkExps, ' && ')
- })
-
- return allCheckExps.length > 0
- ? (context.processingRenderEffect!.earlyCheckExps = [
- ...new Set(allCheckExps),
- ])
- : []
-}
-
-function processValue(
- context: CodegenContext,
- values: CodeFragment[],
- needRewrite: boolean = true,
-): string[] | undefined {
- const { processingRenderEffect, allRenderEffectSeenNames } = context
- const { declareNames, rewrittenNames, earlyCheckExps, operations } =
- processingRenderEffect!
-
- const isSingleLine = operations.length === 1
- for (const frag of values) {
- if (!isArray(frag)) continue
- // [code, newlineIndex, loc, name] -> [(_name = code), newlineIndex, loc, name]
- const [newName, , , rawName] = frag
- if (rawName) {
- let name = rawName.replace(/[^\w]/g, '_')
- if (rewrittenNames.has(name)) continue
- rewrittenNames.add(name)
- name = `_${name}`
- if (declareNames.has(name)) continue
-
- if (allRenderEffectSeenNames[name] === undefined)
- allRenderEffectSeenNames[name] = 0
- else name += ++allRenderEffectSeenNames[name]
-
- declareNames.add(name)
- earlyCheckExps.push(`${name} !== ${newName}`)
-
- if (needRewrite && isSingleLine) {
- // replace the original code fragment with the assignment expression
- frag[0] = `(${name} = ${newName})`
- }
+ if (root) {
+ if (keyName === 'class') {
+ return helpers.setClassIncremental
+ } else if (keyName === 'style') {
+ return helpers.setStyleIncremental
}
}
- if (earlyCheckExps.length > 0) {
- return [[...new Set(earlyCheckExps)].join(' && ')]
+ if (keyName === 'class') {
+ return helpers.setClass
+ } else if (keyName === 'style') {
+ return helpers.setStyle
+ } else if (keyName === 'innerHTML') {
+ return helpers.setHtml
+ } else if (keyName === 'textContent') {
+ return helpers.setText
}
}
genCall,
genMulti,
} from './utils'
-import { processValues } from './prop'
export function genSetText(
oper: SetTextIRNode,
context: CodegenContext,
): CodeFragment[] {
- const { helper, shouldCacheRenderEffectDeps } = context
+ const { helper } = context
const { element, values } = oper
const texts = values.map(value => genExpression(value, context))
- if (shouldCacheRenderEffectDeps()) {
- processValues(context, texts)
- }
return [NEWLINE, ...genCall(helper('setText'), `n${element}`, ...texts)]
}
export interface IREffect {
expressions: SimpleExpressionNode[]
- identifiers: string[]
operations: OperationNode[]
- declareNames: Set<string>
- rewrittenNames: Set<string>
- earlyCheckExps: string[]
- inVFor: boolean
}
type Overwrite<T, U> = Pick<T, Exclude<keyof T, keyof U>> &
type CompilerCompatOptions,
type ElementNode,
ElementTypes,
- type ExpressionNode,
NodeTypes,
type RootNode,
type SimpleExpressionNode,
defaultOnError,
defaultOnWarn,
isVSlot,
- walkIdentifiers,
} from '@vue/compiler-dom'
-import {
- EMPTY_OBJ,
- NOOP,
- extend,
- isArray,
- isString,
- looseEqual,
-} from '@vue/shared'
+import { EMPTY_OBJ, NOOP, extend, isArray, isString } from '@vue/shared'
import {
type BlockIRNode,
DynamicFlag,
if (this.inVOnce || expressions.length === 0) {
return this.registerOperation(...operations)
}
- const ids = new Set<string>()
- expressions.forEach(exp => extractIdentifiers(ids, exp))
const existing = this.block.effect.find(e =>
- looseEqual(e.identifiers, Array.from(ids)),
+ isSameExpression(e.expressions, expressions),
)
if (existing) {
existing.operations.push(...operations)
this.block.effect.push({
expressions,
operations,
- earlyCheckExps: [],
- declareNames: new Set<string>(),
- rewrittenNames: new Set<string>(),
- inVFor: this.inVFor > 0,
- identifiers: Array.from(ids),
})
}
+
+ function isSameExpression(
+ a: SimpleExpressionNode[],
+ b: SimpleExpressionNode[],
+ ) {
+ if (a.length !== b.length) return false
+ return a.every((exp, i) => exp.content === b[i].content)
+ }
}
+
registerOperation(...node: OperationNode[]): void {
this.block.operation.push(...node)
}
}
}
}
-
-function extractIdentifiers(ids: Set<string>, node: ExpressionNode) {
- if (node.ast) {
- walkIdentifiers(node.ast, n => ids.add(n.name), true)
- } else if (node.ast === null) {
- ids.add((node as SimpleExpressionNode).content)
- }
-}
export * from '@vue/runtime-core'
export * from './jsx'
+
+// VAPOR -----------------------------------------------------------------------
+
+/**
+ * @internal
+ */
+export { patchStyle } from './modules/style'
+/**
+ * @internal
+ */
+export { shouldSetAsProp } from './patchProp'
import { DeprecationTypes, compatUtils, warn } from '@vue/runtime-core'
-import { includeBooleanAttr } from '@vue/shared'
+import { canSetValueDirectly, includeBooleanAttr } from '@vue/shared'
import { unsafeToTrustedHTML } from '../nodeOps'
// functions. The user is responsible for using them with only trusted content.
const tag = el.tagName
- if (
- key === 'value' &&
- tag !== 'PROGRESS' &&
- // custom elements may use _value internally
- !tag.includes('-')
- ) {
+ if (key === 'value' && canSetValueDirectly(tag)) {
// #4956: <option> value will fallback to its text content so we need to
// compare against its attribute value instead.
const oldValue =
} from '../directives/vShow'
import { CSS_VAR_TEXT } from '../helpers/useCssVars'
-type Style = string | Record<string, string | string[]> | null
+type Style = string | Record<string, string | string[]> | null | undefined
const displayRE = /(^|;)\s*display\s*:/
isNativeOn,
isOn,
isString,
+ shouldSetAsAttr,
} from '@vue/shared'
import type { RendererOptions } from '@vue/runtime-core'
import type { VueElement } from './apiCustomElement'
}
}
-function shouldSetAsProp(
+export function shouldSetAsProp(
el: Element,
key: string,
value: unknown,
isSVG: boolean,
-) {
+): boolean {
if (isSVG) {
// most keys must be set as attribute on svg elements to work
// ...except innerHTML & textContent
return false
}
- // these are enumerated attrs, however their corresponding DOM properties
- // are actually booleans - this leads to setting it with a string "false"
- // value leading it to be coerced to `true`, so we need to always treat
- // them as attributes.
- // Note that `contentEditable` doesn't have this problem: its DOM
- // property is also enumerated string values.
- if (key === 'spellcheck' || key === 'draggable' || key === 'translate') {
+ if (shouldSetAsAttr(el.tagName, key)) {
return false
}
- // #1787, #2840 form property on form elements is readonly and must be set as
- // attribute.
- if (key === 'form') {
- return false
- }
-
- // #1526 <input list> must be set as attribute
- if (key === 'list' && el.tagName === 'INPUT') {
- return false
- }
-
- // #2766 <textarea type> must be set as attribute
- if (key === 'type' && el.tagName === 'TEXTAREA') {
- return false
- }
-
- // #8780 the width or height of embedded tags must be set as attribute
- if (key === 'width' || key === 'height') {
- const tag = el.tagName
- if (
- tag === 'IMG' ||
- tag === 'VIDEO' ||
- tag === 'CANVAS' ||
- tag === 'SOURCE'
- ) {
- return false
- }
- }
-
// native onclick with string value, must be set as attribute
if (isNativeOn(key) && isString(value)) {
return false
inheritAttrs: false,
setup(props, { attrs }) {
const el = document.createElement('div')
- let prev: any
- renderEffect(() => (prev = setDynamicProps(el, prev, [attrs])))
+ renderEffect(() => setDynamicProps(el, [attrs]))
return el
},
})
const n0 = createComponent(Wrapper, null, {
default: () => {
const n0 = template('<div>')() as HTMLDivElement
- let prev: any
- renderEffect(
- () => (prev = setDynamicProps(n0, prev, [attrs], true)),
- )
+ renderEffect(() => setDynamicProps(n0, [attrs], true))
return n0
},
})
describe('setDOMProp', () => {
test('should be boolean prop', () => {
const el = document.createElement('select')
- setDOMProp(el, 'multiple', '')
- expect(el.multiple).toBe(true)
+ // In vapor static attrs are part of the template and this never happens
+ // setDOMProp(el, 'multiple', '')
+ // expect(el.multiple).toBe(true)
setDOMProp(el, 'multiple', null)
expect(el.multiple).toBe(false)
setDOMProp(el, 'multiple', true)
test('should remove attribute when value is falsy', () => {
const el = document.createElement('div')
- setDOMProp(el, 'id', '')
- expect(el.hasAttribute('id')).toBe(true)
+ el.setAttribute('id', '')
setDOMProp(el, 'id', null)
expect(el.hasAttribute('id')).toBe(false)
- setDOMProp(el, 'id', '')
- expect(el.hasAttribute('id')).toBe(true)
+ el.setAttribute('id', '')
setDOMProp(el, 'id', undefined)
expect(el.hasAttribute('id')).toBe(false)
setDOMProp(el, 'id', '')
- expect(el.hasAttribute('id')).toBe(true)
+ expect(el.hasAttribute('id')).toBe(false)
const img = document.createElement('img')
- setDOMProp(img, 'width', '')
- expect(img.hasAttribute('width')).toBe(false)
setDOMProp(img, 'width', 0)
- expect(img.hasAttribute('width')).toBe(true)
+ expect(img.hasAttribute('width')).toBe(false) // skipped
setDOMProp(img, 'width', null)
expect(img.hasAttribute('width')).toBe(false)
- setDOMProp(img, 'width', 0)
+ setDOMProp(img, 'width', 1)
expect(img.hasAttribute('width')).toBe(true)
setDOMProp(img, 'width', undefined)
expect(img.hasAttribute('width')).toBe(false)
- setDOMProp(img, 'width', 0)
+ setDOMProp(img, 'width', 1)
expect(img.hasAttribute('width')).toBe(true)
})
describe('setDynamicProps', () => {
test('basic set dynamic props', () => {
const el = document.createElement('div')
- setDynamicProps(el, null, [{ foo: 'val' }, { bar: 'val' }])
+ setDynamicProps(el, [{ foo: 'val' }, { bar: 'val' }])
expect(el.getAttribute('foo')).toBe('val')
expect(el.getAttribute('bar')).toBe('val')
})
test('should merge props', () => {
const el = document.createElement('div')
- setDynamicProps(el, null, [{ foo: 'val' }, { foo: 'newVal' }])
+ setDynamicProps(el, [{ foo: 'val' }, { foo: 'newVal' }])
expect(el.getAttribute('foo')).toBe('newVal')
})
test('should reset old props', () => {
const el = document.createElement('div')
- let prev: any
- prev = setDynamicProps(el, prev, [{ foo: 'val' }])
+ setDynamicProps(el, [{ foo: 'val' }])
expect(el.attributes.length).toBe(1)
expect(el.getAttribute('foo')).toBe('val')
- prev = setDynamicProps(el, prev, [{ bar: 'val' }])
+ setDynamicProps(el, [{ bar: 'val' }])
expect(el.attributes.length).toBe(1)
expect(el.getAttribute('bar')).toBe('val')
expect(el.getAttribute('foo')).toBeNull()
test('should reset old modifier props', () => {
const el = document.createElement('div')
- let prev: any
- prev = setDynamicProps(el, prev, [{ ['.foo']: 'val' }])
+ setDynamicProps(el, [{ ['.foo']: 'val' }])
expect((el as any).foo).toBe('val')
- prev = setDynamicProps(el, prev, [{ ['.bar']: 'val' }])
+ setDynamicProps(el, [{ ['.bar']: 'val' }])
expect((el as any).bar).toBe('val')
expect((el as any).foo).toBe('')
- prev = setDynamicProps(el, prev, [{ ['^foo']: 'val' }])
+ setDynamicProps(el, [{ ['^foo']: 'val' }])
expect(el.attributes.length).toBe(1)
expect(el.getAttribute('foo')).toBe('val')
- prev = setDynamicProps(el, prev, [{ ['^bar']: 'val' }])
+ setDynamicProps(el, [{ ['^bar']: 'val' }])
expect(el.attributes.length).toBe(1)
expect(el.getAttribute('bar')).toBe('val')
expect(el.getAttribute('foo')).toBeNull()
proxyRefs,
resetTracking,
} from '@vue/reactivity'
-import {
- EMPTY_OBJ,
- extend,
- invokeArrayFns,
- isFunction,
- isString,
-} from '@vue/shared'
+import { EMPTY_OBJ, invokeArrayFns, isFunction, isString } from '@vue/shared'
import {
type DynamicPropsSource,
type RawProps,
} from './componentProps'
import { renderEffect } from './renderEffect'
import { emit, normalizeEmitsOptions } from './componentEmits'
-import { setStyle } from './dom/style'
-import { setClass, setDynamicProp, setDynamicProps } from './dom/prop'
+import { setDynamicProps } from './dom/prop'
import {
type DynamicSlotSource,
type RawSlots,
instance.block instanceof Element &&
Object.keys(instance.attrs).length
) {
- let prevProps: any
renderEffect(() => {
- setDynamicProps(instance.block as Element, prevProps, [
- (prevProps = extend({}, instance.attrs)),
- ])
+ setDynamicProps(instance.block as Element, [instance.attrs])
})
}
const el = document.createElement(comp)
if (rawProps) {
- let prevProps: any, prevStyle: any
renderEffect(() => {
- let classes: unknown[] | undefined
- let styles: unknown[] | undefined
- const resolved = resolveDynamicProps(rawProps)
- for (const key in resolved) {
- const value = resolved[key]
- if (key === 'class') {
- ;(classes ||= []).push(value)
- } else if (key === 'style') {
- ;(styles ||= []).push(value)
- } else if (value !== prevProps) {
- setDynamicProp(el, key, prevProps, (prevProps = value))
- }
- }
- if (classes) setClass(el, classes, isSingleRoot)
- if (styles) setStyle(el, prevStyle, (prevStyle = styles), isSingleRoot)
+ setDynamicProps(el, [resolveDynamicProps(rawProps)])
})
}
import {
- attributeCache,
+ type NormalizedStyle,
canSetValueDirectly,
- includeBooleanAttr,
- isArray,
- isFunction,
- isNativeOn,
isOn,
isString,
normalizeClass,
normalizeStyle,
- shouldSetAsAttr,
+ parseStringStyle,
toDisplayString,
} from '@vue/shared'
-import { setStyle } from './style'
import { on } from './event'
-import { currentInstance } from '../component'
-import { warn } from '@vue/runtime-dom'
+import {
+ mergeProps,
+ patchStyle as setStyle,
+ shouldSetAsProp,
+ warn,
+} from '@vue/runtime-dom'
+
+type TargetElement = Element & {
+ $html?: string
+ $cls?: string
+ $clsi?: string
+ $sty?: NormalizedStyle
+ $styi?: NormalizedStyle
+ $dprops?: Record<string, any>
+}
-export function mergeInheritAttr(key: string, value: any): unknown {
- const instance = currentInstance!
- return mergeProp(key, instance.attrs[key], value)
+export function setText(el: Node & { $txt?: string }, ...values: any[]): void {
+ const value = values.map(v => toDisplayString(v)).join('')
+ if (el.$txt !== value) {
+ el.textContent = el.$txt = value
+ }
}
-export function setClass(el: Element, value: any, root?: boolean): void {
- el.className = normalizeClass(root ? mergeInheritAttr('class', value) : value)
+export function setHtml(el: TargetElement, value: any): void {
+ value = value == null ? '' : value
+ if (el.$html !== value) {
+ el.innerHTML = el.$html = value
+ }
}
-export function setAttr(el: Element, key: string, value: any): void {
- if (value != null) {
- el.setAttribute(key, value)
- } else {
- el.removeAttribute(key)
+export function setClass(el: TargetElement, value: any): void {
+ if ((value = normalizeClass(value)) !== el.$cls) {
+ el.className = el.$cls = value
+ }
+}
+
+/**
+ * A version of setClass that does not overwrite pre-existing classes.
+ * Used on single root elements so it can patch class independent of fallthrough
+ * attributes.
+ */
+export function setClassIncremental(el: TargetElement, value: any): void {
+ const prev = el.$clsi
+ if ((value = normalizeClass(value)) !== prev) {
+ el.$clsi = value
+ const nextList = value.split(/\s+/)
+ el.classList.add(...nextList)
+ if (prev) {
+ for (const cls of prev.split(/\s+/)) {
+ if (!nextList.includes(cls)) el.classList.remove(cls)
+ }
+ }
+ }
+}
+
+/**
+ * Reuse from runtime-dom
+ */
+export { setStyle }
+
+/**
+ * A version of setStyle that does not overwrite pre-existing styles.
+ * Used on single root elements so it can patch class independent of fallthrough
+ * attributes.
+ */
+export function setStyleIncremental(el: TargetElement, value: any): void {
+ const prev = el.$styi
+ value = el.$styi = isString(value)
+ ? parseStringStyle(value)
+ : ((normalizeStyle(value) || {}) as NormalizedStyle)
+ setStyle(el, prev, value)
+}
+
+export function setAttr(el: any, key: string, value: any): void {
+ if (value !== el[`$${key}`]) {
+ el[`$${key}`] = value
+ if (value != null) {
+ el.setAttribute(key, value)
+ } else {
+ el.removeAttribute(key)
+ }
}
}
-export function setValue(el: any, value: any): void {
+export function setValue(
+ el: Element & { value?: string; _value?: any },
+ value: any,
+): void {
// store value as _value as well since
// non-string values will be stringified.
el._value = value
}
export function setDOMProp(el: any, key: string, value: any): void {
+ const prev = el[key]
+ if (value === prev) {
+ return
+ }
+
let needRemove = false
if (value === '' || value == null) {
- const type = typeof el[key]
- if (type === 'boolean') {
- // e.g. <select multiple> compiles to { multiple: '' }
- value = includeBooleanAttr(value)
- } else if (value == null && type === 'string') {
+ const type = typeof prev
+ if (value == null && type === 'string') {
// e.g. <div :id="null">
value = ''
needRemove = true
needRemove && el.removeAttribute(key)
}
-export function setDynamicProp(
- el: Element,
- key: string,
- prev: any,
- value: any,
-): any {
- // TODO
- const isSVG = false
- if (key === 'class') {
- setClass(el, value)
- } else if (key === 'style') {
- return setStyle(el as HTMLElement, prev, value)
- } else if (isOn(key)) {
- on(el, key[2].toLowerCase() + key.slice(3), () => value, { effect: true })
- } else if (
- key[0] === '.'
- ? ((key = key.slice(1)), true)
- : key[0] === '^'
- ? ((key = key.slice(1)), false)
- : shouldSetAsProp(el, key, value, isSVG)
- ) {
- if (key === 'innerHTML') {
- setHtml(el, value)
- return
- }
-
- if (key === 'textContent') {
- setText(el, value)
- return
- }
-
- const tag = el.tagName
- if (key === 'value' && canSetValueDirectly(tag)) {
- setValue(el, value)
- return
- }
-
- setDOMProp(el, key, value)
- } else {
- // TODO special case for <input v-model type="checkbox">
- setAttr(el, key, value)
- }
-}
-
export function setDynamicProps(
- el: Element,
- oldProps: any,
+ el: TargetElement,
args: any[],
- root?: boolean,
+ root = false,
): void {
- if (root) {
- args.unshift(currentInstance!.attrs)
- }
const props = args.length > 1 ? mergeProps(...args) : args[0]
+ const oldProps = el.$dprops
if (oldProps) {
for (const key in oldProps) {
const oldValue = oldProps[key]
const hasNewValue = props[key] || props['.' + key] || props['^' + key]
if (oldValue && !hasNewValue) {
- setDynamicProp(el, key, oldValue, null)
+ setDynamicProp(el, key, oldValue, null, root)
}
}
}
- const prev = Object.create(null)
+ const prev = (el.$dprops = Object.create(null))
for (const key in props) {
setDynamicProp(
el,
key,
oldProps ? oldProps[key] : undefined,
(prev[key] = props[key]),
+ root,
)
}
-
- return prev
}
-export function mergeProp(
+/**
+ * @internal
+ */
+export function setDynamicProp(
+ el: TargetElement,
key: string,
- existing: unknown,
- incoming: unknown,
-): unknown {
+ prev: any,
+ value: any,
+ root?: boolean,
+): void {
+ // TODO
+ const isSVG = false
if (key === 'class') {
- if (existing !== incoming) {
- return normalizeClass([existing, incoming])
+ if (root) {
+ setClassIncremental(el, value)
+ } else {
+ setClass(el, value)
}
} else if (key === 'style') {
- return normalizeStyle([existing, incoming])
- } else if (isOn(key)) {
- if (
- incoming &&
- existing !== incoming &&
- !(isArray(existing) && existing.includes(incoming))
- ) {
- return existing ? [].concat(existing as any, incoming as any) : incoming
- }
- }
- return incoming
-}
-
-type Data = Record<string, any>
-
-export function mergeProps(...args: Data[]): Data {
- const ret: Data = {}
- for (let i = 0; i < args.length; i++) {
- const toMerge = args[i]
- for (const key in toMerge) {
- if (key !== '') {
- ret[key] = mergeProp(key, ret[key], toMerge[key])
- }
- }
- }
- return ret
-}
-
-export function setText(el: Node, ...values: any[]): void {
- el.textContent = values.map(v => toDisplayString(v)).join('')
-}
-
-export function setHtml(el: Element, value: any): void {
- el.innerHTML = value == null ? '' : value
-}
-
-// TODO copied from runtime-dom
-function shouldSetAsProp(
- el: Element,
- key: string,
- value: unknown,
- isSVG: boolean,
-) {
- if (isSVG) {
- // most keys must be set as attribute on svg elements to work
- // ...except innerHTML & textContent
- if (key === 'innerHTML' || key === 'textContent') {
- return true
- }
- // or native onclick with function values
- if (key in el && isNativeOn(key) && isFunction(value)) {
- return true
+ if (root) {
+ setStyleIncremental(el, value)
+ } else {
+ setStyle(el, prev, value)
}
- return false
- }
-
- const attrCacheKey = `${el.tagName}_${key}`
- if (
- attributeCache[attrCacheKey] === undefined
- ? (attributeCache[attrCacheKey] = shouldSetAsAttr(el.tagName, key))
- : attributeCache[attrCacheKey]
+ } else if (isOn(key)) {
+ on(el, key[2].toLowerCase() + key.slice(3), () => value, { effect: true })
+ } else if (
+ key[0] === '.'
+ ? ((key = key.slice(1)), true)
+ : key[0] === '^'
+ ? ((key = key.slice(1)), false)
+ : shouldSetAsProp(el, key, value, isSVG)
) {
- return false
- }
-
- // native onclick with string value, must be set as attribute
- if (isNativeOn(key) && isString(value)) {
- return false
+ if (key === 'innerHTML') {
+ setHtml(el, value)
+ } else if (key === 'textContent') {
+ setText(el, value)
+ } else if (key === 'value' && canSetValueDirectly(el.tagName)) {
+ setValue(el, value)
+ } else {
+ setDOMProp(el, key, value)
+ }
+ } else {
+ // TODO special case for <input v-model type="checkbox">
+ setAttr(el, key, value)
}
-
- return key in el
}
export { createSlot, createForSlots } from './componentSlots'
export { template, children, next } from './dom/template'
export { createTextNode } from './dom/node'
-export { setStyle } from './dom/style'
export {
setText,
setHtml,
setClass,
+ setStyle,
+ setClassIncremental,
+ setStyleIncremental,
setAttr,
setValue,
setDOMProp,
- setDynamicProp,
setDynamicProps,
} from './dom/prop'
export { on, delegate, delegateEvents, setDynamicEvents } from './dom/event'
return type === 'string' || type === 'number' || type === 'boolean'
}
-/**
- * cache seen attributes which must be set as attribute
- */
-export const attributeCache: Record<string, boolean> = Object.create(null)
-
/*
* The following attributes must be set as attribute
*/
import { hyphenate, isArray, isObject, isString } from './general'
-export type NormalizedStyle = Record<string, string | number>
+export type NormalizedStyle = Record<string, string>
export function normalizeStyle(
value: unknown,
{
name: 'trim-vapor-exports',
transform(code, id) {
- if (id.endsWith('runtime-core/src/index.ts')) {
+ if (
+ id.endsWith('runtime-core/src/index.ts') ||
+ id.endsWith('runtime-dom/src/index.ts')
+ ) {
const index = code.lastIndexOf('// VAPOR ---')
return code.slice(0, index)
}