Skip to content

Commit 8f23b4e

Browse files
committed
wip: style mismatch
1 parent 474cb7f commit 8f23b4e

File tree

4 files changed

+173
-76
lines changed

4 files changed

+173
-76
lines changed

packages/runtime-core/src/hydration.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -946,7 +946,7 @@ export function isSetEqual(a: Set<string>, b: Set<string>): boolean {
946946
return true
947947
}
948948

949-
function toStyleMap(str: string): Map<string, string> {
949+
export function toStyleMap(str: string): Map<string, string> {
950950
const styleMap: Map<string, string> = new Map()
951951
for (const item of str.split(';')) {
952952
let [key, value] = item.split(':')
@@ -959,7 +959,10 @@ function toStyleMap(str: string): Map<string, string> {
959959
return styleMap
960960
}
961961

962-
function isMapEqual(a: Map<string, string>, b: Map<string, string>): boolean {
962+
export function isMapEqual(
963+
a: Map<string, string>,
964+
b: Map<string, string>,
965+
): boolean {
963966
if (a.size !== b.size) {
964967
return false
965968
}

packages/runtime-core/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -571,4 +571,6 @@ export {
571571
toClassSet,
572572
isSetEqual,
573573
warnPropMismatch,
574+
toStyleMap,
575+
isMapEqual,
574576
} from './hydration'

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

Lines changed: 72 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -3121,22 +3121,26 @@ describe('Vapor Mode hydration', () => {
31213121
`<div :class="data"></div>`,
31223122
ref(['foo', 'bar']),
31233123
)
3124+
31243125
await mountWithHydration(
31253126
`<div class="foo bar"></div>`,
31263127
`<div :class="data"></div>`,
31273128
ref({ foo: true, bar: true }),
31283129
)
3130+
31293131
await mountWithHydration(
31303132
`<div class="foo bar"></div>`,
31313133
`<div :class="data"></div>`,
31323134
ref('foo bar'),
31333135
)
3136+
31343137
// svg classes
31353138
await mountWithHydration(
31363139
`<svg class="foo bar"></svg>`,
31373140
`<svg :class="data"></svg>`,
31383141
ref('foo bar'),
31393142
)
3143+
31403144
// class with different order
31413145
await mountWithHydration(
31423146
`<div class="foo bar"></div>`,
@@ -3163,48 +3167,74 @@ describe('Vapor Mode hydration', () => {
31633167
expect(container.innerHTML).toBe('<div class="foo"></div><span></span>')
31643168
expect(`Hydration class mismatch`).toHaveBeenWarned()
31653169
})
3166-
// test('style mismatch', () => {
3167-
// mountWithHydration(`<div style="color:red;"></div>`, () =>
3168-
// h('div', { style: { color: 'red' } }),
3169-
// )
3170-
// mountWithHydration(`<div style="color:red;"></div>`, () =>
3171-
// h('div', { style: `color:red;` }),
3172-
// )
3173-
// mountWithHydration(
3174-
// `<div style="color:red; font-size: 12px;"></div>`,
3175-
// () => h('div', { style: `font-size: 12px; color:red;` }),
3176-
// )
3177-
// mountWithHydration(`<div style="color:red;display:none;"></div>`, () =>
3178-
// withDirectives(createVNode('div', { style: 'color: red' }, ''), [
3179-
// [vShow, false],
3180-
// ]),
3181-
// )
3182-
// expect(`Hydration style mismatch`).not.toHaveBeenWarned()
3183-
// mountWithHydration(`<div style="color:red;"></div>`, () =>
3184-
// h('div', { style: { color: 'green' } }),
3185-
// )
3186-
// expect(`Hydration style mismatch`).toHaveBeenWarnedTimes(1)
3187-
// })
3188-
// test('style mismatch when no style attribute is present', () => {
3189-
// mountWithHydration(`<div></div>`, () =>
3190-
// h('div', { style: { color: 'red' } }),
3191-
// )
3192-
// expect(`Hydration style mismatch`).toHaveBeenWarnedTimes(1)
3193-
// })
3194-
// test('style mismatch w/ v-show', () => {
3195-
// mountWithHydration(`<div style="color:red;display:none"></div>`, () =>
3196-
// withDirectives(createVNode('div', { style: 'color: red' }, ''), [
3197-
// [vShow, false],
3198-
// ]),
3199-
// )
3200-
// expect(`Hydration style mismatch`).not.toHaveBeenWarned()
3201-
// mountWithHydration(`<div style="color:red;"></div>`, () =>
3202-
// withDirectives(createVNode('div', { style: 'color: red' }, ''), [
3203-
// [vShow, false],
3204-
// ]),
3205-
// )
3206-
// expect(`Hydration style mismatch`).toHaveBeenWarnedTimes(1)
3207-
// })
3170+
3171+
test('style mismatch', async () => {
3172+
await mountWithHydration(
3173+
`<div style="color:red;"></div>`,
3174+
`<div :style="data"></div>`,
3175+
ref({ color: 'red' }),
3176+
)
3177+
3178+
await mountWithHydration(
3179+
`<div style="color:red;"></div>`,
3180+
`<div :style="data"></div>`,
3181+
ref('color:red;'),
3182+
)
3183+
3184+
// style with different order
3185+
await mountWithHydration(
3186+
`<div style="color:red; font-size: 12px;"></div>`,
3187+
`<div :style="data"></div>`,
3188+
ref(`font-size: 12px; color:red;`),
3189+
)
3190+
3191+
expect(`Hydration style mismatch`).not.toHaveBeenWarned()
3192+
3193+
// single root mismatch
3194+
const { container: root } = await mountWithHydration(
3195+
`<div style="color:red;"></div>`,
3196+
`<div :style="data"></div>`,
3197+
ref({ color: 'green' }),
3198+
)
3199+
expect(root.innerHTML).toBe('<div style="color: green;"></div>')
3200+
expect(`Hydration style mismatch`).toHaveBeenWarned()
3201+
3202+
// multiple root mismatch
3203+
const { container } = await mountWithHydration(
3204+
`<div style="color:red;"></div><span/>`,
3205+
`<div :style="data"></div><span/>`,
3206+
ref({ color: 'green' }),
3207+
)
3208+
expect(container.innerHTML).toBe(
3209+
'<div style="color: green;"></div><span></span>',
3210+
)
3211+
expect(`Hydration style mismatch`).toHaveBeenWarned()
3212+
})
3213+
3214+
test('style mismatch when no style attribute is present', async () => {
3215+
await mountWithHydration(
3216+
`<div></div>`,
3217+
`<div :style="data"></div>`,
3218+
ref({ color: 'red' }),
3219+
)
3220+
expect(`Hydration style mismatch`).toHaveBeenWarnedTimes(1)
3221+
})
3222+
3223+
test.todo('style mismatch w/ v-show', () => {
3224+
// mountWithHydration(`<div style="color:red;display:none"></div>`, () =>
3225+
// withDirectives(createVNode('div', { style: 'color: red' }, ''), [
3226+
// [vShow, false],
3227+
// ]),
3228+
// )
3229+
// expect(`Hydration style mismatch`).not.toHaveBeenWarned()
3230+
// mountWithHydration(`<div style="color:red;"></div>`, () =>
3231+
// withDirectives(createVNode('div', { style: 'color: red' }, ''), [
3232+
// [vShow, false],
3233+
// ]),
3234+
// )
3235+
// expect(`Hydration style mismatch`).toHaveBeenWarnedTimes(1)
3236+
})
3237+
32083238
// test('attr mismatch', () => {
32093239
// mountWithHydration(`<div id="foo"></div>`, () => h('div', { id: 'foo' }))
32103240
// mountWithHydration(`<div spellcheck></div>`, () =>

packages/runtime-vapor/src/dom/prop.ts

Lines changed: 94 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,21 @@ import {
66
normalizeClass,
77
normalizeStyle,
88
parseStringStyle,
9+
stringifyStyle,
910
toDisplayString,
1011
} from '@vue/shared'
1112
import { on } from './event'
1213
import {
1314
MismatchTypes,
1415
currentInstance,
16+
isMapEqual,
1517
isMismatchAllowed,
1618
isSetEqual,
1719
mergeProps,
1820
patchStyle,
1921
shouldSetAsProp,
2022
toClassSet,
23+
toStyleMap,
2124
warn,
2225
warnPropMismatch,
2326
} from '@vue/runtime-dom'
@@ -116,40 +119,60 @@ export function setDOMProp(el: any, key: string, value: any): void {
116119
}
117120

118121
export function setClass(el: TargetElement, value: any): void {
119-
if (isHydrating) {
120-
const actual = el.getAttribute('class')
121-
const actualClassSet = toClassSet(actual || '')
122-
let expected = normalizeClass(value)
123-
const expectedClassSet = toClassSet(expected)
124-
let hasMismatch = false
125-
if (el.$root) {
126-
hasMismatch = Array.from(expectedClassSet).some(
127-
cls => !actualClassSet.has(cls),
128-
)
129-
if (hasMismatch) {
130-
setClassIncremental(el, value)
131-
expected = el.getAttribute('class')!
122+
if (el.$root) {
123+
setClassIncremental(el, value)
124+
} else {
125+
if (
126+
(__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
127+
isHydrating
128+
) {
129+
const actual = el.getAttribute('class')
130+
const actualClassSet = toClassSet(actual || '')
131+
const expected = normalizeClass(value)
132+
const expectedClassSet = toClassSet(expected)
133+
if (!isSetEqual(actualClassSet, expectedClassSet)) {
134+
warnPropMismatch(el, 'class', MismatchTypes.CLASS, actual, expected)
135+
logMismatchError()
136+
el.className = expected
132137
}
133-
}
134138

135-
if (hasMismatch || !isSetEqual(actualClassSet, expectedClassSet)) {
136-
warnPropMismatch(el, 'class', MismatchTypes.CLASS, actual, expected)
137-
if (!el.$root) el.className = expected
139+
el.$cls = expected
140+
return
138141
}
139142

140-
if (!el.$root) el.$cls = expected
141-
return
142-
}
143-
144-
if (el.$root) {
145-
setClassIncremental(el, value)
146-
} else if ((value = normalizeClass(value)) !== el.$cls) {
147-
el.className = el.$cls = value
143+
if ((value = normalizeClass(value)) !== el.$cls) {
144+
el.className = el.$cls = value
145+
}
148146
}
149147
}
150148

151149
function setClassIncremental(el: any, value: any): void {
152150
const cacheKey = `$clsi${isApplyingFallthroughProps ? '$' : ''}`
151+
152+
if ((__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) && isHydrating) {
153+
const actual = el.getAttribute('class')
154+
const actualClassSet = toClassSet(actual || '')
155+
const expected = normalizeClass(value)
156+
const expectedClassSet = toClassSet(expected)
157+
// check if the expected classes are present in the actual classes
158+
const hasMismatch = Array.from(expectedClassSet).some(
159+
cls => !actualClassSet.has(cls),
160+
)
161+
if (hasMismatch) {
162+
warnPropMismatch(el, 'class', MismatchTypes.CLASS, actual, expected)
163+
logMismatchError()
164+
165+
const nextList = value.split(/\s+/)
166+
if (value) {
167+
el.classList.add(...nextList)
168+
}
169+
} else {
170+
el[cacheKey] = expected
171+
}
172+
173+
return
174+
}
175+
153176
const prev = el[cacheKey]
154177
if ((value = el[cacheKey] = normalizeClass(value)) !== prev) {
155178
const nextList = value.split(/\s+/)
@@ -168,20 +191,59 @@ export function setStyle(el: TargetElement, value: any): void {
168191
if (el.$root) {
169192
setStyleIncremental(el, value)
170193
} else {
171-
const prev = el.$sty
172-
value = el.$sty = normalizeStyle(value)
173-
patchStyle(el, prev, value)
194+
if (
195+
(__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) &&
196+
isHydrating
197+
) {
198+
const actual = el.getAttribute('style')
199+
const actualStyleMap = toStyleMap(actual || '')
200+
const normalizedValue = normalizeStyle(value)
201+
const expected = stringifyStyle(normalizedValue)
202+
const expectedStyleMap = toStyleMap(expected)
203+
204+
// TODO: handle v-show="false"
205+
// TODO: handle css vars
206+
207+
if (!isMapEqual(actualStyleMap, expectedStyleMap)) {
208+
warnPropMismatch(el, 'style', MismatchTypes.STYLE, actual, expected)
209+
logMismatchError()
210+
patchStyle(el, el.$sty, (el.$sty = normalizedValue))
211+
}
212+
return
213+
}
214+
215+
patchStyle(el, el.$sty, (el.$sty = normalizeStyle(value)))
174216
}
175217
}
176218

177219
function setStyleIncremental(el: any, value: any): NormalizedStyle | undefined {
178220
const cacheKey = `$styi${isApplyingFallthroughProps ? '$' : ''}`
179-
const prev = el[cacheKey]
180-
value = el[cacheKey] = isString(value)
221+
const normalizedValue = isString(value)
181222
? parseStringStyle(value)
182223
: (normalizeStyle(value) as NormalizedStyle | undefined)
183-
patchStyle(el, prev, value)
184-
return value
224+
225+
if ((__DEV__ || __FEATURE_PROD_HYDRATION_MISMATCH_DETAILS__) && isHydrating) {
226+
const actual = el.getAttribute('style')
227+
const actualStyleMap = toStyleMap(actual || '')
228+
const expected = isString(value) ? value : stringifyStyle(normalizedValue)
229+
const expectedStyleMap = toStyleMap(expected)
230+
231+
// TODO: handle v-show="false"
232+
// TODO: handle css vars
233+
234+
// check if the expected styles are present in the actual styles
235+
const hasMismatch = Array.from(expectedStyleMap.entries()).some(
236+
([key, val]) => actualStyleMap.get(key) !== val,
237+
)
238+
if (hasMismatch) {
239+
warnPropMismatch(el, 'style', MismatchTypes.STYLE, actual, expected)
240+
logMismatchError()
241+
patchStyle(el, el[cacheKey], (el[cacheKey] = normalizedValue))
242+
}
243+
return
244+
}
245+
246+
patchStyle(el, el[cacheKey], (el[cacheKey] = normalizedValue))
185247
}
186248

187249
export function setValue(el: TargetElement, value: any): void {

0 commit comments

Comments
 (0)