mirror of https://github.com/vuejs/core.git
wip(vapor): handle class / style merging behavior
This commit is contained in:
parent
4160b6d567
commit
f9a6e8cd58
|
@ -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('<div>')() as HTMLDivElement
|
||||
renderEffect(() => setDynamicProps(n0, [attrs], true))
|
||||
let prev: any
|
||||
renderEffect(() => {
|
||||
prev = setDynamicProps(n0, [attrs], prev, true)
|
||||
})
|
||||
return n0
|
||||
},
|
||||
})
|
||||
|
|
|
@ -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<any>()
|
||||
|
||||
|
@ -132,4 +141,135 @@ describe('attribute fallthrough', () => {
|
|||
await nextTick()
|
||||
expect(host.innerHTML).toBe('<div foo="2" id="b">2</div>')
|
||||
})
|
||||
|
||||
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<string | Record<string, string>> = ref('color:red')
|
||||
const parentStyle: Ref<string | null> = 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')
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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<string, unknown> {
|
|||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<string, any>
|
||||
}
|
||||
|
||||
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 <input v-model type="checkbox">
|
||||
setAttr(el, key, value)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue