Skip to content
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

Fix#3336/allow using composables outside setup context #3355

Merged
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
8 changes: 2 additions & 6 deletions packages/ui/src/components/va-modal/hooks/useModal.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
import { getCurrentInstance } from 'vue'
import { createModalInstance } from '../modal'
import { ModalOptions } from '../types'
import { getCurrentApp } from '../../../services/current-app'

/** This hook can be used without plugin used */
export const useModal = () => {
const appContext = getCurrentInstance()?.appContext

if (!appContext) {
throw new Error('useModal can be used only in setup function. You can use app.config.globalProperties.$vaModal outside setup function')
}
const appContext = getCurrentApp()._context

/**
* @param options can be message string or options object
Expand Down
3 changes: 1 addition & 2 deletions packages/ui/src/components/va-modal/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { ExtractComponentPropTypes } from '../../utils/component-options'
import VaModal from './VaModal.vue'
import { ExtractPropTypes } from 'vue'

export type ModalSize = 'small' | 'medium' | 'large'

Expand All @@ -15,4 +14,4 @@ export type ModalEmits = {
'onUpdate:modelValue'?: (value: boolean) => void;
}

export type ModalOptions = Partial<Omit<ExtractComponentPropTypes<typeof VaModal> & ModalEmits, 'anchorClass'>>
export type ModalOptions = Partial<Omit<ExtractComponentPropTypes<typeof VaModal>, 'anchorClass'>> & ModalEmits
5 changes: 2 additions & 3 deletions packages/ui/src/components/va-toast/hooks/useToast.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { getCurrentInstance } from 'vue'

import { createToastInstance, closeById, closeAllNotifications, NotificationOptions } from '../toast'
import { getCurrentApp } from '../../../services/current-app'

/** This hook can be used without plugin used */
export const useToast = () => {
const appContext = getCurrentInstance()?.appContext
const appContext = getCurrentApp()._context

const createdInThisSetupContext: string[] = []

Expand Down
47 changes: 41 additions & 6 deletions packages/ui/src/services/current-app.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,52 @@
import type { App } from 'vue'
import { inject as vueInject } from 'vue'
import type { App, provide as vueProvide } from 'vue'
import { inject as vueInject, getCurrentInstance } from 'vue'

/**
* Similar to `getCurrentInstance` but for plugins, so we can use inject in plugins.
*/
let app: App | null
let singleApp: App | null | undefined

export const setCurrentApp = (instance: App | null) => { app = instance }
export const getCurrentApp = () => app
export const setCurrentApp = (instance: App | null) => {
app = instance

/** Wrapper around vue inject, so it can be used in plugins */
if (singleApp && instance !== singleApp) {
// This means that user has multiple apps on page.
singleApp = null
} else {
singleApp = instance
}
}

/**
* Returns current app if Vuestic UI is used in single app mode.
*
* @throws Error if Vuestic UI is used in multiple apps.
* @throws Error if Vuestic UI plugin is not installed.
*/
export const getCurrentApp = () => {
const app = getCurrentInstance()?.appContext.app
if (app) { return app }

if (singleApp === undefined) {
throw new Error('Vuestic UI plugin is not installed.')
}
if (singleApp === null) {
throw new Error('Vuestic UI is used in multiple apps. You`re not allowed to use composable outside of setup function context.')
}
return singleApp
}

/** Wrapper around vue inject, so we can use it in plugins and outside of setup context if only one app is used */
export const inject = ((key: string, value?: any) => {
const app = getCurrentApp()?._context.provides[key]

return app || vueInject(key, value)
}) as unknown as typeof vueInject
}) as typeof vueInject

/** Wrapper around vue provide, so we can use it in plugins and outside of setup context if only one app is used */
export const provide = ((key: string, value: any) => {
const provides = getCurrentInstance()?.appContext.provides || getCurrentApp()._context.provides

provides[key] = value
}) as typeof vueProvide
21 changes: 5 additions & 16 deletions packages/ui/src/services/global-config/global-config.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
import cloneDeep from 'lodash/cloneDeep.js'
import { ref, getCurrentInstance } from 'vue'
import { Ref, ref } from 'vue'
import { GlobalConfig, GlobalConfigUpdater, PartialGlobalConfig, ProvidedGlobalConfig } from './types'
import { getComponentsDefaultConfig } from '../component-config'
import { getIconDefaultConfig } from '../icon'
import { getColorDefaultConfig } from '../color'
import { getI18nConfigDefaults } from '../i18n'
import { getBreakpointDefaultConfig } from '../breakpoint'
import { getGlobalProperty } from '../vue-plugin/utils'
import { getCurrentApp, inject } from '../current-app'
import { inject, provide } from '../current-app'
import { mergeDeep } from '../../utils/merge-deep'
import { getColorsClassesDefaultConfig } from '../colors-classes'

export const GLOBAL_CONFIG = Symbol('GLOBAL_CONFIG')

export const createGlobalConfig = () => {
export const createGlobalConfig = (): ProvidedGlobalConfig => {
const globalConfig = ref<GlobalConfig>({
colors: getColorDefaultConfig(),
icons: getIconDefaultConfig(),
Expand All @@ -27,7 +26,7 @@ export const createGlobalConfig = () => {
* TODO: if this try won't be success, may be remake to provide/inject
*/
routerComponent: undefined,
})
}) as Ref<GlobalConfig>

const getGlobalConfig = (): GlobalConfig => globalConfig.value
const setGlobalConfig = (updater: GlobalConfig | GlobalConfigUpdater<GlobalConfig>) => {
Expand All @@ -48,24 +47,14 @@ export const createGlobalConfig = () => {
}
}

const provideForCurrentApp = <T>(provide: T) => {
const provides = getCurrentInstance()?.appContext.provides || getCurrentApp()?._context.provides

if (!provides) { throw new Error('Vue app not found for provide') }

provides[GLOBAL_CONFIG] = provide

return provide
}

/** Use this function if you don't want to throw error if hook used outside setup function by useGlobalConfig */
export function useGlobalConfig () {
let injected = inject<ProvidedGlobalConfig>(GLOBAL_CONFIG) as ProvidedGlobalConfig

if (!injected) {
injected = createGlobalConfig()

provideForCurrentApp(injected)
provide(GLOBAL_CONFIG, injected)
}

return injected
Expand Down
8 changes: 4 additions & 4 deletions packages/ui/src/utils/component-options/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ declare type ExtractDefineComponentPropsType<T> = true extends boolean ? {
export type ExtractComponentProps<T extends DefineComponentOptions> = true extends boolean ? ExtractDefineComponentPropsType<T> : never
export type ExtractComponentEmits<T> = T extends ComponentOptionsBase<any, any, any, any, any, any, any, infer E> ? E: []

type UnPropType<T> = T extends PropType<infer P> ? P : never
export type ExtractComponentPropTypes<T extends DefineComponentOptions> = {
[K in keyof ExtractComponentProps<T>]: UnPropType<ExtractComponentProps<T>[K]['type']>
}
export type ExtractComponentPropTypes<T> =
T extends (props: infer P, ...args: any) => any ? P :
T extends new () => { $props: infer P } ? NonNullable<P> :
{}