Skip to content

feat(design-config): design-config adds arbitrary attribute features of global configuration components #3419

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
May 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ const design = {
icons: {
warning: iconWarningTriangle()
},
props: {
center: true
},
/**
*
* @param {*} props 组件属性集合
Expand Down
3 changes: 3 additions & 0 deletions examples/sites/demos/pc/app/config-provider/base.vue
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ export default {
}
},
Alert: {
props: {
center: true
},
icons: {
warning: iconWarningTriangle()
},
Expand Down
5 changes: 4 additions & 1 deletion examples/sites/demos/pc/app/config-provider/basic.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ test('测试自定义事件', async ({ page }) => {
page.on('pageerror', (exception) => expect(exception).toBeNull())
await page.goto('config-provider#base')

// 验证自定义方法
const demo = page.locator('#base')
// 验证文字居中
await expect(demo.locator('.tiny-alert')).toHaveCSS('justify-content', 'center')

// 验证自定义方法
await demo.locator('.tiny-config-provider .tiny-alert > .tiny-alert__close').click()
await page.waitForTimeout(500)
await expect(page.locator('.tiny-modal > .tiny-modal__box').nth(1)).toHaveText('触发自定方法')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ export default {
},
desc: {
'zh-CN':
'可通过<code> design </code>属性设置自定义不同设计规范的图标和逻辑,例如:全局配置 Form 表单组件的必填星号是否默认显示、Button 组件的点击后的禁用时间和是否默认圆角。',
'通过 <code>design</code> 属性可以自定义不同设计规范的图标和逻辑。从 3.23.0 版本开始,支持全局配置组件的任意 <code>props</code> 属性(仅支持双层组件),例如:可以全局配置 Form 组件必填项星号的默认显示状态、Button 组件的点击防抖时间以及是否默认显示圆角等。',
'en-US':
'Icons and logic for different design specifications can be customized through the <code>design</code> attribute configuration.'
'You can use the <code> design </code> property to set custom icons and logic for different design specifications, starting from version 3.23.0, the global configuration component (only supports double-layer components) supports the function of any <code> props </code> attribute, for example: the default display of the required star of the global configuration Form form component, the disabled time after the click of the Button component, and whether the default roundness is enabled.'
},
codeFiles: ['base.vue']
},
Expand Down
16 changes: 15 additions & 1 deletion packages/vue-common/src/adapter/vue2.7/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,32 @@ export const renderComponent = ({
view = null as any,
component = null as any,
props,
customDesignProps,
context: { attrs, listeners: on, slots },
extend = {}
}) => {
return () =>
hooks.h(
(view && view.value) || component,
Object.assign({ props, attrs, [extend.isSvg ? 'nativeOn' : 'on']: on, scopedSlots: { ...slots } }, extend)
Object.assign(
{
props: { ...props, ...customDesignProps },
attrs,
[extend.isSvg ? 'nativeOn' : 'on']: on,
scopedSlots: { ...slots }
},
extend
)
)
}

export const rootConfig = () => hooks.getCurrentInstance()?.proxy.$root

export const getCustomProps = () => {
const instance = hooks.getCurrentInstance()?.proxy
return instance?.$options?.propsData || {}
}
Comment on lines +50 to +53
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

getCustomProps added, but the surrounding adapter is still oblivious to the new customDesignProps flow

getCustomProps itself looks fine, yet the Vue 2.7 adapter never forwards the merged customDesignProps object that $setup now passes in.
As a result, any default props coming from the global design-config are silently ignored in a Vue 2.7 runtime, breaking the very feature this PR introduces (e.g. the new Alert.center default will not work).

Minimal patch (works for both Vue 2 and Vue 2.7 adapters):

 export const renderComponent = ({
   view = null as any,
   component = null as any,
   props,
+  customDesignProps = {},
   context: { attrs, listeners: on, slots },
   extend = {}
 }) => {
   return () =>
     hooks.h(
       (view && view.value) || component,
-      Object.assign({ props, attrs, [extend.isSvg ? 'nativeOn' : 'on']: on, scopedSlots: { ...slots } }, extend)
+      Object.assign(
+        { props: { ...customDesignProps, ...props }, attrs, [extend.isSvg ? 'nativeOn' : 'on']: on, scopedSlots: { ...slots } },
+        extend
+      )
     )
 }

Key points

  1. Keep the existing ordering so user props still win over design defaults.
  2. Provide a default {} to stay backwards-compatible with callers that do not yet supply the parameter.

Please mirror the same change in packages/vue-common/src/adapter/vue2/index.ts to keep all Vue-2 builds consistent.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const getCustomProps = () => {
const instance = hooks.getCurrentInstance()?.proxy
return instance?.$options?.propsData || {}
}
export const renderComponent = ({
view = null as any,
component = null as any,
props,
customDesignProps = {},
context: { attrs, listeners: on, slots },
extend = {}
}) => {
return () =>
hooks.h(
(view && view.value) || component,
Object.assign(
{ props: { ...customDesignProps, ...props }, attrs, [extend.isSvg ? 'nativeOn' : 'on']: on, scopedSlots: { ...slots } },
extend
)
)
}


export const getComponentName = () => {
// 此处组件最多为两层组件,所以对多获取到父级组件即可
const instance = hooks.getCurrentInstance()
Expand Down
14 changes: 13 additions & 1 deletion packages/vue-common/src/adapter/vue2/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,21 +34,33 @@ export const renderComponent = ({
view = null as any,
component = null as any,
props,
customDesignProps,
context: { attrs, listeners: on, slots },
extend = {}
}) => {
return () =>
hooks.h(
(view && view.value) || component,
Object.assign(
{ props, attrs, [extend.isSvg ? 'nativeOn' : 'on']: on, ref: 'modeTemplate', scopedSlots: { ...slots } },
{
props: { ...props, ...customDesignProps },
attrs,
[extend.isSvg ? 'nativeOn' : 'on']: on,
ref: 'modeTemplate',
scopedSlots: { ...slots }
},
extend
)
)
}

export const rootConfig = () => hooks.getCurrentInstance()?.proxy.$root

export const getCustomProps = () => {
const instance = hooks.getCurrentInstance()?.proxy
return instance?.$options?.propsData || {}
}

export const getComponentName = () => {
// 此处组件最多为两层组件,所以对多获取到父级组件即可
const instance = hooks.getCurrentInstance()
Expand Down
13 changes: 12 additions & 1 deletion packages/vue-common/src/adapter/vue3/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,16 @@ export const renderComponent = ({
view = undefined as any,
component = undefined as any,
props,
customDesignProps,
context: { attrs, slots },
extend = {}
}) => {
return () => hooks.h((view && view.value) || component, { ref: 'modeTemplate', ...props, ...attrs, ...extend }, slots)
return () =>
hooks.h(
(view && view.value) || component,
{ ref: 'modeTemplate', ...props, ...attrs, ...customDesignProps, ...extend },
slots
)
}

export const rootConfig = (context) => {
Expand All @@ -38,6 +44,11 @@ export const rootConfig = (context) => {
return instance?.appContext.config.globalProperties
}

export const getCustomProps = () => {
const instance = hooks.getCurrentInstance()
return instance?.vnode?.props || {}
}
Comment on lines +47 to +50
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Prefer instance.props to capture camel-cased prop keys

instance.vnode.props gives you the raw vnode props which may preserve kebab-cased keys and include attrs/events.
instance.props exposes the normalised reactive props object, mapping kebab-case to camelCase – a closer match to the keys declared in designConfig.props.

-export const getCustomProps = () => {
-  const instance = hooks.getCurrentInstance()
-  return instance?.vnode?.props || {}
-}
+export const getCustomProps = () => {
+  const instance = hooks.getCurrentInstance()
+  return instance?.props || {}
+}

This avoids false negatives when a user passes center="false" (kebab) but the design config checks for center (camel).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const getCustomProps = () => {
const instance = hooks.getCurrentInstance()
return instance?.vnode?.props || {}
}
export const getCustomProps = () => {
const instance = hooks.getCurrentInstance()
return instance?.props || {}
}


export const getComponentName = () => {
// 此处组件最多为两层组件,所以对多获取到父级组件即可
const instance = hooks.getCurrentInstance()
Expand Down
62 changes: 44 additions & 18 deletions packages/vue-common/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
tools,
useRouter,
getComponentName,
getCustomProps,
isVnode
} from './adapter'
import { t } from '@opentiny/vue-locale'
Expand Down Expand Up @@ -122,19 +123,6 @@ const resolveChartTheme = (props, context) => {
return tinyChartTheme
}

export const $setup = ({ props, context, template, extend = {} }) => {
const mode = resolveMode(props, context)
const view = hooks.computed(() => {
if (typeof props.tiny_template !== 'undefined') return props.tiny_template

const component = template(mode, props)

return typeof component === 'function' ? defineAsyncComponent(component) : component
})

return renderComponent({ view, props, context, extend })
}

// 提供给没有renderless层的组件使用(比如TinyVuePlus组件)
export const design = {
configKey: Symbol('designConfigKey'),
Expand Down Expand Up @@ -168,17 +156,55 @@ export const customDesignConfig: CustomDesignConfig = {
twMerge: () => ''
}

export const mergeClass = (...cssClasses) => customDesignConfig.twMerge(stringifyCssClass(cssClasses))

export const setup = ({ props, context, renderless, api, extendOptions = {}, mono = false, classes = {} }) => {
const render = typeof props.tiny_renderless === 'function' ? props.tiny_renderless : renderless

const getDesignConfig = () => {
// 获取组件级配置和全局配置(inject需要带有默认值,否则控制台会报警告)
let globalDesignConfig: DesignConfig = customDesignConfig.designConfig || hooks.inject(design.configKey, {})

// globalDesignConfig 可能是响应式对象,比如 computed
globalDesignConfig = globalDesignConfig?.value || globalDesignConfig || {}
const designConfig = globalDesignConfig?.components?.[getComponentName().replace($prefix, '')]
return {
designConfig,
globalDesignConfig
}
}

export const $setup = ({ props: propData, context, template, extend = {} }) => {
const mode = resolveMode(propData, context)
const view = hooks.computed(() => {
if (typeof propData.tiny_template !== 'undefined') return propData.tiny_template

const component = template(mode, propData)

return typeof component === 'function' ? defineAsyncComponent(component) : component
})

const { designConfig } = getDesignConfig()
const customDesignProps = {}

const designProps = designConfig?.props

if (designProps) {
// 获取用户传递的props
const customProps = getCustomProps()

Object.keys(designProps).forEach((key) => {
// 用户没有配置的属性才进行覆盖
if (!Object.prototype.hasOwnProperty.call(customProps, key)) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ensure that Object.prototype.hasOwnProperty.call is used correctly to avoid potential issues with objects that may not inherit from Object.prototype. This is a critical check to prevent unexpected behavior.

customDesignProps[key] = designProps[key]
}
})
}

return renderComponent({ view, props: propData, customDesignProps, context, extend })
}

export const mergeClass = (...cssClasses) => customDesignConfig.twMerge(stringifyCssClass(cssClasses))

export const setup = ({ props, context, renderless, api, extendOptions = {}, mono = false, classes = {} }) => {
const render = typeof props.tiny_renderless === 'function' ? props.tiny_renderless : renderless

const { designConfig, globalDesignConfig } = getDesignConfig()
const utils = {
$prefix,
t,
Expand Down
2 changes: 1 addition & 1 deletion packages/vue/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -237,4 +237,4 @@
"build": "pnpm -w build:ui",
"postversion": "pnpm build"
}
}
}
Loading