fix(components): [descriptions] allow wrapping description-item inside components

This commit is contained in:
dopamine
2025-06-14 01:37:13 +08:00
parent d3dae80b70
commit 2e34a79d73
8 changed files with 130 additions and 61 deletions

View File

@@ -1,9 +1,21 @@
import { defineComponent } from 'vue'
import {
defineComponent,
getCurrentInstance,
inject,
onBeforeMount,
onBeforeUnmount,
} from 'vue'
import { columnAlignment } from '@element-plus/constants'
import { buildProps } from '@element-plus/utils'
import { buildProps, throwError } from '@element-plus/utils'
import { COMPONENT_NAME } from './constants'
import { descriptionsKey } from './token'
import type { ExtractPropTypes, Slot, VNode } from 'vue'
import type {
ComponentInternalInstance,
ExtractPropTypes,
Slot,
VNode,
} from 'vue'
export const descriptionItemProps = buildProps({
/**
@@ -82,6 +94,30 @@ export const descriptionItemProps = buildProps({
const DescriptionItem = defineComponent({
name: COMPONENT_NAME,
props: descriptionItemProps,
setup(props) {
const descriptions = inject(descriptionsKey)
if (!descriptions)
throwError(
COMPONENT_NAME,
'usage: <el-descriptions><el-descriptions-item /></el-descriptions/>'
)
const instance = getCurrentInstance()! as DescriptionItemInternalInstance
onBeforeMount(() => {
descriptions.addDescriptionItem({
uid: instance.uid,
props,
get vnode() {
return instance.vnode
},
})
})
onBeforeUnmount(() => {
descriptions.removeDescriptionItem(instance.uid)
})
},
render: () => '',
})
export default DescriptionItem
@@ -91,3 +127,6 @@ export type DescriptionItemVNode = VNode & {
children: { [name: string]: Slot } | null
props: Partial<DescriptionItemProps> | null
}
type DescriptionItemInternalInstance = ComponentInternalInstance & {
vnode: DescriptionItemVNode
}

View File

@@ -15,9 +15,20 @@
<div :class="ns.e('body')">
<table :class="[ns.e('table'), ns.is('bordered', border)]">
<tbody>
<template v-for="(row, _index) in getRows()" :key="_index">
<el-descriptions-row :row="row" />
</template>
<slot />
<rows-renderer
:magic-prop="
() => {
// `getRows()` returns vnodes that are not reactive,
// so the render functions cannot fully capture its own deps.
// This workaround forces Vue to update `rows-renderer`.
}
"
>
<template v-for="(row, _index) in getRows()" :key="_index">
<el-descriptions-row :row="row" />
</template>
</rows-renderer>
</tbody>
</table>
</div>
@@ -25,17 +36,17 @@
</template>
<script lang="ts" setup>
import { computed, provide, useSlots } from 'vue'
import { flattedChildren } from '@element-plus/utils'
import { useNamespace } from '@element-plus/hooks'
import { computed, getCurrentInstance, provide } from 'vue'
import { useNamespace, useOrderedChildren } from '@element-plus/hooks'
import { useFormSize } from '@element-plus/components/form'
import ElDescriptionsRow from './descriptions-row.vue'
import { descriptionsKey } from './token'
import { descriptionProps } from './description'
import { COMPONENT_NAME } from './constants'
import type { IDescriptionsInject } from './descriptions.type'
import type { DescriptionItemContext } from './type'
import type { DescriptionItemVNode } from './description-item'
import type { Slots } from 'vue'
defineOptions({
name: 'ElDescriptions',
@@ -47,9 +58,20 @@ const ns = useNamespace('descriptions')
const descriptionsSize = useFormSize()
const slots = useSlots()
const {
children: descriptionItems,
addChild: addDescriptionItem,
removeChild: removeDescriptionItem,
} = useOrderedChildren<DescriptionItemContext>(
getCurrentInstance()!,
COMPONENT_NAME
)
provide(descriptionsKey, props as IDescriptionsInject)
provide(descriptionsKey, {
props,
addDescriptionItem,
removeDescriptionItem,
})
const descriptionKls = computed(() => [ns.b(), ns.m(descriptionsSize.value)])
@@ -73,21 +95,19 @@ const filledNode = (
}
const getRows = () => {
if (!slots.default) return []
const children = descriptionItems.value
if (children.length === 0) return []
const children = flattedChildren(slots.default()).filter(
(node): node is DescriptionItemVNode =>
(node as any)?.type?.name === COMPONENT_NAME
)
const rows: DescriptionItemVNode[][] = []
let temp: DescriptionItemVNode[] = []
let count = props.column
let totalSpan = 0 // all spans number of item
const rowspanTemp: number[] = [] // the number of row spans
children.forEach((node, index) => {
const span = node.props?.span || 1
const rowspan = node.props?.rowspan || 1
children.forEach((child, index) => {
const node = child.vnode
const span = child.props?.span || 1
const rowspan = child.props?.rowspan || 1
const rowNo = rows.length
rowspanTemp[rowNo] ||= 0
@@ -127,4 +147,8 @@ const getRows = () => {
return rows
}
const RowsRenderer = (_: unknown, { slots }: { slots: Slots }) => {
return slots.default!()
}
</script>

View File

@@ -5,10 +5,7 @@ import { useNamespace } from '@element-plus/hooks'
import { descriptionsKey } from './token'
import type { DirectiveArguments, PropType, VNode } from 'vue'
import type {
IDescriptionsInject,
IDescriptionsItemInject,
} from './descriptions.type'
import type { IDescriptionsItemInject } from './type'
import type { DescriptionItemVNode } from './description-item'
export default defineComponent({
@@ -26,7 +23,7 @@ export default defineComponent({
},
},
setup() {
const descriptions = inject(descriptionsKey, {} as IDescriptionsInject)
const descriptions = inject(descriptionsKey)!
return {
descriptions,
@@ -42,7 +39,7 @@ export default defineComponent({
return [dir, value, arg, modifiers]
}) as DirectiveArguments
const { border, direction } = this.descriptions
const { border, direction } = this.descriptions.props
const isVertical = direction === 'vertical'
const renderLabel = () => this.cell?.children?.label?.() || item.label
const renderContent = () => this.cell?.children?.default?.()
@@ -54,7 +51,7 @@ export default defineComponent({
const labelClassName = item.labelClassName
const width =
this.type === 'label'
? item.labelWidth || this.descriptions.labelWidth || item.width
? item.labelWidth || this.descriptions.props.labelWidth || item.width
: item.width
const style = {
@@ -109,7 +106,9 @@ export default defineComponent({
default: {
const label = renderLabel()
const labelStyle: Record<string, any> = {}
const width = addUnit(item.labelWidth || this.descriptions.labelWidth)
const width = addUnit(
item.labelWidth || this.descriptions.props.labelWidth
)
if (width) {
labelStyle.width = width
labelStyle.display = 'inline-block'

View File

@@ -1,5 +1,5 @@
<template>
<template v-if="descriptions.direction === 'vertical'">
<template v-if="descriptions.props.direction === 'vertical'">
<tr>
<template v-for="(cell, _index) in row" :key="`tr1-${_index}`">
<el-descriptions-cell :cell="cell" tag="th" type="label" />
@@ -13,7 +13,7 @@
</template>
<tr v-else>
<template v-for="(cell, _index) in row" :key="`tr3-${_index}`">
<template v-if="descriptions.border">
<template v-if="descriptions.props.border">
<el-descriptions-cell :cell="cell" tag="td" type="label" />
<el-descriptions-cell :cell="cell" tag="td" type="content" />
</template>
@@ -29,13 +29,11 @@ import ElDescriptionsCell from './descriptions-cell'
import { descriptionsKey } from './token'
import { descriptionsRowProps } from './descriptions-row'
import type { IDescriptionsInject } from './descriptions.type'
defineOptions({
name: 'ElDescriptionsRow',
})
defineProps(descriptionsRowProps)
const descriptions = inject(descriptionsKey, {} as IDescriptionsInject)
const descriptions = inject(descriptionsKey)!
</script>

View File

@@ -1,24 +0,0 @@
import type { ColumnAlignment, ComponentSize } from '@element-plus/constants'
export interface IDescriptionsInject {
border: boolean
column: number
direction: 'horizontal' | 'vertical'
size: ComponentSize
title: string
extra: string
labelWidth: string | number
}
export interface IDescriptionsItemInject {
label: string
span: number
rowspan: number
width: string | number
minWidth: string | number
labelWidth: string | number
align: ColumnAlignment
labelAlign?: ColumnAlignment
className: string
labelClassName: string
}

View File

@@ -1,5 +1,5 @@
import type { InjectionKey } from 'vue'
import type { IDescriptionsInject } from './descriptions.type'
import type { IDescriptionsInject } from './type'
export const descriptionsKey: InjectionKey<IDescriptionsInject> =
Symbol('elDescriptions')

View File

@@ -0,0 +1,36 @@
import type { ColumnAlignment, ComponentSize } from '@element-plus/constants'
import type {
DescriptionItemProps,
DescriptionItemVNode,
} from './description-item'
export interface IDescriptionsInject {
props: {
border: boolean
column: number
direction: 'horizontal' | 'vertical'
size: ComponentSize
title: string
extra: string
labelWidth: string | number
}
addDescriptionItem: (item: DescriptionItemContext) => void
removeDescriptionItem: (uid: number) => void
}
export interface IDescriptionsItemInject {
label: string
span: number
rowspan: number
width: string | number
minWidth: string | number
labelWidth: string | number
align: ColumnAlignment
labelAlign?: ColumnAlignment
className: string
labelClassName: string
}
export interface DescriptionItemContext {
uid: number
props: DescriptionItemProps
vnode: DescriptionItemVNode
}

View File

@@ -160,9 +160,6 @@ export const flattedChildren = (
result.push(child, ...flattedChildren(child.component.subTree))
} else if (isVNode(child) && isArray(child.children)) {
result.push(...flattedChildren(child.children))
} else if (isVNode(child) && child.shapeFlag === 2) {
// @ts-ignore
result.push(...flattedChildren(child.type()))
} else {
result.push(child)
}