diff --git a/packages/runtime-vapor/__tests__/apiSetupContext.spec.ts b/packages/runtime-vapor/__tests__/apiSetupContext.spec.ts
index 7eb8a0cf0..9bc5da2ea 100644
--- a/packages/runtime-vapor/__tests__/apiSetupContext.spec.ts
+++ b/packages/runtime-vapor/__tests__/apiSetupContext.spec.ts
@@ -72,9 +72,12 @@ describe('api: setup context', () => {
const Child = defineVaporComponent({
inheritAttrs: false,
- setup(props, { attrs }) {
+ setup(_props, { attrs }) {
const el = document.createElement('div')
- renderEffect(() => setDynamicProps(el, [attrs]))
+ let prev: any
+ renderEffect(() => {
+ prev = setDynamicProps(el, [attrs], prev, true)
+ })
return el
},
})
@@ -110,7 +113,10 @@ describe('api: setup context', () => {
const n0 = createComponent(Wrapper, null, {
default: () => {
const n0 = template('
')() as HTMLDivElement
- renderEffect(() => setDynamicProps(n0, [attrs], true))
+ let prev: any
+ renderEffect(() => {
+ prev = setDynamicProps(n0, [attrs], prev, true)
+ })
return n0
},
})
diff --git a/packages/runtime-vapor/__tests__/componentAttrs.spec.ts b/packages/runtime-vapor/__tests__/componentAttrs.spec.ts
index 99544d50d..87b0721f4 100644
--- a/packages/runtime-vapor/__tests__/componentAttrs.spec.ts
+++ b/packages/runtime-vapor/__tests__/componentAttrs.spec.ts
@@ -1,6 +1,15 @@
-import { nextTick, ref, watchEffect } from '@vue/runtime-dom'
-import { createComponent, setText, template } from '../src'
+import { type Ref, nextTick, ref, watchEffect } from '@vue/runtime-dom'
+import {
+ createComponent,
+ defineVaporComponent,
+ renderEffect,
+ setClassIncremental,
+ setStyleIncremental,
+ setText,
+ template,
+} from '../src'
import { makeRender } from './_utils'
+import { stringifyStyle } from '@vue/shared'
const define = makeRender
()
@@ -132,4 +141,135 @@ describe('attribute fallthrough', () => {
await nextTick()
expect(host.innerHTML).toBe('2
')
})
+
+ it('should merge classes', async () => {
+ const rootClass = ref('root')
+ const parentClass = ref('parent')
+ const childClass = ref('child')
+
+ const Child = defineVaporComponent({
+ setup() {
+ const n = document.createElement('div')
+ renderEffect(() => {
+ // binding on template root generates incremental class setter
+ setClassIncremental(n, childClass.value)
+ })
+ return n
+ },
+ })
+
+ const Parent = defineVaporComponent({
+ setup() {
+ return createComponent(
+ Child,
+ {
+ class: () => parentClass.value,
+ },
+ null,
+ true, // pass single root flag
+ )
+ },
+ })
+
+ const { host } = define({
+ setup() {
+ return createComponent(Parent, {
+ class: () => rootClass.value,
+ })
+ },
+ }).render()
+
+ const list = host.children[0].classList
+ // assert classes without being order-sensitive
+ function assertClasses(cls: string[]) {
+ expect(list.length).toBe(cls.length)
+ for (const c of cls) {
+ expect(list.contains(c)).toBe(true)
+ }
+ }
+
+ assertClasses(['root', 'parent', 'child'])
+
+ rootClass.value = 'root1'
+ await nextTick()
+ assertClasses(['root1', 'parent', 'child'])
+
+ parentClass.value = 'parent1'
+ await nextTick()
+ assertClasses(['root1', 'parent1', 'child'])
+
+ childClass.value = 'child1'
+ await nextTick()
+ assertClasses(['root1', 'parent1', 'child1'])
+ })
+
+ it('should merge styles', async () => {
+ const rootStyle: Ref> = ref('color:red')
+ const parentStyle: Ref = ref('font-size:12px')
+ const childStyle = ref('font-weight:bold')
+
+ const Child = defineVaporComponent({
+ setup() {
+ const n = document.createElement('div')
+ renderEffect(() => {
+ // binding on template root generates incremental class setter
+ setStyleIncremental(n, childStyle.value)
+ })
+ return n
+ },
+ })
+
+ const Parent = defineVaporComponent({
+ setup() {
+ return createComponent(
+ Child,
+ {
+ style: () => parentStyle.value,
+ },
+ null,
+ true, // pass single root flag
+ )
+ },
+ })
+
+ const { host } = define({
+ setup() {
+ return createComponent(Parent, {
+ style: () => rootStyle.value,
+ })
+ },
+ }).render()
+
+ const el = host.children[0] as HTMLElement
+
+ function getCSS() {
+ return el.style.cssText.replace(/\s+/g, '')
+ }
+
+ function assertStyles() {
+ const css = getCSS()
+ expect(css).toContain(stringifyStyle(rootStyle.value))
+ if (parentStyle.value) {
+ expect(css).toContain(stringifyStyle(parentStyle.value))
+ }
+ expect(css).toContain(stringifyStyle(childStyle.value))
+ }
+
+ assertStyles()
+
+ rootStyle.value = { color: 'green' }
+ await nextTick()
+ assertStyles()
+ expect(getCSS()).not.toContain('color:red')
+
+ parentStyle.value = null
+ await nextTick()
+ assertStyles()
+ expect(getCSS()).not.toContain('font-size:12px')
+
+ childStyle.value = 'font-weight:500'
+ await nextTick()
+ assertStyles()
+ expect(getCSS()).not.toContain('font-size:bold')
+ })
})
diff --git a/packages/runtime-vapor/__tests__/dom/prop.spec.ts b/packages/runtime-vapor/__tests__/dom/prop.spec.ts
index 383854035..ead9e75cd 100644
--- a/packages/runtime-vapor/__tests__/dom/prop.spec.ts
+++ b/packages/runtime-vapor/__tests__/dom/prop.spec.ts
@@ -305,13 +305,12 @@ describe('patchProp', () => {
describe('setDynamicProp', () => {
const element = document.createElement('div')
- let prev: any
function setDynamicProp(
key: string,
value: any,
el = element.cloneNode(true) as HTMLElement,
) {
- prev = _setDynamicProp(el, key, prev, value)
+ _setDynamicProp(el, key, value)
return el
}
diff --git a/packages/runtime-vapor/src/component.ts b/packages/runtime-vapor/src/component.ts
index 562037927..199b5ba6a 100644
--- a/packages/runtime-vapor/src/component.ts
+++ b/packages/runtime-vapor/src/component.ts
@@ -210,7 +210,12 @@ export function createComponent(
Object.keys(instance.attrs).length
) {
renderEffect(() => {
- setDynamicProps(instance.block as Element, [instance.attrs])
+ setDynamicProps(
+ instance.block as Element,
+ [instance.attrs],
+ true, // root
+ true, // fallthrough
+ )
})
}
@@ -421,7 +426,7 @@ export function createComponentWithFallback(
if (rawProps) {
renderEffect(() => {
- setDynamicProps(el, [resolveDynamicProps(rawProps)])
+ setDynamicProps(el, [resolveDynamicProps(rawProps)], isSingleRoot)
})
}
diff --git a/packages/runtime-vapor/src/componentProps.ts b/packages/runtime-vapor/src/componentProps.ts
index 2876a4e0d..e3e1d6a32 100644
--- a/packages/runtime-vapor/src/componentProps.ts
+++ b/packages/runtime-vapor/src/componentProps.ts
@@ -4,6 +4,7 @@ import {
YES,
camelize,
hasOwn,
+ isArray,
isFunction,
isString,
} from '@vue/shared'
@@ -171,6 +172,8 @@ export function getPropsProxyHandlers(
export function getAttrFromRawProps(rawProps: RawProps, key: string): unknown {
if (key === '$') return
+ // need special merging behavior for class & style
+ const merged = key === 'class' || key === 'style' ? ([] as any[]) : undefined
const dynamicSources = rawProps.$
if (dynamicSources) {
let i = dynamicSources.length
@@ -180,13 +183,23 @@ export function getAttrFromRawProps(rawProps: RawProps, key: string): unknown {
isDynamic = isFunction(source)
source = isDynamic ? (source as Function)() : source
if (hasOwn(source, key)) {
- return isDynamic ? source[key] : source[key]()
+ const value = isDynamic ? source[key] : source[key]()
+ if (merged) {
+ merged.push(value)
+ } else {
+ return value
+ }
}
}
}
if (hasOwn(rawProps, key)) {
- return rawProps[key]()
+ if (merged) {
+ merged.push(rawProps[key]())
+ } else {
+ return rawProps[key]()
+ }
}
+ return merged
}
export function hasAttrFromRawProps(rawProps: RawProps, key: string): boolean {
@@ -299,9 +312,17 @@ export function resolveDynamicProps(props: RawProps): Record {
const isDynamic = isFunction(source)
const resolved = isDynamic ? source() : source
for (const key in resolved) {
- mergedRawProps[key] = isDynamic
- ? resolved[key]
- : (resolved[key] as Function)()
+ const value = isDynamic ? resolved[key] : (resolved[key] as Function)()
+ if (key === 'class' || key === 'style') {
+ const existing = mergedRawProps[key]
+ if (isArray(existing)) {
+ existing.push(value)
+ } else {
+ mergedRawProps[key] = [existing, value]
+ }
+ } else {
+ mergedRawProps[key] = value
+ }
}
}
}
diff --git a/packages/runtime-vapor/src/dom/prop.ts b/packages/runtime-vapor/src/dom/prop.ts
index 3bba642b4..790c134e0 100644
--- a/packages/runtime-vapor/src/dom/prop.ts
+++ b/packages/runtime-vapor/src/dom/prop.ts
@@ -14,10 +14,7 @@ import { mergeProps, patchStyle, shouldSetAsProp, warn } from '@vue/runtime-dom'
type TargetElement = Element & {
$html?: string
$cls?: string
- $clsi?: string
$sty?: NormalizedStyle | string | undefined
- $styi?: NormalizedStyle | undefined
- $dprops?: Record
}
export function setText(el: Node & { $txt?: string }, ...values: any[]): void {
@@ -48,10 +45,14 @@ export function setClass(el: TargetElement, value: any): void {
* 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
+export function setClassIncremental(
+ el: any,
+ value: any,
+ fallthrough?: boolean,
+): void {
+ const cacheKey = `$clsi${fallthrough ? '$' : ''}`
+ const prev = el[cacheKey]
+ if ((value = el[cacheKey] = normalizeClass(value)) !== prev) {
const nextList = value.split(/\s+/)
el.classList.add(...nextList)
if (prev) {
@@ -73,12 +74,18 @@ export function setStyle(el: TargetElement, value: any): void {
* 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)
+export function setStyleIncremental(
+ el: any,
+ value: any,
+ fallthrough?: boolean,
+): NormalizedStyle | undefined {
+ const cacheKey = `$styi${fallthrough ? '$' : ''}`
+ const prev = el[cacheKey]
+ value = el[cacheKey] = isString(value)
? parseStringStyle(value)
: (normalizeStyle(value) as NormalizedStyle | undefined)
patchStyle(el, prev, value)
+ return value
}
export function setAttr(el: any, key: string, value: any): void {
@@ -158,37 +165,25 @@ export function setDOMProp(el: any, key: string, value: any): void {
}
export function setDynamicProps(
- el: TargetElement,
+ el: any,
args: any[],
root = false,
+ fallthrough = false,
): void {
const props = args.length > 1 ? mergeProps(...args) : args[0]
- const oldProps = el.$dprops
+ const cacheKey = `$dprops${fallthrough ? '$' : ''}`
+ const prevKeys = el[cacheKey] as string[]
- if (oldProps) {
- for (const key in oldProps) {
- // TODO should these keys be allowed as dynamic keys? The current logic of the runtime-core will throw an error
- if (key === 'textContent' || key === 'innerHTML') {
- continue
- }
-
- const oldValue = oldProps[key]
- const hasNewValue = props[key] || props['.' + key] || props['^' + key]
- if (oldValue && !hasNewValue) {
- setDynamicProp(el, key, oldValue, null, root)
+ if (prevKeys) {
+ for (const key of prevKeys) {
+ if (!(key in props)) {
+ setDynamicProp(el, key, null, root, fallthrough)
}
}
}
- const prev = (el.$dprops = Object.create(null))
- for (const key in props) {
- setDynamicProp(
- el,
- key,
- oldProps ? oldProps[key] : undefined,
- (prev[key] = props[key]),
- root,
- )
+ for (const key of (el[cacheKey] = Object.keys(props))) {
+ setDynamicProp(el, key, props[key], root, fallthrough)
}
}
@@ -198,21 +193,21 @@ export function setDynamicProps(
export function setDynamicProp(
el: TargetElement,
key: string,
- prev: any,
value: any,
root?: boolean,
-): void {
+ fallthrough?: boolean,
+): any {
// TODO
const isSVG = false
if (key === 'class') {
if (root) {
- setClassIncremental(el, value)
+ return setClassIncremental(el, value, fallthrough)
} else {
setClass(el, value)
}
} else if (key === 'style') {
if (root) {
- setStyleIncremental(el, value)
+ return setStyleIncremental(el, value, fallthrough)
} else {
setStyle(el, value)
}
@@ -238,4 +233,5 @@ export function setDynamicProp(
// TODO special case for
setAttr(el, key, value)
}
+ return value
}