normalScriptDefaultVar,
processNormalScript,
} from './script/normalScript'
-import { CSS_VARS_HELPER, genCssVarsCode } from './style/cssVars'
+import { genCssVarsCode, getCssVarsHelper } from './style/cssVars'
import {
type SFCTemplateCompileOptions,
compileTemplate,
// no need to do this when targeting SSR
!ssr
) {
- ctx.helperImports.add(CSS_VARS_HELPER)
+ ctx.helperImports.add(getCssVarsHelper(vapor))
ctx.helperImports.add('unref')
ctx.s.prependLeft(
startOffset,
ctx.bindingMetadata,
scopeId,
!!options.isProd,
+ vapor,
)}\n`,
)
}
export const CSS_VARS_HELPER = `useCssVars`
+export function getCssVarsHelper(vapor: boolean | undefined): string {
+ return vapor ? `useVaporCssVars` : CSS_VARS_HELPER
+}
+
export function genCssVarsFromList(
vars: string[],
id: string,
bindings: BindingMetadata,
id: string,
isProd: boolean,
+ vapor?: boolean,
) {
const varsExp = genCssVarsFromList(vars, id, isProd)
const exp = createSimpleExpression(varsExp, false)
})
.join('')
- return `_${CSS_VARS_HELPER}(_ctx => (${transformedString}))`
+ return `_${getCssVarsHelper(vapor)}(_ctx => (${transformedString}))`
}
// <script setup> already gets the calls injected as part of the transform
* @internal
*/
suspense: SuspenseBoundary | null
+ /**
+ * `updateTeleportCssVars`
+ * For updating css vars on contained teleports
+ * @internal
+ */
+ ut?: (vars?: Record<string, string>) => void
+ /**
+ * dev only. For style v-bind hydration mismatch checks
+ * @internal
+ */
+ getCssVars?: () => Record<string, string>
// lifecycle
/**
* @internal
*/
n?: () => Promise<void>
- /**
- * `updateTeleportCssVars`
- * For updating css vars on contained teleports
- * @internal
- */
- ut?: (vars?: Record<string, unknown>) => void
-
- /**
- * dev only. For style v-bind hydration mismatch checks
- * @internal
- */
- getCssVars?: () => Record<string, unknown>
/**
* v2 compat only, for caching mutated $options
import {
Fragment,
+ type GenericComponentInstance,
Static,
type VNode,
getCurrentInstance,
): void {
if (!__BROWSER__ && !__TEST__) return
- const instance = getCurrentInstance()
- /* v8 ignore start */
- if (!instance) {
- __DEV__ &&
- warn(`useCssVars is called without current active component instance.`)
- return
- }
- /* v8 ignore stop */
-
- const updateTeleports = (instance.ut = (vars = getter(instance.proxy)) => {
- Array.from(
- document.querySelectorAll(`[data-v-owner="${instance.uid}"]`),
- ).forEach(node => setVarsOnNode(node, vars))
- })
-
- if (__DEV__) {
- instance.getCssVars = () => getter(instance.proxy)
- }
-
- const setVars = () => {
- const vars = getter(instance.proxy)
+ const instance = getCurrentInstance()! // to be check in baseUseCssVars
+ const getVars = () => getter(instance.proxy)
+ const setVars = (vars: Record<string, any>) => {
if (instance.ce) {
setVarsOnNode(instance.ce as any, vars)
} else {
setVarsOnVNode(instance.subTree, vars)
}
- updateTeleports(vars)
}
- // handle cases where child component root is affected
- // and triggers reflow in onMounted
- onBeforeUpdate(() => {
- queuePostFlushCb(setVars)
- })
-
- onMounted(() => {
- // run setVars synchronously here, but run as post-effect on changes
- watch(setVars, NOOP, { flush: 'post' })
- const ob = new MutationObserver(setVars)
- ob.observe(instance.subTree.el!.parentNode, { childList: true })
- onUnmounted(() => ob.disconnect())
- })
+ baseUseCssVars(
+ instance as GenericComponentInstance,
+ () => instance.subTree.el!.parentNode!,
+ getVars,
+ setVars,
+ )
}
-function setVarsOnVNode(vnode: VNode, vars: Record<string, unknown>) {
+function setVarsOnVNode(vnode: VNode, vars: Record<string, string>) {
if (__FEATURE_SUSPENSE__ && vnode.shapeFlag & ShapeFlags.SUSPENSE) {
const suspense = vnode.suspense!
vnode = suspense.activeBranch!
}
}
-function setVarsOnNode(el: Node, vars: Record<string, unknown>) {
+/**
+ * @internal
+ * shared between vdom and vapor
+ */
+export function baseUseCssVars(
+ instance: GenericComponentInstance | null,
+ getParentNode: () => Node,
+ getVars: () => Record<string, any>,
+ setVars: (vars: Record<string, any>) => void,
+): void {
+ /* v8 ignore start */
+ if (!instance) {
+ __DEV__ &&
+ warn(`useCssVars is called without current active component instance.`)
+ return
+ }
+ /* v8 ignore stop */
+
+ if (__DEV__) {
+ instance.getCssVars = getVars
+ }
+
+ const updateTeleports = (instance.ut = (vars = getVars()) => {
+ Array.from(
+ document.querySelectorAll(`[data-v-owner="${instance.uid}"]`),
+ ).forEach(node => setVarsOnNode(node, vars))
+ })
+
+ const applyCssCars = () => {
+ const vars = getVars()
+ setVars(vars)
+ updateTeleports(vars)
+ }
+
+ // handle cases where child component root is affected
+ // and triggers reflow in onMounted
+ onBeforeUpdate(() => {
+ queuePostFlushCb(applyCssCars)
+ })
+
+ onMounted(() => {
+ // run setVars synchronously here, but run as post-effect on changes
+ watch(applyCssCars, NOOP, { flush: 'post' })
+ const ob = new MutationObserver(applyCssCars)
+ ob.observe(getParentNode(), { childList: true })
+ onUnmounted(() => ob.disconnect())
+ })
+}
+
+/**
+ * @internal
+ * shared between vdom and vapor
+ */
+export function setVarsOnNode(el: Node, vars: Record<string, string>): void {
if (el.nodeType === 1) {
const style = (el as HTMLElement).style
let cssText = ''
* @internal
*/
export { shouldSetAsProp } from './patchProp'
+/**
+ * @internal
+ */
+export { baseUseCssVars, setVarsOnNode } from './helpers/useCssVars'
/**
* @internal
*/
--- /dev/null
+import {
+ VaporTeleport,
+ createComponent,
+ createIf,
+ createPlainElement,
+ defineVaporComponent,
+ defineVaporCustomElement,
+ renderEffect,
+ setStyle,
+ template,
+ useVaporCssVars,
+ withVaporCtx,
+} from '@vue/runtime-vapor'
+import { nextTick, onMounted, reactive, ref } from '@vue/runtime-core'
+import { makeRender } from '../_utils'
+import type { VaporComponent } from '../../src/component'
+
+const define = makeRender()
+
+describe('useVaporCssVars', () => {
+ async function assertCssVars(getApp: (state: any) => VaporComponent) {
+ const state = reactive({ color: 'red' })
+ const App = getApp(state)
+ const root = document.createElement('div')
+
+ define(App).render({}, root)
+ for (const c of [].slice.call(root.children as any)) {
+ expect((c as HTMLElement).style.getPropertyValue(`--color`)).toBe(`red`)
+ }
+
+ state.color = 'green'
+ await nextTick()
+ for (const c of [].slice.call(root.children as any)) {
+ expect((c as HTMLElement).style.getPropertyValue(`--color`)).toBe('green')
+ }
+ }
+
+ test('basic', async () => {
+ const t0 = template('<div></div>')
+ await assertCssVars(state => ({
+ setup() {
+ useVaporCssVars(() => state)
+ const n0 = t0()
+ return n0
+ },
+ }))
+ })
+
+ test('on multiple root', async () => {
+ const t0 = template('<div></div>')
+ await assertCssVars(state => ({
+ setup() {
+ useVaporCssVars(() => state)
+ const n0 = t0()
+ const n1 = t0()
+ return [n0, n1]
+ },
+ }))
+ })
+
+ test('on HOCs', async () => {
+ const t0 = template('<div></div>')
+ const Child = defineVaporComponent({
+ setup() {
+ const n0 = t0()
+ return n0
+ },
+ })
+ await assertCssVars(state => ({
+ setup() {
+ useVaporCssVars(() => state)
+ return createComponent(Child)
+ },
+ }))
+ })
+
+ test.todo('on suspense root', async () => {})
+
+ test.todo('with v-if & async component & suspense', async () => {})
+
+ test('with subTree changes', async () => {
+ const state = reactive({ color: 'red' })
+ const value = ref(true)
+ const root = document.createElement('div')
+ const t0 = template('<div></div>')
+
+ define({
+ setup() {
+ useVaporCssVars(() => state)
+ const n0 = createIf(
+ () => value.value,
+ () => {
+ const n2 = t0()
+ return n2
+ },
+ () => {
+ const n4 = t0()
+ const n5 = t0()
+ return [n4, n5]
+ },
+ )
+ return n0
+ },
+ }).render({}, root)
+
+ // css vars use with fallback tree
+ for (const c of [].slice.call(root.children as any)) {
+ expect((c as HTMLElement).style.getPropertyValue(`--color`)).toBe(`red`)
+ }
+
+ value.value = false
+ await nextTick()
+ for (const c of [].slice.call(root.children as any)) {
+ expect((c as HTMLElement).style.getPropertyValue(`--color`)).toBe('red')
+ }
+ })
+
+ test('with subTree change inside HOC', async () => {
+ const state = reactive({ color: 'red' })
+ const value = ref(true)
+ const root = document.createElement('div')
+
+ const Child = defineVaporComponent({
+ setup(_, { slots }) {
+ return slots.default!()
+ },
+ })
+
+ const t0 = template('<div></div>')
+ define({
+ setup() {
+ useVaporCssVars(() => state)
+ return createComponent(Child, null, {
+ default: () => {
+ return createIf(
+ () => value.value,
+ () => {
+ const n2 = t0()
+ return n2
+ },
+ () => {
+ const n4 = t0()
+ const n5 = t0()
+ return [n4, n5]
+ },
+ )
+ },
+ })
+ },
+ }).render({}, root)
+
+ await nextTick()
+ // css vars use with fallback tree
+ for (const c of [].slice.call(root.children as any)) {
+ expect((c as HTMLElement).style.getPropertyValue(`--color`)).toBe(`red`)
+ }
+
+ value.value = false
+ await nextTick()
+ for (const c of [].slice.call(root.children as any)) {
+ expect((c as HTMLElement).style.getPropertyValue(`--color`)).toBe('red')
+ }
+ })
+
+ test('with teleport', async () => {
+ const state = reactive({ color: 'red' })
+ const target = document.createElement('div')
+ document.body.appendChild(target)
+
+ define({
+ setup() {
+ useVaporCssVars(() => state)
+ return createComponent(
+ VaporTeleport,
+ {
+ to: () => target,
+ },
+ {
+ default: () => template('<div></div>', true)(),
+ },
+ )
+ },
+ }).render()
+
+ await nextTick()
+ for (const c of [].slice.call(target.children as any)) {
+ expect((c as HTMLElement).style.getPropertyValue(`--color`)).toBe('red')
+ }
+
+ state.color = 'green'
+ await nextTick()
+ for (const c of [].slice.call(target.children as any)) {
+ expect((c as HTMLElement).style.getPropertyValue(`--color`)).toBe('green')
+ }
+ })
+
+ test('with teleport in child slot', async () => {
+ const state = reactive({ color: 'red' })
+ const target = document.createElement('div')
+ document.body.appendChild(target)
+
+ const Child = defineVaporComponent({
+ setup(_, { slots }) {
+ return slots.default!()
+ },
+ })
+
+ define({
+ setup() {
+ useVaporCssVars(() => state)
+ return createComponent(Child, null, {
+ default: () =>
+ createComponent(
+ VaporTeleport,
+ { to: () => target },
+ {
+ default: () => template('<div></div>', true)(),
+ },
+ ),
+ })
+ },
+ }).render()
+
+ await nextTick()
+ for (const c of [].slice.call(target.children as any)) {
+ expect((c as HTMLElement).style.getPropertyValue(`--color`)).toBe('red')
+ }
+
+ state.color = 'green'
+ await nextTick()
+ for (const c of [].slice.call(target.children as any)) {
+ expect((c as HTMLElement).style.getPropertyValue(`--color`)).toBe('green')
+ }
+ })
+
+ test('with teleport(change subTree)', async () => {
+ const state = reactive({ color: 'red' })
+ const target = document.createElement('div')
+ document.body.appendChild(target)
+ const toggle = ref(false)
+
+ define({
+ setup() {
+ useVaporCssVars(() => state)
+ return createComponent(
+ VaporTeleport,
+ { to: () => target },
+ {
+ default: withVaporCtx(() => {
+ const n0 = template('<div></div>', true)()
+ const n1 = createIf(
+ () => toggle.value,
+ () => template('<div></div>', true)(),
+ )
+ return [n0, n1]
+ }),
+ },
+ )
+ },
+ }).render()
+
+ await nextTick()
+ for (const c of [].slice.call(target.children as any)) {
+ expect((c as HTMLElement).style.getPropertyValue(`--color`)).toBe('red')
+ expect((c as HTMLElement).outerHTML.includes('data-v-owner')).toBe(true)
+ }
+
+ toggle.value = true
+ await nextTick()
+ expect(target.children.length).toBe(2)
+ for (const c of [].slice.call(target.children as any)) {
+ expect((c as HTMLElement).style.getPropertyValue(`--color`)).toBe('red')
+ expect((c as HTMLElement).outerHTML.includes('data-v-owner')).toBe(true)
+ }
+ })
+
+ test('with teleport(disabled)', async () => {
+ const state = reactive({ color: 'red' })
+ const target = document.createElement('div')
+ document.body.appendChild(target)
+
+ const { host } = define({
+ setup() {
+ useVaporCssVars(() => state)
+ return createComponent(
+ VaporTeleport,
+ { to: () => target, disabled: () => true },
+ {
+ default: withVaporCtx(() => template('<div></div>', true)()),
+ },
+ )
+ },
+ }).render()
+
+ await nextTick()
+ expect(target.children.length).toBe(0)
+ expect(host.children[0].outerHTML.includes('data-v-owner')).toBe(true)
+ })
+
+ test('with string style', async () => {
+ const state = reactive({ color: 'red' })
+ const root = document.createElement('div')
+ const disabled = ref(false)
+ const t0 = template('<h1></h1>')
+
+ define({
+ setup() {
+ useVaporCssVars(() => state)
+ const n0 = t0() as any
+ renderEffect(() =>
+ setStyle(n0, state.color ? 'pointer-events: none' : undefined),
+ )
+ return n0
+ },
+ }).render({}, root)
+
+ await nextTick()
+ for (const c of [].slice.call(root.children as any)) {
+ expect((c as HTMLElement).style.getPropertyValue(`--color`)).toBe('red')
+ }
+
+ disabled.value = true
+ await nextTick()
+ for (const c of [].slice.call(root.children as any)) {
+ expect((c as HTMLElement).style.getPropertyValue(`--color`)).toBe('red')
+ }
+ })
+
+ test('with delay mount child', async () => {
+ const state = reactive({ color: 'red' })
+ const value = ref(false)
+ const root = document.createElement('div')
+
+ const Child = defineVaporComponent({
+ setup() {
+ onMounted(() => {
+ const childEl = root.children[0]
+ expect(getComputedStyle(childEl!).getPropertyValue(`--color`)).toBe(
+ `red`,
+ )
+ })
+ return template('<div id="childId"></div>')()
+ },
+ })
+
+ define({
+ setup() {
+ useVaporCssVars(() => state)
+ return createIf(
+ () => value.value,
+ () => createComponent(Child),
+ () => template('<div></div>')(),
+ )
+ },
+ }).render({}, root)
+
+ await nextTick()
+ // css vars use with fallback tree
+ for (const c of [].slice.call(root.children as any)) {
+ expect((c as HTMLElement).style.getPropertyValue(`--color`)).toBe(`red`)
+ }
+
+ // mount child
+ value.value = true
+ await nextTick()
+ for (const c of [].slice.call(root.children as any)) {
+ expect((c as HTMLElement).style.getPropertyValue(`--color`)).toBe(`red`)
+ }
+ })
+
+ test('with custom element', async () => {
+ const state = reactive({ color: 'red' })
+ const CE = defineVaporCustomElement({
+ setup() {
+ useVaporCssVars(() => state)
+ return template('<div>hello</div>', true)()
+ },
+ })
+
+ customElements.define('css-vars-ce', CE)
+
+ const { html } = define({
+ setup() {
+ return createPlainElement('css-vars-ce', null, null, true)
+ },
+ }).render()
+
+ expect(html()).toBe('<css-vars-ce style="--color: red;"></css-vars-ce>')
+
+ state.color = 'green'
+ await nextTick()
+ expect(html()).toBe('<css-vars-ce style="--color: green;"></css-vars-ce>')
+ })
+
+ test('should set vars before child component onMounted hook', () => {
+ const state = reactive({ color: 'red' })
+ const root = document.createElement('div')
+ let colorInOnMount
+
+ define({
+ setup() {
+ useVaporCssVars(() => state)
+ onMounted(() => {
+ colorInOnMount = (
+ root.children[0] as HTMLElement
+ ).style.getPropertyValue(`--color`)
+ })
+ return template('<div></div>')()
+ },
+ }).render({}, root)
+
+ expect(colorInOnMount).toBe(`red`)
+ })
+})
import {
+ child,
+ createComponent,
createPlainElement,
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()
})
})
} else {
oldBlocks = []
}
+
+ if (frag.updated) frag.updated.forEach(m => m())
setActiveSub(prevSub)
}
import { rawPropsProxyHandlers } from '../componentProps'
import { renderEffect } from '../renderEffect'
import { extend, isArray } from '@vue/shared'
-import { VaporFragment } from '../fragment'
+import { VaporFragment, isFragment } from '../fragment'
import {
advanceHydrationNode,
currentHydrationNode,
private rawProps?: LooseRawProps
private resolvedProps?: TeleportProps
private rawSlots?: LooseRawSlots
+ isDisabled?: boolean
target?: ParentNode | null
targetAnchor?: Node | null
rawPropsProxyHandlers,
) as any as TeleportProps,
)
+ this.isDisabled = isTeleportDisabled(this.resolvedProps!)
this.handlePropsUpdate()
})
)
})
+ const nodes = this.nodes
+ // register updateCssVars to root fragments's update hooks so that
+ // it will be called when root fragment changed
+ if (this.parentComponent && this.parentComponent.ut) {
+ if (isFragment(nodes)) {
+ ;(nodes.updated || (nodes.updated = [])).push(() => updateCssVars(this))
+ } else if (isArray(nodes)) {
+ nodes.forEach(node => {
+ if (isFragment(node)) {
+ ;(node.updated || (node.updated = [])).push(() =>
+ updateCssVars(this),
+ )
+ }
+ })
+ }
+ }
+
if (__DEV__) {
- const nodes = this.nodes
if (isVaporComponent(nodes)) {
nodes.parentTeleport = this
} else if (isArray(nodes)) {
}
mount(target, this.targetAnchor!)
+ updateCssVars(this)
} else if (__DEV__) {
warn(
`Invalid Teleport target on ${this.targetAnchor ? 'update' : 'mount'}:`,
}
// mount into main container
- if (isTeleportDisabled(this.resolvedProps!)) {
+ if (this.isDisabled) {
mount(this.parent, this.anchor!)
+ updateCssVars(this)
}
// mount into target container
else {
}
return null
}
+
+function updateCssVars(frag: TeleportFragment) {
+ const ctx = frag.parentComponent as GenericComponentInstance
+ if (ctx && ctx.ut) {
+ let node, anchor
+ if (frag.isDisabled) {
+ node = frag.placeholder
+ anchor = frag.anchor
+ } else {
+ node = frag.targetStart
+ anchor = frag.targetAnchor
+ }
+ while (node && node !== anchor) {
+ if (node.nodeType === 1)
+ (node as Element).setAttribute('data-v-owner', String(ctx.uid))
+ node = node.nextSibling
+ }
+ ctx.ut()
+ }
+}
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
+ * defer style matching checks until hydration completes (instance.block is set) if
+ * the component uses style v-bind or the element contains CSS variables, to correctly
+ * verify if the element is the component root.
+ */
+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 (!instance.isMounted) return
+ 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)
refKey: string | undefined,
) => void
+ // hooks
+ updated?: ((nodes?: Block) => void)[]
+
constructor(nodes: T) {
this.nodes = nodes
}
scope: EffectScope,
) => boolean)[]
beforeMount?: ((newKey: any, nodes: Block, scope: EffectScope) => void)[]
- mounted?: ((nodes: Block, scope: EffectScope) => void)[]
constructor(anchorLabel?: string) {
super([])
if (parent) {
insert(this.nodes, parent, this.anchor)
- if (this.mounted) {
- this.mounted.forEach(hook => hook(this.nodes, this.scope!))
+ if (this.updated) {
+ this.updated.forEach(hook => hook(this.nodes))
}
}
} else {
--- /dev/null
+import {
+ type GenericComponentInstance,
+ baseUseCssVars,
+ currentInstance,
+ setVarsOnNode,
+} from '@vue/runtime-dom'
+import { type VaporComponentInstance, isVaporComponent } from '../component'
+import { isArray } from '@vue/shared'
+import type { Block } from '../block'
+
+export function useVaporCssVars(getter: () => Record<string, string>): void {
+ if (!__BROWSER__ && !__TEST__) return
+ const instance = currentInstance as VaporComponentInstance
+ baseUseCssVars(
+ instance,
+ () => resolveParentNode(instance.block),
+ getter,
+ vars => setVars(instance, vars),
+ )
+}
+
+function resolveParentNode(block: Block): Node {
+ if (block instanceof Node) {
+ return block.parentNode!
+ } else if (isArray(block)) {
+ return resolveParentNode(block[0])
+ } else if (isVaporComponent(block)) {
+ return resolveParentNode(block.block!)
+ } else {
+ return resolveParentNode(block.nodes)
+ }
+}
+
+function setVars(
+ instance: VaporComponentInstance,
+ vars: Record<string, string>,
+): void {
+ if ((instance as GenericComponentInstance).ce) {
+ setVarsOnNode((instance as GenericComponentInstance).ce as any, vars)
+ } else {
+ setVarsOnBlock(instance.block, vars)
+ }
+}
+
+function setVarsOnBlock(block: Block, vars: Record<string, string>): void {
+ if (block instanceof Node) {
+ setVarsOnNode(block, vars)
+ } else if (isArray(block)) {
+ block.forEach(child => setVarsOnBlock(child, vars))
+ } else if (isVaporComponent(block)) {
+ setVarsOnBlock(block.block!, vars)
+ } else {
+ setVarsOnBlock(block.nodes, vars)
+ }
+}
getDefaultValue,
} from './apiCreateFor'
export { createTemplateRefSetter } from './apiTemplateRef'
+export { useVaporCssVars } from './helpers/useCssVars'
export { createDynamicComponent } from './apiCreateDynamicComponent'
export { applyVShow } from './directives/vShow'
export {
}
frag.nodes = vnode.el as any
+ if (frag.updated) frag.updated.forEach(m => m())
}
frag.remove = unmount
internals.um(oldVNode, parentComponent as any, null)
}
}
+
+ if (frag.updated) frag.updated.forEach(m => m())
}
const render = (parentNode?: ParentNode, anchor?: Node | null) => {