Skip to content

Commit 7ae732f

Browse files
IcetCodejd-solanki
andauthored
feat: Add reactivity to useDefaults & AConfig (#184)
Co-authored-by: jd-solanki <jdsolanki0001@gmail.com>
1 parent a92f222 commit 7ae732f

File tree

7 files changed

+143
-45
lines changed

7 files changed

+143
-45
lines changed

packages/anu-vue/src/components/config/AConfig.vue

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
<script lang="ts" setup>
22
import { aConfigProps } from './meta'
33
import { mergePropsDefaults } from '@/composables/useDefaults'
4-
import { ANU_DEFAULTS } from '@/symbols'
4+
import { ANU_PROPS_DEFAULTS } from '@/symbols'
55
66
const props = defineProps(aConfigProps)
77
defineOptions({
88
name: 'AConfig',
99
})
10-
const defaults = inject(ANU_DEFAULTS)
11-
watch(
12-
() => props.props,
13-
() => {
14-
provide(ANU_DEFAULTS, mergePropsDefaults(defaults, props.props))
15-
},
16-
{ immediate: true },
10+
11+
const defaults = inject(ANU_PROPS_DEFAULTS)
12+
13+
// ℹ️ Pass new reactive value to avoid updates in upward tree
14+
provide(
15+
ANU_PROPS_DEFAULTS,
16+
computed(() => mergePropsDefaults(defaults, props.props)),
1717
)
1818
</script>
1919

packages/anu-vue/src/components/config/meta.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,22 @@ import type { PluginOptions } from '@/plugin'
55

66
// 👉 Props
77
export const aConfigProps = {
8+
/**
9+
* Component props defaults. Similar to what you pass to `propsDefaults` while initializing Anu plugin.
10+
*/
811
props: {
912
type: Object as PropType<PluginOptions['propsDefaults']>,
1013
default: {},
1114
},
1215
} as const
1316

1417
export type AConfigProps = ExtractPublicPropTypes<typeof aConfigProps>
18+
19+
// 👉 Slots
20+
export const aAlertSlots = {
21+
22+
/**
23+
* Default slot to render components affected by provided config
24+
*/
25+
default: (_: any) => null,
26+
} as const

packages/anu-vue/src/components/index.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,7 @@ export { ABtn } from './btn'
77
export { ACard } from './card'
88
export { ACheckbox } from './checkbox'
99
export { AChip } from './chip'
10-
11-
// ℹ️ It's not ready yet
12-
// export { AConfig } from './config'
10+
export { AConfig } from './config'
1311
export { ADataTable } from './data-table'
1412
export type { ADataTableItemsFunction, ADataTableItemsFunctionParams, ADataTableProps } from './data-table'
1513
export { ADialog } from './dialog'

packages/anu-vue/src/composables/useDefaults.ts

Lines changed: 49 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { objectKeys, objectPick } from '@antfu/utils'
22
import { deepmergeCustom } from 'deepmerge-ts'
3-
import { type StyleValue } from 'vue'
4-
import { ANU_DEFAULTS } from '@/symbols'
3+
import type { Ref, StyleValue } from 'vue'
4+
import { toValue } from 'vue'
5+
import { ANU_PROPS_DEFAULTS } from '@/symbols'
56
import type { PluginOptionDefaults } from '@/pluginDefaults'
7+
import type { PluginOptions } from '@/plugin'
68

79
export const mergePropsDefaults = deepmergeCustom({
810
mergeArrays: false,
@@ -11,25 +13,41 @@ export const mergePropsDefaults = deepmergeCustom({
1113
interface ReturnType<Props> {
1214
props: Props
1315
// eslint-disable-next-line @typescript-eslint/no-explicit-any
14-
defaultsClass: any
15-
defaultsStyle: StyleValue | undefined
16-
defaultsAttrs: Record<string, unknown> | undefined
16+
defaultsClass: Ref<any>
17+
defaultsStyle: Ref<StyleValue | undefined>
18+
defaultsAttrs: Ref<Record<string, unknown> | undefined>
1719
}
1820

1921
export function useDefaults<Props extends Record<string, unknown>>(definitionProps: Props, componentName?: keyof PluginOptionDefaults): ReturnType<Props> {
20-
const propsDefaults = inject(ANU_DEFAULTS, {})
21-
2222
const vm = getCurrentInstance()
2323
const _componentName = (componentName ?? vm?.type.name ?? vm?.type.__name) as keyof PluginOptionDefaults | undefined
2424

2525
if (!_componentName)
2626
throw new Error('Unable to identify the component name. Please define component name or use the `componentName` parameter while using `useDefaults` composable.')
2727

28-
const { class: defaultsClass, style: defaultsStyle, attrs: defaultsAttrs, ...restProps } = propsDefaults[_componentName] || {}
28+
// Get defaults
29+
const propsDefaults = inject(ANU_PROPS_DEFAULTS, {})
30+
31+
// New defaults
32+
const newPropsDefaults = ref({}) as Ref<PluginOptions['propsDefaults']>
33+
34+
// ℹ️ Pass new reactive value to avoid updates in upward tree
35+
provide(ANU_PROPS_DEFAULTS, newPropsDefaults)
36+
37+
// Return Values
38+
const propsRef = ref() as Ref<ReturnType<Props>['props']>
39+
const defaultsClass = ref() as ReturnType<Props>['defaultsClass']
40+
const defaultsStyle = ref() as ReturnType<Props>['defaultsStyle']
41+
const defaultsAttrs = ref() as ReturnType<Props>['defaultsAttrs']
42+
43+
const calculateProps = () => {
44+
const _propsDefaults = toValue(propsDefaults)
45+
const { class: _class, style, attrs, ...restProps } = _propsDefaults[_componentName] || {}
2946

30-
// console.log('restProps :>> ', restProps);
47+
defaultsClass.value = _class
48+
defaultsStyle.value = style
49+
defaultsAttrs.value = attrs
3150

32-
const { componentProps: defaultsProps, otherProps: subProps } = (() => {
3351
/* eslint-disable @typescript-eslint/no-explicit-any */
3452
const componentProps = {} as any
3553
const otherProps = {} as any
@@ -44,26 +62,31 @@ export function useDefaults<Props extends Record<string, unknown>>(definitionPro
4462
otherProps[key] = value
4563
})
4664

47-
return { componentProps, otherProps }
48-
})()
65+
// Provide subProps to the nested component
66+
// newDefaults.value = mergePropsDefaults(_propsDefaults, otherProps)
67+
/**
68+
* ℹ️ This line optimizes object by removing nested component's defaults from the current component tree
69+
* Assume we have { AAlert: { ABtn: { color: 'info' } } } then below line will move ABtn on top and remove it from children of AAlert
70+
* To see the difference log the result of `mergePropsDefaults(...)` of below line and comment line above
71+
*/
72+
newPropsDefaults.value = mergePropsDefaults({ ..._propsDefaults, [_componentName]: componentProps }, otherProps)
4973

50-
// Provide subProps to the nested component
51-
provide(ANU_DEFAULTS, mergePropsDefaults(propsDefaults, subProps))
74+
const explicitPropsNames = objectKeys(vm?.vnode.props || {}) as unknown as (keyof Props)[]
75+
const explicitProps = objectPick(definitionProps, explicitPropsNames)
5276

53-
const propsRef = computedWithControl(
54-
() => definitionProps,
55-
() => {
56-
const explicitPropsNames = objectKeys(vm?.vnode.props || {}) as unknown as (keyof Props)[]
57-
const explicitProps = objectPick(definitionProps, explicitPropsNames)
58-
59-
return mergePropsDefaults(definitionProps, defaultsProps, explicitProps) as Props
60-
},
61-
)
77+
propsRef.value = mergePropsDefaults(definitionProps, componentProps, explicitProps) as Props
78+
}
6279

6380
watch(
64-
() => definitionProps,
65-
propsRef.trigger,
66-
{ deep: true },
81+
[
82+
() => definitionProps,
83+
() => toValue(propsDefaults),
84+
],
85+
calculateProps,
86+
{
87+
deep: true,
88+
immediate: true,
89+
},
6790
)
6891

6992
return {

packages/anu-vue/src/plugin.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { PartialDeep } from 'type-fest'
33
import type { App } from 'vue'
44
import { defineComponent } from 'vue'
55
import type { PluginOptionDefaults } from './pluginDefaults'
6-
import { ANU_CONFIG, ANU_DEFAULTS } from '@/symbols'
6+
import { ANU_CONFIG, ANU_PROPS_DEFAULTS } from '@/symbols'
77
import { useDefaults } from '@/composables/useDefaults'
88
import { useAnu } from '@/composables/useAnu'
99
import * as components from '@/components'
@@ -23,7 +23,7 @@ export interface PluginOptions {
2323
registerComponents: boolean
2424
initialTheme: keyof ConfigThemes
2525
themes: ConfigThemes
26-
aliases: Record<string, any>
26+
componentAliases: Record<string, any>
2727
propsDefaults: PartialDeep<PluginOptionDefaults>
2828
}
2929

@@ -64,7 +64,7 @@ const configDefaults: PluginOptions = {
6464
},
6565
},
6666
},
67-
aliases: {},
67+
componentAliases: {},
6868
propsDefaults: {},
6969
}
7070

@@ -81,8 +81,8 @@ export const plugin = {
8181
}
8282
}
8383

84-
for (const aliasComponentName in config.aliases) {
85-
const baseComponent = config.aliases[aliasComponentName]
84+
for (const aliasComponentName in config.componentAliases) {
85+
const baseComponent = config.componentAliases[aliasComponentName]
8686

8787
app.component(aliasComponentName, defineComponent({
8888
...baseComponent,
@@ -99,7 +99,7 @@ export const plugin = {
9999
}
100100

101101
app.provide(ANU_CONFIG, config)
102-
app.provide(ANU_DEFAULTS, config.propsDefaults)
102+
app.provide(ANU_PROPS_DEFAULTS, config.propsDefaults)
103103

104104
// Initialize Anu instance with config values
105105
useAnu({

packages/anu-vue/src/symbols.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import type { InjectionKey } from 'vue'
1+
import type { InjectionKey, MaybeRef } from 'vue'
22
import type { PluginOptions } from '@/plugin'
33

44
export const ANU_CONFIG = Symbol('ANU_CONFIG') as InjectionKey<PluginOptions>
5-
export const ANU_DEFAULTS = Symbol('ANU_DEFAULTS') as InjectionKey<PluginOptions['propsDefaults']>
5+
export const ANU_PROPS_DEFAULTS = Symbol('ANU_PROPS_DEFAULTS') as InjectionKey<MaybeRef<PluginOptions['propsDefaults']>>
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { mount } from '@vue/test-utils'
2+
import { afterEach, describe, expect, it } from 'vitest'
3+
import { nextTick, ref } from 'vue'
4+
import { AAlert, ABtn, ACard, AConfig } from '../src'
5+
6+
describe('AConfig', () => {
7+
let wrapper: ReturnType<typeof mount<AConfig>>
8+
9+
function getSelectorStyle(selector: string) {
10+
return wrapper.find(selector).attributes('style')
11+
}
12+
13+
afterEach(() => {
14+
wrapper.unmount()
15+
})
16+
17+
it('should provide config to matched component', () => {
18+
const props = ref({
19+
ABtn: { color: 'success' },
20+
AAlert: { color: 'info' },
21+
})
22+
wrapper = mount(() =>
23+
<AConfig props={props.value}>
24+
<AAlert>Alert</AAlert>
25+
<ABtn>Btn</ABtn>
26+
</AConfig>,
27+
)
28+
expect(getSelectorStyle('.a-btn')).toContain('--a-success')
29+
expect(getSelectorStyle('.a-alert')).toContain('--a-info')
30+
})
31+
32+
it('config can be reactive', async () => {
33+
const props = ref({
34+
ABtn: { color: 'success' },
35+
})
36+
wrapper = mount(() =>
37+
<AConfig props={props.value}>
38+
<ABtn>Btn</ABtn>
39+
</AConfig>,
40+
)
41+
expect(getSelectorStyle('.a-btn')).toContain('--a-success')
42+
props.value.ABtn.color = 'info'
43+
await nextTick()
44+
expect(getSelectorStyle('.a-btn')).toContain('--a-info')
45+
})
46+
47+
it('should apply nested config correctly', () => {
48+
const props = ref({
49+
ABtn: { color: 'success' },
50+
})
51+
const nestedProps = ref({
52+
ABtn: { color: 'info' },
53+
})
54+
wrapper = mount(() =>
55+
<AConfig props={props.value}>
56+
<ACard>
57+
<AConfig props={nestedProps.value}>
58+
<ABtn>Btn</ABtn>
59+
</AConfig>
60+
</ACard>
61+
</AConfig>,
62+
)
63+
expect(getSelectorStyle('.a-btn')).toContain('--a-info')
64+
})
65+
})

0 commit comments

Comments
 (0)