diff --git a/packages/compiler-core/__tests__/transforms/transformElement.spec.ts b/packages/compiler-core/__tests__/transforms/transformElement.spec.ts
index bf3510a052d..f93e1131b6d 100644
--- a/packages/compiler-core/__tests__/transforms/transformElement.spec.ts
+++ b/packages/compiler-core/__tests__/transforms/transformElement.spec.ts
@@ -99,6 +99,40 @@ describe('compiler: element transform', () => {
expect(node.tag).toBe(`$setup["Example"]`)
})
+ test('resolve component from setup bindings & component', () => {
+ const { root, node } = parseWithElementTransform(``, {
+ bindingMetadata: {
+ search: BindingTypes.SETUP_CONST,
+ },
+ isNativeTag: (tag: string) => tag !== 'search',
+ })
+ expect(root.helpers).not.toContain(RESOLVE_COMPONENT)
+ expect(node.tag).toBe(`_resolveLateAddedTag("search", 'setupState')`)
+
+ const { root: root2, node: node2 } = parseWithElementTransform(
+ ``,
+ {
+ bindingMetadata: {
+ search: BindingTypes.SETUP_LET,
+ },
+ isNativeTag: (tag: string) => tag !== 'search',
+ },
+ )
+ expect(root2.helpers).not.toContain(RESOLVE_COMPONENT)
+ expect(node2.tag).toBe(`_resolveLateAddedTag("search", 'setupState')`)
+ })
+
+ test('resolve component from props', () => {
+ const { root, node } = parseWithElementTransform(``, {
+ bindingMetadata: {
+ search: BindingTypes.PROPS,
+ },
+ isNativeTag: (tag: string) => tag !== 'search',
+ })
+ expect(root.helpers).not.toContain(RESOLVE_COMPONENT)
+ expect(node.tag).toBe(`_unref(_resolveLateAddedTag("search", 'props'))`)
+ })
+
test('resolve component from setup bindings (inline)', () => {
const { root, node } = parseWithElementTransform(``, {
inline: true,
diff --git a/packages/compiler-core/src/ast.ts b/packages/compiler-core/src/ast.ts
index bae13372a98..26c86f1175c 100644
--- a/packages/compiler-core/src/ast.ts
+++ b/packages/compiler-core/src/ast.ts
@@ -9,6 +9,7 @@ import {
OPEN_BLOCK,
type RENDER_LIST,
type RENDER_SLOT,
+ RESOLVE_LATE_ADDED_TAG,
WITH_DIRECTIVES,
type WITH_MEMO,
} from './runtimeHelpers'
@@ -875,6 +876,10 @@ export function getVNodeBlockHelper(
return ssr || isComponent ? CREATE_BLOCK : CREATE_ELEMENT_BLOCK
}
+export function getResolveLateAddedTagHelper(): typeof RESOLVE_LATE_ADDED_TAG {
+ return RESOLVE_LATE_ADDED_TAG
+}
+
export function convertToBlock(
node: VNodeCall,
{ helper, removeHelper, inSSR }: TransformContext,
diff --git a/packages/compiler-core/src/codegen.ts b/packages/compiler-core/src/codegen.ts
index 99020bcf1ae..ed3feb1341f 100644
--- a/packages/compiler-core/src/codegen.ts
+++ b/packages/compiler-core/src/codegen.ts
@@ -24,6 +24,7 @@ import {
type TemplateLiteral,
type TextNode,
type VNodeCall,
+ getResolveLateAddedTagHelper,
getVNodeBlockHelper,
getVNodeHelper,
locStub,
@@ -336,6 +337,8 @@ export function generate(
if (!__BROWSER__ && options.bindingMetadata && !options.inline) {
// binding optimization args
args.push('$props', '$setup', '$data', '$options')
+ // Add helper 'getResolveLateAddedTagHelper' for $setup
+ context.helper(getResolveLateAddedTagHelper())
}
const signature =
!__BROWSER__ && options.isTS
diff --git a/packages/compiler-core/src/runtimeHelpers.ts b/packages/compiler-core/src/runtimeHelpers.ts
index 7cf3757b249..21d4de02de8 100644
--- a/packages/compiler-core/src/runtimeHelpers.ts
+++ b/packages/compiler-core/src/runtimeHelpers.ts
@@ -26,6 +26,9 @@ export const CREATE_STATIC: unique symbol = Symbol(
export const RESOLVE_COMPONENT: unique symbol = Symbol(
__DEV__ ? `resolveComponent` : ``,
)
+export const RESOLVE_LATE_ADDED_TAG: unique symbol = Symbol(
+ __DEV__ ? `resolveLateAddedTag` : ``,
+)
export const RESOLVE_DYNAMIC_COMPONENT: unique symbol = Symbol(
__DEV__ ? `resolveDynamicComponent` : ``,
)
@@ -98,6 +101,7 @@ export const helperNameMap: Record = {
[CREATE_TEXT]: `createTextVNode`,
[CREATE_STATIC]: `createStaticVNode`,
[RESOLVE_COMPONENT]: `resolveComponent`,
+ [RESOLVE_LATE_ADDED_TAG]: `resolveLateAddedTag`,
[RESOLVE_DYNAMIC_COMPONENT]: `resolveDynamicComponent`,
[RESOLVE_DIRECTIVE]: `resolveDirective`,
[RESOLVE_FILTER]: `resolveFilter`,
diff --git a/packages/compiler-core/src/transforms/transformElement.ts b/packages/compiler-core/src/transforms/transformElement.ts
index 1dca0c514c1..4bb62909c66 100644
--- a/packages/compiler-core/src/transforms/transformElement.ts
+++ b/packages/compiler-core/src/transforms/transformElement.ts
@@ -21,12 +21,14 @@ import {
createObjectProperty,
createSimpleExpression,
createVNodeCall,
+ getResolveLateAddedTagHelper,
} from '../ast'
import {
PatchFlags,
camelize,
capitalize,
isBuiltInDirective,
+ isLateTag,
isObject,
isOn,
isReservedProp,
@@ -85,7 +87,6 @@ export const transformElement: NodeTransform = (node, context) => {
) {
return
}
-
const { tag, props } = node
const isComponent = node.tagType === ElementTypes.COMPONENT
@@ -344,10 +345,13 @@ function resolveSetupReference(name: string, context: TransformContext) {
checkType(BindingTypes.SETUP_REACTIVE_CONST) ||
checkType(BindingTypes.LITERAL_CONST)
if (fromConst) {
+ const helper = context.helperString
return context.inline
? // in inline mode, const setup bindings (e.g. imports) can be used as-is
fromConst
- : `$setup[${JSON.stringify(fromConst)}]`
+ : isLateTag(fromConst)
+ ? `${helper(getResolveLateAddedTagHelper())}(${JSON.stringify(fromConst)}, 'setupState')`
+ : `$setup[${JSON.stringify(fromConst)}]`
}
const fromMaybeRef =
@@ -355,17 +359,25 @@ function resolveSetupReference(name: string, context: TransformContext) {
checkType(BindingTypes.SETUP_REF) ||
checkType(BindingTypes.SETUP_MAYBE_REF)
if (fromMaybeRef) {
+ const helper = context.helperString
return context.inline
? // setup scope bindings that may be refs need to be unrefed
`${context.helperString(UNREF)}(${fromMaybeRef})`
- : `$setup[${JSON.stringify(fromMaybeRef)}]`
+ : isLateTag(fromMaybeRef)
+ ? `${helper(getResolveLateAddedTagHelper())}(${JSON.stringify(fromMaybeRef)}, 'setupState')`
+ : `$setup[${JSON.stringify(fromMaybeRef)}]`
}
const fromProps = checkType(BindingTypes.PROPS)
if (fromProps) {
- return `${context.helperString(UNREF)}(${
- context.inline ? '__props' : '$props'
- }[${JSON.stringify(fromProps)}])`
+ const helper = context.helperString
+ const fromPropsStr = JSON.stringify(fromProps)
+ let propsCode = context.inline
+ ? `__props[${fromPropsStr}]`
+ : isLateTag(fromProps)
+ ? `${helper(getResolveLateAddedTagHelper())}(${fromPropsStr}, 'props')`
+ : `$props[${fromPropsStr}]`
+ return `${helper(UNREF)}(${propsCode})`
}
}
diff --git a/packages/compiler-dom/__tests__/transforms/validateHtmlNesting.spec.ts b/packages/compiler-dom/__tests__/transforms/validateHtmlNesting.spec.ts
index 46d69846bae..3cc49851770 100644
--- a/packages/compiler-dom/__tests__/transforms/validateHtmlNesting.spec.ts
+++ b/packages/compiler-dom/__tests__/transforms/validateHtmlNesting.spec.ts
@@ -34,6 +34,20 @@ describe('validate html nesting', () => {
* with ISC license
*/
describe('isValidHTMLNesting', () => {
+ test('customizable select', () => {
+ // invalid
+ expect(isValidHTMLNesting('select', 'selectedcontent')).toBe(false)
+ expect(isValidHTMLNesting('option', 'button')).toBe(false)
+
+ // using example from https://developer.mozilla.org/en-US/docs/Learn_web_development/Extensions/Forms/Customizable_select
+
+ // valid
+ expect(isValidHTMLNesting('select', 'button')).toBe(true)
+ expect(isValidHTMLNesting('button', 'selectedcontent')).toBe(true)
+ expect(isValidHTMLNesting('select', 'option')).toBe(true)
+ expect(isValidHTMLNesting('option', 'span')).toBe(true)
+ })
+
test('form', () => {
// invalid
expect(isValidHTMLNesting('form', 'form')).toBe(false)
diff --git a/packages/compiler-dom/src/htmlNesting.ts b/packages/compiler-dom/src/htmlNesting.ts
index 5f924880bd0..d496e45cbc1 100644
--- a/packages/compiler-dom/src/htmlNesting.ts
+++ b/packages/compiler-dom/src/htmlNesting.ts
@@ -62,8 +62,75 @@ const onlyValidChildren: Record> = {
'script',
'template',
]),
- optgroup: new Set(['option']),
- select: new Set(['optgroup', 'option', 'hr']),
+ option: new Set([
+ 'div',
+ 'a',
+ 'abbr',
+ 'area',
+ 'audio',
+ 'b',
+ 'bdi',
+ 'bdo',
+ 'br',
+ 'canvas',
+ 'cite',
+ 'code',
+ 'data',
+ 'del',
+ 'dfn',
+ 'em',
+ 'i',
+ 'img',
+ 'input',
+ 'ins',
+ 'kbd',
+ 'link',
+ 'map',
+ 'mark',
+ 'math',
+ 'meta',
+ 'meter',
+ 'noscript',
+ 'output',
+ 'picture',
+ 'progress',
+ 'q',
+ 'ruby',
+ 's',
+ 'samp',
+ 'script',
+ 'slot',
+ 'small',
+ 'span',
+ 'strong',
+ 'sub',
+ 'sup',
+ 'svg',
+ 'template',
+ 'time',
+ 'u',
+ 'var',
+ 'video',
+ 'wbr',
+ ]),
+ optgroup: new Set([
+ 'option',
+ 'legend',
+ 'script',
+ 'template',
+ 'noscript',
+ 'div',
+ ]),
+ select: new Set([
+ 'optgroup',
+ 'option',
+ 'hr',
+ 'button',
+ 'script',
+ 'template',
+ 'noscript',
+ 'div',
+ ]),
// table
table: new Set(['caption', 'colgroup', 'tbody', 'tfoot', 'thead']),
tr: new Set(['td', 'th']),
@@ -74,10 +141,10 @@ const onlyValidChildren: Record> = {
// these elements can not have any children elements
script: emptySet,
iframe: emptySet,
- option: emptySet,
textarea: emptySet,
style: emptySet,
title: emptySet,
+ selectedcontent: emptySet,
}
/** maps elements to set of elements which can be it's parent, no other */
@@ -104,6 +171,7 @@ const onlyValidParents: Record> = {
// li: new Set(["ul", "ol"]),
summary: new Set(['details']),
area: new Set(['map']),
+ selectedcontent: new Set(['button']),
} as const
/** maps element to set of elements that can not be it's children, others can */
diff --git a/packages/runtime-core/src/helpers/resolveAssets.ts b/packages/runtime-core/src/helpers/resolveAssets.ts
index 910fab33424..57a74d4e72c 100644
--- a/packages/runtime-core/src/helpers/resolveAssets.ts
+++ b/packages/runtime-core/src/helpers/resolveAssets.ts
@@ -7,7 +7,7 @@ import {
} from '../component'
import { currentRenderingInstance } from '../componentRenderContext'
import type { Directive } from '../directives'
-import { camelize, capitalize, isString } from '@vue/shared'
+import { camelize, capitalize, isLateTag, isString } from '@vue/shared'
import { warn } from '../warning'
import type { VNodeTypes } from '../vnode'
@@ -118,12 +118,21 @@ function resolveAsset(
return Component
}
- if (__DEV__ && warnMissing && !res) {
- const extra =
- type === COMPONENTS
- ? `\nIf this is a native custom element, make sure to exclude it from ` +
+ if (
+ __DEV__ &&
+ warnMissing &&
+ ((!res && !isLateTag(name)) || (res && isLateTag(name)))
+ ) {
+ let extra = ''
+ if (type === COMPONENTS) {
+ if (isLateTag(name)) {
+ extra = `\nplease do not use built-in tag names as component names.`
+ } else {
+ extra =
+ `\nIf this is a native custom element, make sure to exclude it from ` +
`component resolution via compilerOptions.isCustomElement.`
- : ``
+ }
+ }
warn(`Failed to resolve ${type.slice(0, -1)}: ${name}${extra}`)
}
@@ -144,3 +153,27 @@ function resolve(registry: Record | undefined, name: string) {
registry[capitalize(camelize(name))])
)
}
+
+/**
+ * @private
+ */
+export function resolveLateAddedTag(
+ name: string,
+ key: 'setupState' | 'props',
+): unknown {
+ if (!currentRenderingInstance || !currentRenderingInstance[key]) return name
+ const data = currentRenderingInstance[key]
+ const value = data[name]
+ // Only the render function for the value is parsed as a component
+ // and a warning is reported
+ if (
+ __DEV__ &&
+ value &&
+ (value as ComponentInternalInstance).render &&
+ isLateTag(name as string)
+ ) {
+ const extra = `\nplease do not use built-in tag names as component names.`
+ warn(`Failed to resolve component: ${name},${extra}`)
+ }
+ return value
+}
diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts
index 1ed6f21df77..42683fd4abc 100644
--- a/packages/runtime-core/src/index.ts
+++ b/packages/runtime-core/src/index.ts
@@ -144,6 +144,7 @@ export {
resolveComponent,
resolveDirective,
resolveDynamicComponent,
+ resolveLateAddedTag,
} from './helpers/resolveAssets'
// For integration with runtime compiler
export { registerRuntimeCompiler, isRuntimeOnly } from './component'
diff --git a/packages/shared/src/domTagConfig.ts b/packages/shared/src/domTagConfig.ts
index 7f9d198e569..a0e5d9f03ba 100644
--- a/packages/shared/src/domTagConfig.ts
+++ b/packages/shared/src/domTagConfig.ts
@@ -14,6 +14,8 @@ const HTML_TAGS =
'option,output,progress,select,textarea,details,dialog,menu,' +
'summary,template,blockquote,iframe,tfoot'
+const LATE_ADDED_TAGS = 'search,selectedcontent'
+
// https://developer.mozilla.org/en-US/docs/Web/SVG/Element
const SVG_TAGS =
'svg,animate,animateMotion,animateTransform,circle,clipPath,color-profile,' +
@@ -62,3 +64,6 @@ export const isMathMLTag: (key: string) => boolean =
*/
export const isVoidTag: (key: string) => boolean =
/*@__PURE__*/ makeMap(VOID_TAGS)
+
+export const isLateTag: (key: string) => boolean =
+ /*#__PURE__*/ makeMap(LATE_ADDED_TAGS)