Skip to content

Commit f9a6e8c

Browse files
committed
wip(vapor): handle class / style merging behavior
1 parent 4160b6d commit f9a6e8c

File tree

6 files changed

+217
-50
lines changed

6 files changed

+217
-50
lines changed

packages/runtime-vapor/__tests__/apiSetupContext.spec.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,12 @@ describe('api: setup context', () => {
7272

7373
const Child = defineVaporComponent({
7474
inheritAttrs: false,
75-
setup(props, { attrs }) {
75+
setup(_props, { attrs }) {
7676
const el = document.createElement('div')
77-
renderEffect(() => setDynamicProps(el, [attrs]))
77+
let prev: any
78+
renderEffect(() => {
79+
prev = setDynamicProps(el, [attrs], prev, true)
80+
})
7881
return el
7982
},
8083
})
@@ -110,7 +113,10 @@ describe('api: setup context', () => {
110113
const n0 = createComponent(Wrapper, null, {
111114
default: () => {
112115
const n0 = template('<div>')() as HTMLDivElement
113-
renderEffect(() => setDynamicProps(n0, [attrs], true))
116+
let prev: any
117+
renderEffect(() => {
118+
prev = setDynamicProps(n0, [attrs], prev, true)
119+
})
114120
return n0
115121
},
116122
})

packages/runtime-vapor/__tests__/componentAttrs.spec.ts

Lines changed: 142 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
1-
import { nextTick, ref, watchEffect } from '@vue/runtime-dom'
2-
import { createComponent, setText, template } from '../src'
1+
import { type Ref, nextTick, ref, watchEffect } from '@vue/runtime-dom'
2+
import {
3+
createComponent,
4+
defineVaporComponent,
5+
renderEffect,
6+
setClassIncremental,
7+
setStyleIncremental,
8+
setText,
9+
template,
10+
} from '../src'
311
import { makeRender } from './_utils'
12+
import { stringifyStyle } from '@vue/shared'
413

514
const define = makeRender<any>()
615

@@ -132,4 +141,135 @@ describe('attribute fallthrough', () => {
132141
await nextTick()
133142
expect(host.innerHTML).toBe('<div foo="2" id="b">2</div>')
134143
})
144+
145+
it('should merge classes', async () => {
146+
const rootClass = ref('root')
147+
const parentClass = ref('parent')
148+
const childClass = ref('child')
149+
150+
const Child = defineVaporComponent({
151+
setup() {
152+
const n = document.createElement('div')
153+
renderEffect(() => {
154+
// binding on template root generates incremental class setter
155+
setClassIncremental(n, childClass.value)
156+
})
157+
return n
158+
},
159+
})
160+
161+
const Parent = defineVaporComponent({
162+
setup() {
163+
return createComponent(
164+
Child,
165+
{
166+
class: () => parentClass.value,
167+
},
168+
null,
169+
true, // pass single root flag
170+
)
171+
},
172+
})
173+
174+
const { host } = define({
175+
setup() {
176+
return createComponent(Parent, {
177+
class: () => rootClass.value,
178+
})
179+
},
180+
}).render()
181+
182+
const list = host.children[0].classList
183+
// assert classes without being order-sensitive
184+
function assertClasses(cls: string[]) {
185+
expect(list.length).toBe(cls.length)
186+
for (const c of cls) {
187+
expect(list.contains(c)).toBe(true)
188+
}
189+
}
190+
191+
assertClasses(['root', 'parent', 'child'])
192+
193+
rootClass.value = 'root1'
194+
await nextTick()
195+
assertClasses(['root1', 'parent', 'child'])
196+
197+
parentClass.value = 'parent1'
198+
await nextTick()
199+
assertClasses(['root1', 'parent1', 'child'])
200+
201+
childClass.value = 'child1'
202+
await nextTick()
203+
assertClasses(['root1', 'parent1', 'child1'])
204+
})
205+
206+
it('should merge styles', async () => {
207+
const rootStyle: Ref<string | Record<string, string>> = ref('color:red')
208+
const parentStyle: Ref<string | null> = ref('font-size:12px')
209+
const childStyle = ref('font-weight:bold')
210+
211+
const Child = defineVaporComponent({
212+
setup() {
213+
const n = document.createElement('div')
214+
renderEffect(() => {
215+
// binding on template root generates incremental class setter
216+
setStyleIncremental(n, childStyle.value)
217+
})
218+
return n
219+
},
220+
})
221+
222+
const Parent = defineVaporComponent({
223+
setup() {
224+
return createComponent(
225+
Child,
226+
{
227+
style: () => parentStyle.value,
228+
},
229+
null,
230+
true, // pass single root flag
231+
)
232+
},
233+
})
234+
235+
const { host } = define({
236+
setup() {
237+
return createComponent(Parent, {
238+
style: () => rootStyle.value,
239+
})
240+
},
241+
}).render()
242+
243+
const el = host.children[0] as HTMLElement
244+
245+
function getCSS() {
246+
return el.style.cssText.replace(/\s+/g, '')
247+
}
248+
249+
function assertStyles() {
250+
const css = getCSS()
251+
expect(css).toContain(stringifyStyle(rootStyle.value))
252+
if (parentStyle.value) {
253+
expect(css).toContain(stringifyStyle(parentStyle.value))
254+
}
255+
expect(css).toContain(stringifyStyle(childStyle.value))
256+
}
257+
258+
assertStyles()
259+
260+
rootStyle.value = { color: 'green' }
261+
await nextTick()
262+
assertStyles()
263+
expect(getCSS()).not.toContain('color:red')
264+
265+
parentStyle.value = null
266+
await nextTick()
267+
assertStyles()
268+
expect(getCSS()).not.toContain('font-size:12px')
269+
270+
childStyle.value = 'font-weight:500'
271+
await nextTick()
272+
assertStyles()
273+
expect(getCSS()).not.toContain('font-size:bold')
274+
})
135275
})

packages/runtime-vapor/__tests__/dom/prop.spec.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -305,13 +305,12 @@ describe('patchProp', () => {
305305

306306
describe('setDynamicProp', () => {
307307
const element = document.createElement('div')
308-
let prev: any
309308
function setDynamicProp(
310309
key: string,
311310
value: any,
312311
el = element.cloneNode(true) as HTMLElement,
313312
) {
314-
prev = _setDynamicProp(el, key, prev, value)
313+
_setDynamicProp(el, key, value)
315314
return el
316315
}
317316

packages/runtime-vapor/src/component.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,12 @@ export function createComponent(
210210
Object.keys(instance.attrs).length
211211
) {
212212
renderEffect(() => {
213-
setDynamicProps(instance.block as Element, [instance.attrs])
213+
setDynamicProps(
214+
instance.block as Element,
215+
[instance.attrs],
216+
true, // root
217+
true, // fallthrough
218+
)
214219
})
215220
}
216221

@@ -421,7 +426,7 @@ export function createComponentWithFallback(
421426

422427
if (rawProps) {
423428
renderEffect(() => {
424-
setDynamicProps(el, [resolveDynamicProps(rawProps)])
429+
setDynamicProps(el, [resolveDynamicProps(rawProps)], isSingleRoot)
425430
})
426431
}
427432

packages/runtime-vapor/src/componentProps.ts

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
YES,
55
camelize,
66
hasOwn,
7+
isArray,
78
isFunction,
89
isString,
910
} from '@vue/shared'
@@ -171,6 +172,8 @@ export function getPropsProxyHandlers(
171172

172173
export function getAttrFromRawProps(rawProps: RawProps, key: string): unknown {
173174
if (key === '$') return
175+
// need special merging behavior for class & style
176+
const merged = key === 'class' || key === 'style' ? ([] as any[]) : undefined
174177
const dynamicSources = rawProps.$
175178
if (dynamicSources) {
176179
let i = dynamicSources.length
@@ -180,13 +183,23 @@ export function getAttrFromRawProps(rawProps: RawProps, key: string): unknown {
180183
isDynamic = isFunction(source)
181184
source = isDynamic ? (source as Function)() : source
182185
if (hasOwn(source, key)) {
183-
return isDynamic ? source[key] : source[key]()
186+
const value = isDynamic ? source[key] : source[key]()
187+
if (merged) {
188+
merged.push(value)
189+
} else {
190+
return value
191+
}
184192
}
185193
}
186194
}
187195
if (hasOwn(rawProps, key)) {
188-
return rawProps[key]()
196+
if (merged) {
197+
merged.push(rawProps[key]())
198+
} else {
199+
return rawProps[key]()
200+
}
189201
}
202+
return merged
190203
}
191204

192205
export function hasAttrFromRawProps(rawProps: RawProps, key: string): boolean {
@@ -299,9 +312,17 @@ export function resolveDynamicProps(props: RawProps): Record<string, unknown> {
299312
const isDynamic = isFunction(source)
300313
const resolved = isDynamic ? source() : source
301314
for (const key in resolved) {
302-
mergedRawProps[key] = isDynamic
303-
? resolved[key]
304-
: (resolved[key] as Function)()
315+
const value = isDynamic ? resolved[key] : (resolved[key] as Function)()
316+
if (key === 'class' || key === 'style') {
317+
const existing = mergedRawProps[key]
318+
if (isArray(existing)) {
319+
existing.push(value)
320+
} else {
321+
mergedRawProps[key] = [existing, value]
322+
}
323+
} else {
324+
mergedRawProps[key] = value
325+
}
305326
}
306327
}
307328
}

0 commit comments

Comments
 (0)