import {
+ child,
+ createComponent,
createVaporSSRApp,
defineVaporAsyncComponent,
+ defineVaporComponent,
delegateEvents,
+ renderEffect,
+ setStyle,
+ template,
+ useVaporCssVars,
} from '../src'
import { defineAsyncComponent, nextTick, reactive, ref } from '@vue/runtime-dom'
import { isString } from '@vue/shared'
expect(`Hydration attribute mismatch`).not.toHaveBeenWarned()
})
- test.todo('should not warn css v-bind', () => {
- // const container = document.createElement('div')
- // container.innerHTML = `<div style="--foo:red;color:var(--foo);" />`
- // const app = createSSRApp({
- // setup() {
- // useCssVars(() => ({
- // foo: 'red',
- // }))
- // return () => h('div', { style: { color: 'var(--foo)' } })
- // },
- // })
- // app.mount(container)
- // expect(`Hydration style mismatch`).not.toHaveBeenWarned()
+ test('should not warn css v-bind', async () => {
+ const container = document.createElement('div')
+ container.innerHTML = `<div style="--foo:red;color:var(--foo);" />`
+ const app = createVaporSSRApp({
+ setup() {
+ useVaporCssVars(() => ({ foo: 'red' }))
+ const n0 = template('<div></div>', true)() as any
+ renderEffect(() => setStyle(n0, { color: 'var(--foo)' }))
+ return n0
+ },
+ })
+ app.mount(container)
+ expect(`Hydration style mismatch`).not.toHaveBeenWarned()
})
- test.todo(
- 'css vars should only be added to expected on component root dom',
- () => {
- // const container = document.createElement('div')
- // container.innerHTML = `<div style="--foo:red;"><div style="color:var(--foo);" /></div>`
- // const app = createSSRApp({
- // setup() {
- // useCssVars(() => ({
- // foo: 'red',
- // }))
- // return () =>
- // h('div', null, [h('div', { style: { color: 'var(--foo)' } })])
- // },
- // })
- // app.mount(container)
- // expect(`Hydration style mismatch`).not.toHaveBeenWarned()
- },
- )
+ test('css vars should only be added to expected on component root dom', () => {
+ const container = document.createElement('div')
+ container.innerHTML = `<div style="--foo:red;"><div style="color:var(--foo);" /></div>`
+ const app = createVaporSSRApp({
+ setup() {
+ useVaporCssVars(() => ({ foo: 'red' }))
+ const n0 = template('<div><div></div></div>', true)() as any
+ const n1 = child(n0) as any
+ renderEffect(() => setStyle(n1, { color: 'var(--foo)' }))
+ return n0
+ },
+ })
+ app.mount(container)
+ expect(`Hydration style mismatch`).not.toHaveBeenWarned()
+ })
- test.todo('css vars support fallthrough', () => {
- // const container = document.createElement('div')
- // container.innerHTML = `<div style="padding: 4px;--foo:red;"></div>`
- // const app = createSSRApp({
- // setup() {
- // useCssVars(() => ({
- // foo: 'red',
- // }))
- // return () => h(Child)
- // },
- // })
- // const Child = {
- // setup() {
- // return () => h('div', { style: 'padding: 4px' })
- // },
- // }
- // app.mount(container)
- // expect(`Hydration style mismatch`).not.toHaveBeenWarned()
+ test('css vars support fallthrough', () => {
+ const container = document.createElement('div')
+ container.innerHTML = `<div style="padding: 4px;--foo:red;"></div>`
+ const app = createVaporSSRApp({
+ setup() {
+ useVaporCssVars(() => ({ foo: 'red' }))
+ return createComponent(Child)
+ },
+ })
+ const Child = defineVaporComponent({
+ setup() {
+ const n0 = template('<div></div>', true)() as any
+ renderEffect(() => setStyle(n0, { padding: '4px' }))
+ return n0
+ },
+ })
+ app.mount(container)
+ expect(`Hydration style mismatch`).not.toHaveBeenWarned()
})
// vapor directive does not have a created hook
// expect(`Hydration style mismatch`).not.toHaveBeenWarned()
})
- test.todo('escape css var name', () => {
- // const container = document.createElement('div')
- // container.innerHTML = `<div style="padding: 4px;--foo\\.bar:red;"></div>`
- // const app = createSSRApp({
- // setup() {
- // useCssVars(() => ({
- // 'foo.bar': 'red',
- // }))
- // return () => h(Child)
- // },
- // })
- // const Child = {
- // setup() {
- // return () => h('div', { style: 'padding: 4px' })
- // },
- // }
- // app.mount(container)
- // expect(`Hydration style mismatch`).not.toHaveBeenWarned()
+ test('escape css var name', () => {
+ const container = document.createElement('div')
+ container.innerHTML = `<div style="padding: 4px;--foo\\.bar:red;"></div>`
+ const app = createVaporSSRApp({
+ setup() {
+ useVaporCssVars(() => ({ 'foo.bar': 'red' }))
+ return createComponent(Child)
+ },
+ })
+ const Child = defineVaporComponent({
+ setup() {
+ const n0 = template('<div></div>', true)() as any
+ renderEffect(() => setStyle(n0, { padding: '4px' }))
+ return n0
+ },
+ })
+ app.mount(container)
+ expect(`Hydration style mismatch`).not.toHaveBeenWarned()
})
})
type NormalizedStyle,
camelize,
canSetValueDirectly,
+ getEscapedCssVarName,
includeBooleanAttr,
isArray,
isOn,
isString,
normalizeClass,
+ normalizeCssVarValue,
normalizeStyle,
parseStringStyle,
stringifyStyle,
} from '@vue/shared'
import { on } from './event'
import {
+ type GenericComponentInstance,
MismatchTypes,
currentInstance,
getAttributeMismatch,
isValidHtmlOrSvgAttribute,
mergeProps,
patchStyle,
+ queuePostFlushCb,
shouldSetAsProp,
toClassSet,
toStyleMap,
isVaporComponent,
} from '../component'
import { isHydrating, logMismatchError } from './hydration'
-import type { Block } from '../block'
+import { type Block, normalizeBlock } from '../block'
import type { VaporElement } from '../apiDefineVaporCustomElement'
type TargetElement = Element & {
}
}
+/**
+ * dev only
+ * if a component uses style v-bind, or an el's style contains style vars (potentially
+ * fallthrough from parent components), then style matching checks must be deferred until
+ * after hydration (when instance.block is set). At this point, it can be confirmed
+ * whether the el is at the root level of the component.
+ */
+function shouldDeferCheckStyleMismatch(el: TargetElement): boolean {
+ return (
+ __DEV__ &&
+ (!!currentInstance!.getCssVars ||
+ Object.values((el as HTMLElement).style).some(v => v.startsWith('--')))
+ )
+}
+
export function setStyle(el: TargetElement, value: any): void {
if (el.$root) {
setStyleIncremental(el, value)
const normalizedValue = normalizeStyle(value)
if (
(__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
- isHydrating &&
- !styleHasMismatch(el, value, normalizedValue, false)
+ isHydrating
) {
- el.$sty = normalizedValue
- return
+ if (shouldDeferCheckStyleMismatch(el)) {
+ const instance = currentInstance as VaporComponentInstance
+ queuePostFlushCb(() => {
+ if (!styleHasMismatch(el, value, normalizedValue, false, instance)) {
+ el.$sty = normalizedValue
+ return
+ }
+ patchStyle(el, el.$sty, (el.$sty = normalizedValue))
+ })
+ return
+ } else if (!styleHasMismatch(el, value, normalizedValue, false)) {
+ el.$sty = normalizedValue
+ return
+ }
}
patchStyle(el, el.$sty, (el.$sty = normalizedValue))
? parseStringStyle(value)
: (normalizeStyle(value) as NormalizedStyle | undefined)
- if (
- (__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
- isHydrating &&
- !styleHasMismatch(el, value, normalizedValue, true)
- ) {
- el[cacheKey] = normalizedValue
- return
+ if ((__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) && isHydrating) {
+ if (shouldDeferCheckStyleMismatch(el)) {
+ const instance = currentInstance as VaporComponentInstance
+ queuePostFlushCb(() => {
+ if (!styleHasMismatch(el, value, normalizedValue, true, instance)) {
+ el[cacheKey] = normalizedValue
+ return
+ }
+ patchStyle(el, el[cacheKey], (el[cacheKey] = normalizedValue))
+ })
+ return
+ } else if (!styleHasMismatch(el, value, normalizedValue, true)) {
+ el[cacheKey] = normalizedValue
+ return
+ }
}
patchStyle(el, el[cacheKey], (el[cacheKey] = normalizedValue))
value: any,
normalizedValue: string | NormalizedStyle | undefined,
isIncremental: boolean,
+ instance = currentInstance,
): boolean {
const actual = el.getAttribute('style')
const actualStyleMap = toStyleMap(actual || '')
expectedStyleMap.set('display', 'none')
}
- // TODO: handle css vars
+ // handle css vars
+ if (instance) {
+ resolveCssVars(instance as VaporComponentInstance, el, expectedStyleMap)
+ }
let hasMismatch: boolean = false
if (isIncremental) {
return false
}
+/**
+ * dev only
+ */
+function resolveCssVars(
+ instance: VaporComponentInstance,
+ block: Block,
+ expectedMap: Map<string, string>,
+): void {
+ if (__DEV__ && !instance.isMounted) {
+ throw new Error(
+ 'resolveCssVars should NOT be called before component is mounted',
+ )
+ }
+
+ const rootBlocks = normalizeBlock(instance)
+ if (
+ (instance as GenericComponentInstance).getCssVars &&
+ normalizeBlock(block).every(b => rootBlocks.includes(b))
+ ) {
+ const cssVars = (instance as GenericComponentInstance).getCssVars!()
+ for (const key in cssVars) {
+ const value = normalizeCssVarValue(cssVars[key])
+ expectedMap.set(`--${getEscapedCssVarName(key, false)}`, value)
+ }
+ }
+
+ if (
+ normalizeBlock(block).every(b => rootBlocks.includes(b)) &&
+ instance.parent
+ ) {
+ resolveCssVars(
+ instance.parent as VaporComponentInstance,
+ instance.block,
+ expectedMap,
+ )
+ }
+}
+
function attributeHasMismatch(el: any, key: string, value: any): boolean {
if (isValidHtmlOrSvgAttribute(el, key)) {
const { actual, expected } = getAttributeMismatch(el, key, value)