Skip to content

feat: Added expose to component instances #75

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
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
2 changes: 1 addition & 1 deletion packages/runtime-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export {

// For getting a hold of the internal instance in setup() - useful for advanced
// plugins
export { getCurrentInstance } from './component'
export { getCurrentInstance, getExposeProxy } from './component'

// For raw render function users
export { h } from './h'
Expand Down
62 changes: 59 additions & 3 deletions packages/runtime-vapor/src/component.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { EffectScope, type Ref, ref } from '@vue/reactivity'
import { EffectScope, isRef, type Ref, ref } from '@vue/reactivity'

import { EMPTY_OBJ } from '@vue/shared'
import { EMPTY_OBJ, isArray } from '@vue/shared'
import type { Block } from './render'
import type { DirectiveBinding } from './directive'
import {
Expand All @@ -11,7 +11,7 @@ import {

import type { Data } from '@vue/shared'
import { VaporLifecycleHooks } from './enums'

import { warn } from '@vue/runtime-core'
export type Component = FunctionalComponent | ObjectComponent

export type SetupFn = (props: any, ctx: any) => Block | Data
Expand Down Expand Up @@ -41,6 +41,10 @@ export interface ComponentInternalInstance {
props: Data
setupState: Data

// exposed properties via expose()
exposed: Record<string, any> | null
exposeProxy: Record<string, any> | null

/** directives */
dirs: Map<Node, DirectiveBinding[]>

Expand Down Expand Up @@ -122,6 +126,54 @@ export const unsetCurrentInstance = () => {
currentInstance = null
}

// TODO: Maybe it can be reused
// TODO: https://github.com/vuejs/core/blob/04d2c05054c26b02fbc1d84839b0ed5cd36455b6/packages/runtime-core/src/component.ts#L186C1-L186C1
export type SetupContext = {
expose: (exposed?: Record<string, any>) => void
}

// TODO: Maybe it can be reused
// TODO: https://github.com/vuejs/core/blob/04d2c05054c26b02fbc1d84839b0ed5cd36455b6/packages/runtime-core/src/component.ts#L983
export function createSetupContext(
instance: ComponentInternalInstance,
): SetupContext {
const expose: SetupContext['expose'] = (exposed) => {
if (__DEV__) {
if (instance.exposed) {
warn(`expose() should be called only once per setup().`)
}
if (exposed != null) {
let exposedType: string = typeof exposed
if (exposedType === 'object') {
if (isArray(exposed)) {
exposedType = 'array'
} else if (isRef(exposed)) {
exposedType = 'ref'
}
}
if (exposedType !== 'object') {
warn(
`expose() should be passed a plain object, received ${exposedType}.`,
)
}
}
}
instance.exposed = exposed || {}
}

if (__DEV__) {
// We use getters in dev in case libs like test-utils overwrite instance
// properties (overwrites should not be done in prod)
return Object.freeze({
expose,
})
} else {
return {
expose,
}
}
}

let uid = 0
export const createComponentInstance = (
component: ObjectComponent | FunctionalComponent,
Expand All @@ -146,6 +198,10 @@ export const createComponentInstance = (
props: EMPTY_OBJ,
setupState: EMPTY_OBJ,

// exposed properties via expose()
exposed: null,
exposeProxy: null,

dirs: new Map(),

// lifecycle
Expand Down
8 changes: 6 additions & 2 deletions packages/runtime-vapor/src/directive.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { isFunction } from '@vue/shared'
import { type ComponentInternalInstance, currentInstance } from './component'
import { watchEffect } from './apiWatch'

import { getExposeProxy } from '@vue/runtime-core'
export type DirectiveModifiers<M extends string = string> = Record<M, boolean>

export interface DirectiveBinding<V = any, M extends string = string> {
Expand Down Expand Up @@ -69,6 +69,10 @@ export function withDirectives<T extends Node>(
}

const instance = currentInstance
// TODO: Wait for the component instance type to be completed before deleting `any`
const instanceProxy =
(getExposeProxy(currentInstance as any) as ComponentInternalInstance) ||
currentInstance.proxy
if (!instance.dirs.has(node)) instance.dirs.set(node, [])
const bindings = instance.dirs.get(node)!

Expand All @@ -84,7 +88,7 @@ export function withDirectives<T extends Node>(

const binding: DirectiveBinding = {
dir,
instance,
instance: instanceProxy,
source,
value: null, // set later
oldValue: null,
Expand Down
3 changes: 2 additions & 1 deletion packages/runtime-vapor/src/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
type Component,
type ComponentInternalInstance,
createComponentInstance,
createSetupContext,
setCurrentInstance,
unsetCurrentInstance,
} from './component'
Expand Down Expand Up @@ -47,7 +48,7 @@ export function mountComponent(
setCurrentInstance(instance)
const block = instance.scope.run(() => {
const { component, props } = instance
const ctx = { expose: () => {} }
const ctx = createSetupContext(instance)

const setupFn =
typeof component === 'function' ? component : component.setup
Expand Down