Skip to content

Commit

Permalink
Added ability to apply multiple presets (#4303)
Browse files Browse the repository at this point in the history
* feat(presets): Added ability to apply multiple presets

* chore: remove lodash fn

* chore: remove RemoveIndex type from globalls

---------

Co-authored-by: Maksim Nedoshev <[email protected]>
  • Loading branch information
Fsss126 and m0ksem committed Jul 23, 2024
1 parent 288c611 commit cf3d2d1
Show file tree
Hide file tree
Showing 14 changed files with 161 additions and 41 deletions.
1 change: 1 addition & 0 deletions packages/docs/config/vuestic-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
1 change: 1 addition & 0 deletions packages/docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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' }
},
},
},
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<template>
<VaButton
:preset="['addToCart', 'promotion']"
/>
</template>
Original file line number Diff line number Diff line change
Expand Up @@ -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:"),
Expand All @@ -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 }),
Expand Down
5 changes: 4 additions & 1 deletion packages/docs/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,8 @@
"types": [
"vite/client",
]
}
},
"exclude": [
"page-config/**/**/code/*.ts",
]
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import times from 'lodash/times.js'
const times = (n: number, fn: () => any) => Array.from({ length: n }, fn)

export const getInitialRecords = (amount = 50) => times(amount, () => ({ text: 'record', id: Math.random() }))

Expand Down
16 changes: 0 additions & 16 deletions packages/ui/src/composables/useChildComponents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
8 changes: 7 additions & 1 deletion packages/ui/src/composables/useComponentPreset.ts
Original file line number Diff line number Diff line change
@@ -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<PresetPropValue>,
default: undefined,
},
}

export type ComponentPresetProp = ExtractPropTypes<typeof useComponentPresetProp>
70 changes: 65 additions & 5 deletions packages/ui/src/services/component-config/types.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,79 @@
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'

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> = C extends { new(): { $props: infer Props } } ? Omit<Props, VueDefaultPropNames> : 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<VuesticComponentsMap[componentName]>]?: PropTypes<VuesticComponentsMap[componentName]>[key]
} & HTMLAttributes
} & { all: Props, presets: Presets }>
}

export type Props = { [propName: string]: any }

/**
* Removes index signature from object
*
* @example
*
* Type:
* ```
* RemoveIndex<{
* [index: string]: number
* a: number
* b: number
* }>
* ```
* Converts to
*
* ```
* { a: number, b: number }
* ```
*
*/
type RemoveIndex<T> = {
[ K in keyof T as
string extends K
? never
: number extends K
? never
: symbol extends K
? never
: K
]: T[K];
}

type VuesticComponentSlotsMap = {
[componentName in VuesticComponentName]: {
[key in keyof RemoveIndex<ComponentSlots<VuesticComponentsMap[componentName]>>]?: ComponentSlots<VuesticComponentsMap[componentName]>[key]
}
}

type SlotPropPrefix<T extends string> = `slot:${T}`

export type SlotProp<Scope> = VNode | string | DefineComponent<Partial<Scope>, {}, {}, {}, {}>

type VuesticComponentSlotPropsMap = {
[componentName in VuesticComponentName]: {
// @ts-ignore
[key in keyof VuesticComponentSlotsMap[componentName] as SlotPropPrefix<key>]: SlotProp<Parameters<VuesticComponentSlotsMap[componentName][key]>[0]>
}
}

type VuesticComponentPreset<T extends VuesticComponentName> = VuesticComponentPropsMap[T] & VuesticComponentSlotPropsMap[T]

export type Presets = {
[componentName in VuesticComponentName]?: {
[presetName: string]: VuesticComponentPreset<componentName>
}
}

export type ComponentConfig = Partial<VuesticComponentPropsMap & { all: Props, presets: Presets }>

export type { DefineComponent as VuesticComponent } from 'vue'
Original file line number Diff line number Diff line change
@@ -1,31 +1,66 @@
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'

const withPresetProp = <P extends Props>(props: P): props is P & ComponentPresetProp => 'preset' in props
const getPresetProp = <P extends Props>(props: P) => withPresetProp(props) ? props.preset : undefined

export const useComponentConfigProps = <T extends VuesticComponent>(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<Props>((acc, presetName) => {
const presetProps = globalConfig.value.components?.presets?.[componentName]?.[presetName]

if (!presetProps) {
return acc
}

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<Props>((finalConfig, config) => {
const componentConfigProps = config[componentName]

return componentConfigProps
? { ...finalConfig, ...componentConfigProps }
: finalConfig
}, {})

const presetProp = [
originalProps,
parentInjectedProps?.value,
localConfigProps,
globalConfigProps,
]
.filter(notNil)
.map(getPresetProp)
.filter(notNil)
.at(0)

const presetName = parentPropPreset?.value || instancePreset.value || localConfigProps.preset || globalConfigProps.preset
const presetProps = presetName && getPresetProps(presetName)
const presetProps = presetProp ? getPresetProps(presetProp) : undefined

return { ...globalConfigProps, ...localConfigProps, ...presetProps }
})
Expand Down
3 changes: 2 additions & 1 deletion packages/ui/src/services/config-transport/createRenderFn.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { withCtx, h, DefineComponent, VNode, isVNode, Text, createBlock } from 'vue'
import type { SlotProp } from '../component-config'

type VueInternalRenderFunction = Function

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

export const makeVNode = (node: VNode | string | DefineComponent) => {
export const makeVNode = <T>(node: SlotProp<T>) => {
if (typeof node === 'string') {
return h(Text, node)
}
Expand Down
5 changes: 5 additions & 0 deletions packages/ui/src/utils/component-options/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ export type ComponentProps<T> =
T extends (props: infer P, ...args: any) => any ? P :
unknown;

export type ComponentSlots<T> =
T extends new () => { $slots: infer S; } ? NonNullable<S> :
T extends (props: any, ctx: { slots: infer S; attrs: any; emit: any; }, ...args: any) => any ? NonNullable<S> :
{};

export type UnKeyofString<T> = T extends infer E & ThisType<void> ? E : never
export type ExtractVolarEmitsType<T> = 'emits' extends keyof T
? UnKeyofString<(T['emits'] extends infer E | undefined ? E : never)>
Expand Down
10 changes: 8 additions & 2 deletions packages/ui/src/utils/isNilValue.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
const nilValues = [null, undefined, '' as const]
const nullOrUndefined = [null, undefined]

/**
* Checks if provided value not exists.
*
* @param value any value to check it.
*/
export const isNilValue = (value: any): value is null | undefined | '' => {
return [null, undefined, ''].includes(value)
// lodash `isNil` isn't an alternative, because we also want to handle empty string values
return nilValues.includes(value)
}

export const notNil = <T>(value: T): value is NonNullable<T> => !isNilValue(value)

export const isNil = (value: any): value is null | undefined => {
return [null, undefined].includes(value)
return nullOrUndefined.includes(value)
}

0 comments on commit cf3d2d1

Please sign in to comment.