Skip to content

Commit

Permalink
feat: Add reactivity to useDefaults & AConfig (#184)
Browse files Browse the repository at this point in the history
Co-authored-by: jd-solanki <[email protected]>
  • Loading branch information
IcetCode and jd-solanki authored Jun 13, 2023
1 parent a92f222 commit 7ae732f
Show file tree
Hide file tree
Showing 7 changed files with 143 additions and 45 deletions.
16 changes: 8 additions & 8 deletions packages/anu-vue/src/components/config/AConfig.vue
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
<script lang="ts" setup>
import { aConfigProps } from './meta'
import { mergePropsDefaults } from '@/composables/useDefaults'
import { ANU_DEFAULTS } from "@/symbols"
import { ANU_PROPS_DEFAULTS } from "@/symbols"
const props = defineProps(aConfigProps)
defineOptions({
name: 'AConfig',
})
const defaults = inject(ANU_DEFAULTS)
watch(
() => props.props,
() => {
provide(ANU_DEFAULTS, mergePropsDefaults(defaults, props.props))
},
{ immediate: true },
const defaults = inject(ANU_PROPS_DEFAULTS)
// ℹ️ Pass new reactive value to avoid updates in upward tree
provide(
ANU_PROPS_DEFAULTS,
computed(() => mergePropsDefaults(defaults, props.props)),
)
</script>

Expand Down
12 changes: 12 additions & 0 deletions packages/anu-vue/src/components/config/meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,22 @@ import type { PluginOptions } from '@/plugin'

// 👉 Props
export const aConfigProps = {
/**
* Component props defaults. Similar to what you pass to `propsDefaults` while initializing Anu plugin.
*/
props: {
type: Object as PropType<PluginOptions['propsDefaults']>,
default: {},
},
} as const

export type AConfigProps = ExtractPublicPropTypes<typeof aConfigProps>

// 👉 Slots
export const aAlertSlots = {

/**
* Default slot to render components affected by provided config
*/
default: (_: any) => null,
} as const
4 changes: 1 addition & 3 deletions packages/anu-vue/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,7 @@ export { ABtn } from './btn'
export { ACard } from './card'
export { ACheckbox } from './checkbox'
export { AChip } from './chip'

// ℹ️ It's not ready yet
// export { AConfig } from './config'
export { AConfig } from './config'
export { ADataTable } from './data-table'
export type { ADataTableItemsFunction, ADataTableItemsFunctionParams, ADataTableProps } from './data-table'
export { ADialog } from './dialog'
Expand Down
75 changes: 49 additions & 26 deletions packages/anu-vue/src/composables/useDefaults.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { objectKeys, objectPick } from '@antfu/utils'
import { deepmergeCustom } from 'deepmerge-ts'
import { type StyleValue } from 'vue'
import { ANU_DEFAULTS } from '@/symbols'
import type { Ref, StyleValue } from 'vue'
import { toValue } from 'vue'
import { ANU_PROPS_DEFAULTS } from '@/symbols'
import type { PluginOptionDefaults } from '@/pluginDefaults'
import type { PluginOptions } from '@/plugin'

export const mergePropsDefaults = deepmergeCustom({
mergeArrays: false,
Expand All @@ -11,25 +13,41 @@ export const mergePropsDefaults = deepmergeCustom({
interface ReturnType<Props> {
props: Props
// eslint-disable-next-line @typescript-eslint/no-explicit-any
defaultsClass: any
defaultsStyle: StyleValue | undefined
defaultsAttrs: Record<string, unknown> | undefined
defaultsClass: Ref<any>
defaultsStyle: Ref<StyleValue | undefined>
defaultsAttrs: Ref<Record<string, unknown> | undefined>
}

export function useDefaults<Props extends Record<string, unknown>>(definitionProps: Props, componentName?: keyof PluginOptionDefaults): ReturnType<Props> {
const propsDefaults = inject(ANU_DEFAULTS, {})

const vm = getCurrentInstance()
const _componentName = (componentName ?? vm?.type.name ?? vm?.type.__name) as keyof PluginOptionDefaults | undefined

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

const { class: defaultsClass, style: defaultsStyle, attrs: defaultsAttrs, ...restProps } = propsDefaults[_componentName] || {}
// Get defaults
const propsDefaults = inject(ANU_PROPS_DEFAULTS, {})

// New defaults
const newPropsDefaults = ref({}) as Ref<PluginOptions['propsDefaults']>

// ℹ️ Pass new reactive value to avoid updates in upward tree
provide(ANU_PROPS_DEFAULTS, newPropsDefaults)

// Return Values
const propsRef = ref() as Ref<ReturnType<Props>['props']>
const defaultsClass = ref() as ReturnType<Props>['defaultsClass']
const defaultsStyle = ref() as ReturnType<Props>['defaultsStyle']
const defaultsAttrs = ref() as ReturnType<Props>['defaultsAttrs']

const calculateProps = () => {
const _propsDefaults = toValue(propsDefaults)
const { class: _class, style, attrs, ...restProps } = _propsDefaults[_componentName] || {}

// console.log('restProps :>> ', restProps);
defaultsClass.value = _class
defaultsStyle.value = style
defaultsAttrs.value = attrs

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

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

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

const propsRef = computedWithControl(
() => definitionProps,
() => {
const explicitPropsNames = objectKeys(vm?.vnode.props || {}) as unknown as (keyof Props)[]
const explicitProps = objectPick(definitionProps, explicitPropsNames)

return mergePropsDefaults(definitionProps, defaultsProps, explicitProps) as Props
},
)
propsRef.value = mergePropsDefaults(definitionProps, componentProps, explicitProps) as Props
}

watch(
() => definitionProps,
propsRef.trigger,
{ deep: true },
[
() => definitionProps,
() => toValue(propsDefaults),
],
calculateProps,
{
deep: true,
immediate: true,
},
)

return {
Expand Down
12 changes: 6 additions & 6 deletions packages/anu-vue/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { PartialDeep } from 'type-fest'
import type { App } from 'vue'
import { defineComponent } from 'vue'
import type { PluginOptionDefaults } from './pluginDefaults'
import { ANU_CONFIG, ANU_DEFAULTS } from "@/symbols"
import { ANU_CONFIG, ANU_PROPS_DEFAULTS } from "@/symbols"
import { useDefaults } from '@/composables/useDefaults'
import { useAnu } from '@/composables/useAnu'
import * as components from '@/components'
Expand All @@ -23,7 +23,7 @@ export interface PluginOptions {
registerComponents: boolean
initialTheme: keyof ConfigThemes
themes: ConfigThemes
aliases: Record<string, any>
componentAliases: Record<string, any>
propsDefaults: PartialDeep<PluginOptionDefaults>
}

Expand Down Expand Up @@ -64,7 +64,7 @@ const configDefaults: PluginOptions = {
},
},
},
aliases: {},
componentAliases: {},
propsDefaults: {},
}

Expand All @@ -81,8 +81,8 @@ export const plugin = {
}
}

for (const aliasComponentName in config.aliases) {
const baseComponent = config.aliases[aliasComponentName]
for (const aliasComponentName in config.componentAliases) {
const baseComponent = config.componentAliases[aliasComponentName]

app.component(aliasComponentName, defineComponent({
...baseComponent,
Expand All @@ -99,7 +99,7 @@ export const plugin = {
}

app.provide(ANU_CONFIG, config)
app.provide(ANU_DEFAULTS, config.propsDefaults)
app.provide(ANU_PROPS_DEFAULTS, config.propsDefaults)

// Initialize Anu instance with config values
useAnu({
Expand Down
4 changes: 2 additions & 2 deletions packages/anu-vue/src/symbols.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { InjectionKey } from "vue"
import type { InjectionKey, MaybeRef } from "vue"
import type { PluginOptions } from '@/plugin'

export const ANU_CONFIG = Symbol('ANU_CONFIG') as InjectionKey<PluginOptions>
export const ANU_DEFAULTS = Symbol("ANU_DEFAULTS") as InjectionKey<PluginOptions["propsDefaults"]>
export const ANU_PROPS_DEFAULTS = Symbol("ANU_PROPS_DEFAULTS") as InjectionKey<MaybeRef<PluginOptions["propsDefaults"]>>
65 changes: 65 additions & 0 deletions packages/anu-vue/test/AConfig.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { mount } from '@vue/test-utils'
import { afterEach, describe, expect, it } from 'vitest'
import { nextTick, ref } from 'vue'
import { AAlert, ABtn, ACard, AConfig } from '../src'

describe('AConfig', () => {
let wrapper: ReturnType<typeof mount<AConfig>>

function getSelectorStyle(selector: string) {
return wrapper.find(selector).attributes('style')
}

afterEach(() => {
wrapper.unmount()
})

it('should provide config to matched component', () => {
const props = ref({
ABtn: { color: 'success' },
AAlert: { color: 'info' },
})
wrapper = mount(() =>
<AConfig props={props.value}>
<AAlert>Alert</AAlert>
<ABtn>Btn</ABtn>
</AConfig>,
)
expect(getSelectorStyle('.a-btn')).toContain('--a-success')
expect(getSelectorStyle('.a-alert')).toContain('--a-info')
})

it('config can be reactive', async () => {
const props = ref({
ABtn: { color: 'success' },
})
wrapper = mount(() =>
<AConfig props={props.value}>
<ABtn>Btn</ABtn>
</AConfig>,
)
expect(getSelectorStyle('.a-btn')).toContain('--a-success')
props.value.ABtn.color = 'info'
await nextTick()
expect(getSelectorStyle('.a-btn')).toContain('--a-info')
})

it('should apply nested config correctly', () => {
const props = ref({
ABtn: { color: 'success' },
})
const nestedProps = ref({
ABtn: { color: 'info' },
})
wrapper = mount(() =>
<AConfig props={props.value}>
<ACard>
<AConfig props={nestedProps.value}>
<ABtn>Btn</ABtn>
</AConfig>
</ACard>
</AConfig>,
)
expect(getSelectorStyle('.a-btn')).toContain('--a-info')
})
})

0 comments on commit 7ae732f

Please sign in to comment.