Skip to content

Commit 8b76cdb

Browse files
committed
feat(presets): Added ability to apply multiple presets
1 parent 0ffdb22 commit 8b76cdb

File tree

14 files changed

+132
-39
lines changed

14 files changed

+132
-39
lines changed

packages/docs/config/vuestic-config.ts

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export const VuesticConfig = defineVuesticConfig({
2929
presets: {
3030
VaButton: {
3131
addToCart: { round: true, color: 'success', icon: 'shopping_cart', 'slot:default': 'Add to card' },
32+
promotion: { gradient: true, color: 'primary' },
3233
deleteFromCart: { size: 'small', plain: true },
3334
landingHeader: VaButtonLandingHeader,
3435
github: {

packages/docs/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"build": "yarn build:analysis && nuxt generate --max_old_space_size=4096",
77
"build:ci": "yarn build:analysis && nuxt generate",
88
"start:ci": "yarn preview",
9+
"typecheck": "yarn vue-tsc --noEmit",
910
"build:analysis": "yarn workspace sandbox build:analysis ../docs/page-config/getting-started/tree-shaking",
1011
"serve": "yarn build:analysis --use-cache && nuxt dev",
1112
"generate": "yarn build:analysis && nuxt generate --max_old_space_size=4096",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
createVuestic({
2+
components: {
3+
presets: {
4+
VaButton: {
5+
addToCart: { round: true, color: 'success', icon: 'shopping_cart', 'slot:default': 'Add to card' },
6+
promotion: { gradient: true, color: 'primary' }
7+
},
8+
},
9+
},
10+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<template>
2+
<VaButton
3+
:preset="['addToCart', 'promotion']"
4+
/>
5+
</template>

packages/docs/page-config/services/components-config/index.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ export default definePageConfig({
5454
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:"),
5555
block.code("components-presets"),
5656
block.example("presets", { hideTitle: true, forceShowCode: true }),
57+
block.paragraph("You can apply multiple presets to the same component. Props from the later presets will override props from the former:"),
58+
block.code("components-presets-multiple"),
59+
block.example("presets-multiple", { hideTitle: true, forceShowCode: true }),
5760

5861
block.subtitle("All components config"),
5962
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({
6669
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."),
6770

6871
block.subtitle("Slots config"),
69-
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:"),
72+
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:"),
7073
block.code("components-slots"),
7174
block.code("components-slots-style", "css"),
7275
block.example("slots", { hideTitle: true }),

packages/docs/tsconfig.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,8 @@
66
"types": [
77
"vite/client",
88
]
9-
}
9+
},
10+
"exclude": [
11+
"page-config/**/**/code/*.ts",
12+
]
1013
}

packages/ui/src/composables/useChildComponents.ts

-16
Original file line numberDiff line numberDiff line change
@@ -78,19 +78,3 @@ export const injectChildPropsFromParent = () => {
7878

7979
return computed(() => childProps.value[childName])
8080
}
81-
82-
export const injectChildPresetPropFromParent = () => {
83-
const childName = getCurrentInstance()?.attrs['va-child'] as string
84-
85-
if (!childName) {
86-
return null
87-
}
88-
89-
const childProps = inject(CHILD_COMPONENTS_INJECT_KEY)
90-
91-
if (!childProps?.value) {
92-
return null
93-
}
94-
95-
return computed(() => childProps.value[childName]?.preset as string)
96-
}
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
1+
import { PropType, ExtractPropTypes } from 'vue'
2+
3+
export type PresetPropValue = string | string[];
4+
15
export const useComponentPresetProp = {
26
preset: {
3-
type: String,
7+
type: [String, Array] as PropType<PresetPropValue>,
48
default: undefined,
59
},
610
}
11+
12+
export type ComponentPresetProp = ExtractPropTypes<typeof useComponentPresetProp>
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,47 @@
11
import type { VuesticComponentsMap } from '../vue-plugin'
2-
import type { VNodeProps, AllowedComponentProps, HTMLAttributes } from 'vue'
2+
import type { VNodeProps, AllowedComponentProps, HTMLAttributes, VNode, DefineComponent } from 'vue'
3+
import { ComponentSlots } from '../../utils/component-options'
34

45
export type VuesticComponentName = keyof VuesticComponentsMap
56
export type VueDefaultPropNames = keyof (VNodeProps & AllowedComponentProps) | `on${string}`
67

7-
export type Props = { [propName: string]: any }
8-
export type Presets = { [componentName in VuesticComponentName]?: { [presetName: string]: Props } }
98
export type PropTypes<C> = C extends { new(): { $props: infer Props } } ? Omit<Props, VueDefaultPropNames> : never
109

11-
export type ComponentConfig = Partial<{
10+
export type VuesticComponentPropsMap = {
1211
// key-value hack to avoid generics in type (like Omit, PropTypes, etc.)
1312
// `key: type` as result
1413
[componentName in VuesticComponentName]: {
1514
[key in keyof PropTypes<VuesticComponentsMap[componentName]>]?: PropTypes<VuesticComponentsMap[componentName]>[key]
1615
} & HTMLAttributes
17-
} & { all: Props, presets: Presets }>
16+
}
17+
18+
export type Props = { [propName: string]: any }
19+
20+
type VuesticComponentSlotsMap = {
21+
[componentName in VuesticComponentName]: {
22+
[key in keyof RemoveIndex<ComponentSlots<VuesticComponentsMap[componentName]>>]?: ComponentSlots<VuesticComponentsMap[componentName]>[key]
23+
}
24+
}
25+
26+
type SlotPropPrefix<T extends string> = `slot:${T}`
27+
28+
export type SlotProp<Scope> = VNode | string | DefineComponent<Partial<Scope>, {}, {}, {}, {}>
29+
30+
type VuesticComponentSlotPropsMap = {
31+
[componentName in VuesticComponentName]: {
32+
// @ts-ignore
33+
[key in keyof VuesticComponentSlotsMap[componentName] as SlotPropPrefix<key>]: SlotProp<Parameters<VuesticComponentSlotsMap[componentName][key]>[0]>
34+
}
35+
}
36+
37+
type VuesticComponentPreset<T extends VuesticComponentName> = VuesticComponentPropsMap[T] & VuesticComponentSlotPropsMap[T]
38+
39+
export type Presets = {
40+
[componentName in VuesticComponentName]?: {
41+
[presetName: string]: VuesticComponentPreset<componentName>
42+
}
43+
}
44+
45+
export type ComponentConfig = Partial<VuesticComponentPropsMap & { all: Props, presets: Presets }>
1846

1947
export type { DefineComponent as VuesticComponent } from 'vue'

packages/ui/src/services/component-config/utils/use-component-config-props.ts

+44-13
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,62 @@
1-
import type { VuesticComponent, VuesticComponentName, Props } from '../types'
1+
import { VuesticComponentName, Props, VuesticComponent } from '../types'
22
import { useLocalConfig } from '../../../composables/useLocalConfig'
33
import { useGlobalConfig } from '../../global-config/global-config'
44
import { computed } from 'vue'
5-
import { injectChildPresetPropFromParent } from '../../../composables/useChildComponents'
5+
import { injectChildPropsFromParent } from '../../../composables/useChildComponents'
6+
import { ComponentPresetProp, PresetPropValue } from '../../../composables'
7+
import { notNil } from '../../../utils/isNilValue'
8+
import { head } from 'lodash'
9+
10+
const withPresetProp = <P extends Props>(props: P): props is P & ComponentPresetProp => 'preset' in props
11+
const getPresetProp = <P extends Props>(props: P) => withPresetProp(props) ? props.preset : undefined
612

713
export const useComponentConfigProps = <T extends VuesticComponent>(component: T, originalProps: Props) => {
814
const localConfig = useLocalConfig()
915
const { globalConfig } = useGlobalConfig()
1016

11-
const instancePreset = computed(() => originalProps.preset)
12-
const getPresetProps = (presetName: string) => globalConfig.value.components?.presets?.[component.name as VuesticComponentName]?.[presetName]
13-
const parentPropPreset = injectChildPresetPropFromParent()
17+
const componentName = component.name as VuesticComponentName
18+
19+
const getPresetProps = (presetPropValue: PresetPropValue): Props => {
20+
return (presetPropValue instanceof Array ? presetPropValue : [presetPropValue]).reduce<Props>((acc, presetName) => {
21+
const presetProps = globalConfig.value.components?.presets?.[componentName]?.[presetName]
22+
23+
if (!presetProps) {
24+
return acc
25+
}
26+
27+
const extendedPresets = getPresetProp(presetProps)
28+
29+
return {
30+
...acc,
31+
...(extendedPresets ? getPresetProps(extendedPresets) : undefined),
32+
...presetProps,
33+
}
34+
}, {})
35+
}
36+
const parentInjectedProps = injectChildPropsFromParent()
1437

1538
return computed(() => {
1639
const globalConfigProps: Props = {
1740
...globalConfig.value.components?.all,
18-
...globalConfig.value.components?.[component.name as VuesticComponentName],
41+
...globalConfig.value.components?.[componentName],
1942
}
2043

21-
const localConfigProps: Props = localConfig.value
22-
.reduce((finalConfig, config) => config[component.name as VuesticComponentName]
23-
? { ...finalConfig, ...config[component.name as VuesticComponentName] }
24-
: finalConfig
25-
, {})
44+
const localConfigProps = localConfig.value
45+
.reduce<Props>((finalConfig, config) => {
46+
const componentConfigProps = config[componentName]
47+
48+
return componentConfigProps
49+
? { ...finalConfig, ...componentConfigProps }
50+
: finalConfig
51+
}, {})
2652

27-
const presetName = parentPropPreset?.value || instancePreset.value || localConfigProps.preset || globalConfigProps.preset
28-
const presetProps = presetName && getPresetProps(presetName)
53+
const presetProp = head([
54+
originalProps,
55+
parentInjectedProps?.value,
56+
localConfigProps,
57+
globalConfigProps,
58+
].filter(notNil).map(getPresetProp).filter(notNil))
59+
const presetProps = presetProp ? getPresetProps(presetProp) : undefined
2960

3061
return { ...globalConfigProps, ...localConfigProps, ...presetProps }
3162
})

packages/ui/src/services/config-transport/createRenderFn.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import { withCtx, h, DefineComponent, VNode, isVNode, Text, createBlock } from 'vue'
2+
import type { SlotProp } from '../component-config'
23

34
type VueInternalRenderFunction = Function
45

56
export const renderSlotNode = (node: VNode, ctx = null) => {
67
return withCtx(() => [node], ctx)
78
}
89

9-
export const makeVNode = (node: VNode | string | DefineComponent) => {
10+
export const makeVNode = <T>(node: SlotProp<T>) => {
1011
if (typeof node === 'string') {
1112
return h(Text, node)
1213
}

packages/ui/src/utils/component-options/types.ts

+5
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ export type ComponentProps<T> =
66
T extends (props: infer P, ...args: any) => any ? P :
77
unknown;
88

9+
export type ComponentSlots<T> =
10+
T extends new () => { $slots: infer S; } ? NonNullable<S> :
11+
T extends (props: any, ctx: { slots: infer S; attrs: any; emit: any; }, ...args: any) => any ? NonNullable<S> :
12+
{};
13+
914
export type UnKeyofString<T> = T extends infer E & ThisType<void> ? E : never
1015
export type ExtractVolarEmitsType<T> = 'emits' extends keyof T
1116
? UnKeyofString<(T['emits'] extends infer E | undefined ? E : never)>

packages/ui/src/utils/isNilValue.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1+
const nilValues = [null, undefined, '' as const]
2+
13
/**
24
* Checks if provided value not exists.
35
*
46
* @param value any value to check it.
57
*/
68
export const isNilValue = (value: any): value is null | undefined | '' => {
79
// lodash `isNil` isn't an alternative, because we also want to handle empty string values
8-
return [null, undefined, ''].includes(value)
10+
return nilValues.includes(value)
911
}
12+
13+
export const notNil = <T>(value: T): value is NonNullable<T> => !isNilValue(value)
+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
type RemoveIndex<T> = {
2+
[ K in keyof T as
3+
string extends K
4+
? never
5+
: number extends K
6+
? never
7+
: symbol extends K
8+
? never
9+
: K
10+
]: T[K];
11+
}

0 commit comments

Comments
 (0)