diff --git a/packages/docs/config/vuestic-config.ts b/packages/docs/config/vuestic-config.ts index 0fbb838deb..51907f1729 100644 --- a/packages/docs/config/vuestic-config.ts +++ b/packages/docs/config/vuestic-config.ts @@ -29,6 +29,7 @@ export const VuesticConfig = defineVuesticConfig({ presets: { VaButton: { addToCart: { round: true, color: 'success', icon: 'shopping_cart', 'slot:default': 'Add to card' }, + promotion: { gradient: true, color: 'primary' }, deleteFromCart: { size: 'small', plain: true }, landingHeader: VaButtonLandingHeader, github: { diff --git a/packages/docs/package.json b/packages/docs/package.json index 9363a4a14f..7bb6444d14 100644 --- a/packages/docs/package.json +++ b/packages/docs/package.json @@ -6,6 +6,7 @@ "build": "yarn build:analysis && nuxt generate --max_old_space_size=4096", "build:ci": "yarn build:analysis && nuxt generate", "start:ci": "yarn preview", + "typecheck": "yarn vue-tsc --noEmit", "build:analysis": "yarn workspace sandbox build:analysis ../docs/page-config/getting-started/tree-shaking", "serve": "yarn build:analysis --use-cache && nuxt dev", "generate": "yarn build:analysis && nuxt generate --max_old_space_size=4096", diff --git a/packages/docs/page-config/services/components-config/code/components-presets-multiple.ts b/packages/docs/page-config/services/components-config/code/components-presets-multiple.ts new file mode 100644 index 0000000000..229361f94e --- /dev/null +++ b/packages/docs/page-config/services/components-config/code/components-presets-multiple.ts @@ -0,0 +1,10 @@ +createVuestic({ + components: { + presets: { + VaButton: { + addToCart: { round: true, color: 'success', icon: 'shopping_cart', 'slot:default': 'Add to card' }, + promotion: { gradient: true, color: 'primary' } + }, + }, + }, +}) diff --git a/packages/docs/page-config/services/components-config/examples/presets-multiple.vue b/packages/docs/page-config/services/components-config/examples/presets-multiple.vue new file mode 100644 index 0000000000..1094685d82 --- /dev/null +++ b/packages/docs/page-config/services/components-config/examples/presets-multiple.vue @@ -0,0 +1,5 @@ + diff --git a/packages/docs/page-config/services/components-config/index.ts b/packages/docs/page-config/services/components-config/index.ts index 184d767aef..4ccf3c3d52 100644 --- a/packages/docs/page-config/services/components-config/index.ts +++ b/packages/docs/page-config/services/components-config/index.ts @@ -54,6 +54,9 @@ export default definePageConfig({ block.paragraph("For each component you can make preset configurations. It is useful when you have a set of props that you want to use in different places. For example, you can create a preset for a button with a specific color and size. Then you can use this preset in different places. For example:"), block.code("components-presets"), block.example("presets", { hideTitle: true, forceShowCode: true }), + block.paragraph("You can apply multiple presets to the same component. Props from the later presets will override props from the former:"), + block.code("components-presets-multiple"), + block.example("presets-multiple", { hideTitle: true, forceShowCode: true }), block.subtitle("All components config"), block.paragraph("You could use `components.all` global config property to set prop values for all components at once. It will be applied if there are no other source of prop value. For example:"), @@ -66,7 +69,7 @@ export default definePageConfig({ block.alert("This feature is work in progress. We need to give names to child components and document them. If you want to be able to customize concrete child component, please create an issue on GitHub."), block.subtitle("Slots config"), - block.paragraph("There are cases when `class` and `style` are not enough. In case you need to change HTML content for component globally use can provide `slot:`. For example:"), + block.paragraph("There are cases when `class` and `style` are not enough. In case you need to change HTML content for component globally use `slot:`. For example:"), block.code("components-slots"), block.code("components-slots-style", "css"), block.example("slots", { hideTitle: true }), diff --git a/packages/docs/tsconfig.json b/packages/docs/tsconfig.json index 12e3dc6652..7118c591fd 100644 --- a/packages/docs/tsconfig.json +++ b/packages/docs/tsconfig.json @@ -6,5 +6,8 @@ "types": [ "vite/client", ] - } + }, + "exclude": [ + "page-config/**/**/code/*.ts", + ] } diff --git a/packages/ui/src/composables/useChildComponents.ts b/packages/ui/src/composables/useChildComponents.ts index 18bc978f6a..1007630739 100644 --- a/packages/ui/src/composables/useChildComponents.ts +++ b/packages/ui/src/composables/useChildComponents.ts @@ -78,19 +78,3 @@ export const injectChildPropsFromParent = () => { return computed(() => childProps.value[childName]) } - -export const injectChildPresetPropFromParent = () => { - const childName = getCurrentInstance()?.attrs['va-child'] as string - - if (!childName) { - return null - } - - const childProps = inject(CHILD_COMPONENTS_INJECT_KEY) - - if (!childProps?.value) { - return null - } - - return computed(() => childProps.value[childName]?.preset as string) -} diff --git a/packages/ui/src/composables/useComponentPreset.ts b/packages/ui/src/composables/useComponentPreset.ts index 7f1926c3be..94ab1d2979 100644 --- a/packages/ui/src/composables/useComponentPreset.ts +++ b/packages/ui/src/composables/useComponentPreset.ts @@ -1,6 +1,12 @@ +import { PropType, ExtractPropTypes } from 'vue' + +export type PresetPropValue = string | string[]; + export const useComponentPresetProp = { preset: { - type: String, + type: [String, Array] as PropType, default: undefined, }, } + +export type ComponentPresetProp = ExtractPropTypes diff --git a/packages/ui/src/services/component-config/types.ts b/packages/ui/src/services/component-config/types.ts index 6b519c291f..d64f28120d 100644 --- a/packages/ui/src/services/component-config/types.ts +++ b/packages/ui/src/services/component-config/types.ts @@ -1,19 +1,48 @@ import type { VuesticComponentsMap } from '../vue-plugin' -import type { VNodeProps, AllowedComponentProps, HTMLAttributes } from 'vue' +import type { VNodeProps, AllowedComponentProps, HTMLAttributes, VNode, DefineComponent } from 'vue' +import { ComponentSlots } from '../../utils/component-options' +import { ObjectOrGetter } from '../../utils/types/object-or-getter' export type VuesticComponentName = keyof VuesticComponentsMap export type VueDefaultPropNames = keyof (VNodeProps & AllowedComponentProps) | `on${string}` -export type Props = { [propName: string]: any } -export type Presets = { [componentName in VuesticComponentName]?: { [presetName: string]: Props } } export type PropTypes = C extends { new(): { $props: infer Props } } ? Omit : never -export type ComponentConfig = Partial<{ +export type VuesticComponentPropsMap = { // key-value hack to avoid generics in type (like Omit, PropTypes, etc.) // `key: type` as result [componentName in VuesticComponentName]: { [key in keyof PropTypes]?: PropTypes[key] } & HTMLAttributes -} & { all: Props, presets: Presets }> +} + +export type Props = { [propName: string]: any } + +type VuesticComponentSlotsMap = { + [componentName in VuesticComponentName]: { + [key in keyof RemoveIndex>]?: ComponentSlots[key] + } +} + +type SlotPropPrefix = `slot:${T}` + +export type SlotProp = VNode | string | DefineComponent, {}, {}, {}, {}> + +type VuesticComponentSlotPropsMap = { + [componentName in VuesticComponentName]: { + // @ts-ignore + [key in keyof VuesticComponentSlotsMap[componentName] as SlotPropPrefix]: SlotProp[0]> + } +} + +export type VuesticComponentPresetProps = VuesticComponentPropsMap[T] & VuesticComponentSlotPropsMap[T] + +export type Presets = { + [componentName in VuesticComponentName]?: { + [presetName: string]: ObjectOrGetter, VuesticComponentPropsMap[componentName]> + } +} + +export type ComponentConfig = Partial export type { DefineComponent as VuesticComponent } from 'vue' diff --git a/packages/ui/src/services/component-config/utils/use-component-config-props.ts b/packages/ui/src/services/component-config/utils/use-component-config-props.ts index 7c853e8f1b..bd4d61ba8a 100644 --- a/packages/ui/src/services/component-config/utils/use-component-config-props.ts +++ b/packages/ui/src/services/component-config/utils/use-component-config-props.ts @@ -1,31 +1,65 @@ -import type { VuesticComponent, VuesticComponentName, Props } from '../types' +import { VuesticComponentName, Props, VuesticComponent } from '../types' import { useLocalConfig } from '../../../composables/useLocalConfig' import { useGlobalConfig } from '../../global-config/global-config' import { computed } from 'vue' -import { injectChildPresetPropFromParent } from '../../../composables/useChildComponents' +import { injectChildPropsFromParent } from '../../../composables/useChildComponents' +import { ComponentPresetProp, PresetPropValue } from '../../../composables' +import { notNil } from '../../../utils/isNilValue' +import { head } from 'lodash' +import { getObject } from '../../../utils/object-or-getter' + +const withPresetProp =

(props: P): props is P & ComponentPresetProp => 'preset' in props +const getPresetProp =

(props: P) => withPresetProp(props) ? props.preset : undefined export const useComponentConfigProps = (component: T, originalProps: Props) => { const localConfig = useLocalConfig() const { globalConfig } = useGlobalConfig() - const instancePreset = computed(() => originalProps.preset) - const getPresetProps = (presetName: string) => globalConfig.value.components?.presets?.[component.name as VuesticComponentName]?.[presetName] - const parentPropPreset = injectChildPresetPropFromParent() + const componentName = component.name as VuesticComponentName + + const getPresetProps = (presetPropValue: PresetPropValue): Props => { + return (presetPropValue instanceof Array ? presetPropValue : [presetPropValue]).reduce((acc, presetName) => { + const preset = globalConfig.value.components?.presets?.[componentName]?.[presetName] + + if (!preset) { + return acc + } + + const presetProps = getObject(preset, originalProps) + + const extendedPresets = getPresetProp(presetProps) + + return { + ...acc, + ...(extendedPresets ? getPresetProps(extendedPresets) : undefined), + ...presetProps, + } + }, {}) + } + const parentInjectedProps = injectChildPropsFromParent() return computed(() => { const globalConfigProps: Props = { ...globalConfig.value.components?.all, - ...globalConfig.value.components?.[component.name as VuesticComponentName], + ...globalConfig.value.components?.[componentName], } - const localConfigProps: Props = localConfig.value - .reduce((finalConfig, config) => config[component.name as VuesticComponentName] - ? { ...finalConfig, ...config[component.name as VuesticComponentName] } - : finalConfig - , {}) + const localConfigProps = localConfig.value + .reduce((finalConfig, config) => { + const componentConfigProps = config[componentName] + + return componentConfigProps + ? { ...finalConfig, ...componentConfigProps } + : finalConfig + }, {}) - const presetName = parentPropPreset?.value || instancePreset.value || localConfigProps.preset || globalConfigProps.preset - const presetProps = presetName && getPresetProps(presetName) + const presetProp = head([ + parentInjectedProps?.value, + originalProps, + localConfigProps, + globalConfigProps, + ].filter(notNil).map(getPresetProp).filter(notNil)) + const presetProps = presetProp ? getPresetProps(presetProp) : undefined return { ...globalConfigProps, ...localConfigProps, ...presetProps } }) diff --git a/packages/ui/src/services/config-transport/createRenderFn.ts b/packages/ui/src/services/config-transport/createRenderFn.ts index e7113a882f..e6bf5f70eb 100644 --- a/packages/ui/src/services/config-transport/createRenderFn.ts +++ b/packages/ui/src/services/config-transport/createRenderFn.ts @@ -1,4 +1,5 @@ import { withCtx, h, DefineComponent, VNode, isVNode, Text, createBlock } from 'vue' +import type { SlotProp } from '../component-config' type VueInternalRenderFunction = Function @@ -6,7 +7,7 @@ export const renderSlotNode = (node: VNode, ctx = null) => { return withCtx(() => [node], ctx) } -export const makeVNode = (node: VNode | string | DefineComponent) => { +export const makeVNode = (node: SlotProp) => { if (typeof node === 'string') { return h(Text, node) } diff --git a/packages/ui/src/utils/component-options/types.ts b/packages/ui/src/utils/component-options/types.ts index e7040eb95b..045438bf3b 100644 --- a/packages/ui/src/utils/component-options/types.ts +++ b/packages/ui/src/utils/component-options/types.ts @@ -6,6 +6,11 @@ export type ComponentProps = T extends (props: infer P, ...args: any) => any ? P : unknown; +export type ComponentSlots = + T extends new () => { $slots: infer S; } ? NonNullable : + T extends (props: any, ctx: { slots: infer S; attrs: any; emit: any; }, ...args: any) => any ? NonNullable : + {}; + export type UnKeyofString = T extends infer E & ThisType ? E : never export type ExtractVolarEmitsType = 'emits' extends keyof T ? UnKeyofString<(T['emits'] extends infer E | undefined ? E : never)> diff --git a/packages/ui/src/utils/isNilValue.ts b/packages/ui/src/utils/isNilValue.ts index 9290399394..c7e46f998f 100644 --- a/packages/ui/src/utils/isNilValue.ts +++ b/packages/ui/src/utils/isNilValue.ts @@ -1,3 +1,5 @@ +const nilValues = [null, undefined, '' as const] + /** * Checks if provided value not exists. * @@ -5,5 +7,7 @@ */ export const isNilValue = (value: any): value is null | undefined | '' => { // lodash `isNil` isn't an alternative, because we also want to handle empty string values - return [null, undefined, ''].includes(value) + return nilValues.includes(value) } + +export const notNil = (value: T): value is NonNullable => !isNilValue(value) diff --git a/packages/ui/src/utils/object-or-getter.ts b/packages/ui/src/utils/object-or-getter.ts new file mode 100644 index 0000000000..eb161823c2 --- /dev/null +++ b/packages/ui/src/utils/object-or-getter.ts @@ -0,0 +1,9 @@ +import { ObjectOrGetter } from './types/object-or-getter' + +export const getObject = (objectOrGetter: ObjectOrGetter, baseProps: P) => { + if (typeof objectOrGetter === 'function') { + return objectOrGetter(baseProps) + } + + return objectOrGetter +} diff --git a/packages/ui/src/utils/types/global.d.ts b/packages/ui/src/utils/types/global.d.ts new file mode 100644 index 0000000000..70353e2762 --- /dev/null +++ b/packages/ui/src/utils/types/global.d.ts @@ -0,0 +1,11 @@ +type RemoveIndex = { + [ K in keyof T as + string extends K + ? never + : number extends K + ? never + : symbol extends K + ? never + : K + ]: T[K]; +} diff --git a/packages/ui/src/utils/types/object-or-getter.ts b/packages/ui/src/utils/types/object-or-getter.ts new file mode 100644 index 0000000000..ce8f0efb69 --- /dev/null +++ b/packages/ui/src/utils/types/object-or-getter.ts @@ -0,0 +1 @@ +export type ObjectOrGetter = T | ((props: P) => T)