Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions packages/runtime-vapor/__tests__/apiCreateDynamicComponent.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,44 @@ describe('api: createDynamicComponent', () => {
expect(html()).toBe('<baz></baz><!--dynamic-component-->')
})

test('with v-once', async () => {
const val = shallowRef<any>(A)

const { html } = define({
setup() {
return createDynamicComponent(() => val.value, null, null, true, true)
},
}).render()

expect(html()).toBe('AAA<!--dynamic-component-->')

val.value = B
await nextTick()
expect(html()).toBe('AAA<!--dynamic-component-->') // still AAA
})

test('fallback with v-once', async () => {
const val = shallowRef<any>('button')
const id = ref(0)
const { html } = define({
setup() {
return createDynamicComponent(
() => val.value,
{ id: () => id.value },
null,
true,
true,
)
},
}).render()

expect(html()).toBe('<button id="0"></button><!--dynamic-component-->')

id.value++
await nextTick()
expect(html()).toBe('<button id="0"></button><!--dynamic-component-->')
})

test('render fallback with insertionState', async () => {
const { html, mount } = define({
setup() {
Expand Down
62 changes: 62 additions & 0 deletions packages/runtime-vapor/__tests__/component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ import {
onUpdated,
provide,
ref,
useAttrs,
watch,
watchEffect,
} from '@vue/runtime-dom'
import {
createComponent,
createIf,
createTextNode,
defineVaporComponent,
renderEffect,
setInsertionState,
template,
Expand Down Expand Up @@ -315,6 +317,66 @@ describe('component', () => {
expect(getEffectsCount(i.scope)).toBe(0)
})

it('work with v-once + props', () => {
const Child = defineVaporComponent({
props: {
count: Number,
},
setup(props) {
const n0 = template(' ')() as any
renderEffect(() => setText(n0, props.count))
return n0
},
})

const count = ref(0)
const { html } = define({
setup() {
return createComponent(
Child,
{ count: () => count.value },
null,
true,
true, // v-once
)
},
}).render()

expect(html()).toBe('0')

count.value++
expect(html()).toBe('0')
})

it('work with v-once + attrs', () => {
const Child = defineVaporComponent({
setup() {
const attrs = useAttrs()
const n0 = template(' ')() as any
renderEffect(() => setText(n0, attrs.count as string))
return n0
},
})

const count = ref(0)
const { html } = define({
setup() {
return createComponent(
Child,
{ count: () => count.value },
null,
true,
true, // v-once
)
},
}).render()

expect(html()).toBe('0')

count.value++
expect(html()).toBe('0')
})

test('should mount component only with template in production mode', () => {
__DEV__ = false
const { component: Child } = define({
Expand Down
2 changes: 2 additions & 0 deletions packages/runtime-vapor/src/apiCreateApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const mountApp: AppMountFn<ParentNode> = (app, container) => {
app._props as RawProps,
null,
false,
false,
app._context,
)
mountComponent(instance, container)
Expand All @@ -61,6 +62,7 @@ const hydrateApp: AppMountFn<ParentNode> = (app, container) => {
app._props as RawProps,
null,
false,
false,
app._context,
)
mountComponent(instance, container)
Expand Down
9 changes: 7 additions & 2 deletions packages/runtime-vapor/src/apiCreateDynamicComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export function createDynamicComponent(
rawProps?: RawProps | null,
rawSlots?: RawSlots | null,
isSingleRoot?: boolean,
once?: boolean,
): VaporFragment {
const _insertionParent = insertionParent
const _insertionAnchor = insertionAnchor
Expand All @@ -29,7 +30,7 @@ export function createDynamicComponent(
? new DynamicFragment('dynamic-component')
: new DynamicFragment()

renderEffect(() => {
const renderFn = () => {
const value = getter()
frag.update(
() =>
Expand All @@ -38,10 +39,14 @@ export function createDynamicComponent(
rawProps,
rawSlots,
isSingleRoot,
once,
),
value,
)
})
}

if (once) renderFn()
else renderEffect(renderFn)

if (!isHydrating && _insertionParent) {
insert(frag, _insertionParent, _insertionAnchor)
Expand Down
13 changes: 9 additions & 4 deletions packages/runtime-vapor/src/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ export function createComponent(
rawProps?: LooseRawProps | null,
rawSlots?: LooseRawSlots | null,
isSingleRoot?: boolean,
once?: boolean,
appContext: GenericAppContext = (currentInstance &&
currentInstance.appContext) ||
emptyContext,
Expand Down Expand Up @@ -185,6 +186,7 @@ export function createComponent(
rawProps as RawProps,
rawSlots as RawSlots,
appContext,
once,
)

// HMR
Expand Down Expand Up @@ -401,6 +403,7 @@ export class VaporComponentInstance implements GenericComponentInstance {
rawProps?: RawProps | null,
rawSlots?: RawSlots | null,
appContext?: GenericAppContext,
once?: boolean,
) {
this.vapor = true
this.uid = nextUid()
Expand Down Expand Up @@ -441,7 +444,7 @@ export class VaporComponentInstance implements GenericComponentInstance {
this.rawProps = rawProps || EMPTY_OBJ
this.hasFallthrough = hasFallthroughAttrs(comp, rawProps)
if (rawProps || comp.props) {
const [propsHandlers, attrsHandlers] = getPropsProxyHandlers(comp)
const [propsHandlers, attrsHandlers] = getPropsProxyHandlers(comp, once)
this.attrs = new Proxy(this, attrsHandlers)
this.props = comp.props
? new Proxy(this, propsHandlers!)
Expand Down Expand Up @@ -486,9 +489,10 @@ export function createComponentWithFallback(
rawProps?: LooseRawProps | null,
rawSlots?: LooseRawSlots | null,
isSingleRoot?: boolean,
once?: boolean,
): HTMLElement | VaporComponentInstance {
if (!isString(comp)) {
return createComponent(comp, rawProps, rawSlots, isSingleRoot)
return createComponent(comp, rawProps, rawSlots, isSingleRoot, once)
}

const _insertionParent = insertionParent
Expand All @@ -504,9 +508,10 @@ export function createComponentWithFallback(
;(el as any).$root = isSingleRoot

if (rawProps) {
renderEffect(() => {
const setFn = () =>
setDynamicProps(el, [resolveDynamicProps(rawProps as RawProps)])
})
if (once) setFn()
Copy link
Contributor

@alex-snezhko alex-snezhko Oct 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are all of the other side effects of renderEffect safe/desired to avoid in the event of a v-once e.g. setCurrentInstance, beforeUpdate/updated lifecycle hooks, startMeasure in RenderEffect.fn()?

else renderEffect(setFn)
}

if (rawSlots) {
Expand Down
28 changes: 24 additions & 4 deletions packages/runtime-vapor/src/componentProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
import { ReactiveFlags } from '@vue/reactivity'
import { normalizeEmitsOptions } from './componentEmits'
import { renderEffect } from './renderEffect'
import { pauseTracking, resetTracking } from '@vue/reactivity'
import type { interopKey } from './vdomInterop'

export type RawProps = Record<string, () => unknown> & {
Expand All @@ -43,6 +44,7 @@ export function resolveSource(

export function getPropsProxyHandlers(
comp: VaporComponent,
once?: boolean,
): [
ProxyHandler<VaporComponentInstance> | null,
ProxyHandler<VaporComponentInstance>,
Expand Down Expand Up @@ -111,17 +113,26 @@ export function getPropsProxyHandlers(
)
}

const getPropValue = once
? (...args: Parameters<typeof getProp>) => {
pauseTracking()
const value = getProp(...args)
resetTracking()
return value
}
: getProp

const propsHandlers = propsOptions
? ({
get: (target, key) => getProp(target, key),
get: (target, key) => getPropValue(target, key),
has: (_, key) => isProp(key),
ownKeys: () => Object.keys(propsOptions),
getOwnPropertyDescriptor(target, key) {
if (isProp(key)) {
return {
configurable: true,
enumerable: true,
get: () => getProp(target, key),
get: () => getPropValue(target, key),
}
}
},
Expand Down Expand Up @@ -149,16 +160,25 @@ export function getPropsProxyHandlers(
}
}

const getAttrValue = once
? (...args: Parameters<typeof getAttr>) => {
pauseTracking()
const value = getAttr(...args)
resetTracking()
return value
}
: getAttr

const attrsHandlers = {
get: (target, key: string) => getAttr(target.rawProps, key),
get: (target, key: string) => getAttrValue(target.rawProps, key),
has: (target, key: string) => hasAttr(target.rawProps, key),
ownKeys: target => getKeysFromRawProps(target.rawProps).filter(isAttr),
getOwnPropertyDescriptor(target, key: string) {
if (hasAttr(target.rawProps, key)) {
return {
configurable: true,
enumerable: true,
get: () => getAttr(target.rawProps, key),
get: () => getAttrValue(target.rawProps, key),
}
}
},
Expand Down