refactor(components): refactor space (#3757)

This commit is contained in:
三咲智子 2021-10-06 19:34:32 +08:00 committed by GitHub
parent 23a2ac5515
commit 3b9e63d587
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 320 additions and 316 deletions

View File

@ -1,6 +1,6 @@
import { nextTick, h } from 'vue'
import { mount } from '@vue/test-utils'
import Space from '../src/index'
import Space from '../src/space'
const AXIOM = 'Rem is the best girl'

View File

@ -1,13 +1,9 @@
import Space from './src/index'
import { withInstall } from '@element-plus/utils/with-install'
import type { App } from 'vue'
import type { SFCWithInstall } from '@element-plus/utils/types'
import Space from './src/space'
const _Space = Space as SFCWithInstall<typeof Space>
export const ElSpace = withInstall(Space)
export default ElSpace
_Space.install = (app: App) => {
app.component(_Space.name, _Space)
}
export default _Space
export const ElSpace = _Space
export * from './src/space'
export * from './src/use-space'

View File

@ -1,143 +0,0 @@
import {
defineComponent,
renderSlot,
createVNode,
createTextVNode,
isVNode,
} from 'vue'
import {
PatchFlags,
isFragment,
isValidElementNode,
} from '@element-plus/utils/vnode'
import { isArray } from '@element-plus/utils/util'
import Item from './item.vue'
import { useSpace, defaultProps } from './useSpace'
import type { VNode, ExtractPropTypes, Slots } from 'vue'
export default defineComponent({
name: 'ElSpace',
props: defaultProps,
setup(props) {
return useSpace(props)
},
render(
ctx: ReturnType<typeof useSpace> &
ExtractPropTypes<typeof defaultProps> & { $slots: Slots }
) {
const {
classes,
$slots,
containerStyle,
itemStyle,
spacer,
prefixCls,
direction,
} = ctx
const children = renderSlot($slots, 'default', { key: 0 }, () => [])
// retrieve the children out via a simple for loop
// the edge case here is that when users uses directives like <v-for>, <v-if>
// we need to go one layer deeper
if (children.children.length === 0) return null
// loop the children, if current children is rendered via `renderList` or `<v-for>`
if (isArray(children.children)) {
let extractedChildren = []
children.children.forEach((child: VNode, loopKey) => {
if (isFragment(child)) {
if (isArray(child.children)) {
child.children.forEach((nested, key) => {
extractedChildren.push(
createVNode(
Item,
{
style: itemStyle,
prefixCls,
key: `nested-${key}`,
},
{
default: () => [nested as VNode],
},
PatchFlags.PROPS | PatchFlags.STYLE,
['style', 'prefixCls']
)
)
})
}
// if the current child is valid vnode, then append this current vnode
// to item as child node.
} else if (isValidElementNode(child)) {
extractedChildren.push(
createVNode(
Item,
{
style: itemStyle,
prefixCls,
key: `LoopKey${loopKey}`,
},
{
default: () => [child as VNode],
},
PatchFlags.PROPS | PatchFlags.STYLE,
['style', 'prefixCls']
)
)
}
})
if (spacer) {
// track the current rendering index, when encounters the last element
// then no need to add a spacer after it.
const len = extractedChildren.length - 1
extractedChildren = extractedChildren.reduce((acc, child, idx) => {
return idx === len
? [...acc, child]
: [
...acc,
child,
createVNode(
'span',
// adding width 100% for vertical alignment,
// when the spacer inherit the width from the
// parent, this span's width was not set, so space
// might disappear
{
style: [
itemStyle,
direction === 'vertical' ? 'width: 100%' : null,
],
key: idx,
},
[
// if spacer is already a valid vnode, then append it to the current
// span element.
// otherwise, treat it as string.
isVNode(spacer)
? spacer
: createTextVNode(spacer as string, PatchFlags.TEXT),
],
PatchFlags.STYLE
),
]
}, [])
}
// spacer container.
return createVNode(
'div',
{
class: classes,
style: containerStyle,
},
extractedChildren,
PatchFlags.STYLE | PatchFlags.CLASS
)
}
return children.children
},
})

View File

@ -1,22 +1,28 @@
<template>
<div :class="classes">
<slot></slot>
<slot />
</div>
</template>
<script lang="ts">
import { defineComponent, computed } from 'vue'
import { buildProps } from '@element-plus/utils/props'
export default defineComponent({
props: {
const spaceItem = buildProps({
prefixCls: {
type: String,
default: 'el-space',
},
},
} as const)
export default defineComponent({
props: spaceItem,
setup(props) {
const classes = computed(() => [`${props.prefixCls}__item`])
return {
classes: computed(() => `${props.prefixCls}__item`),
classes,
}
},
})

View File

@ -0,0 +1,208 @@
import {
defineComponent,
renderSlot,
createVNode,
createTextVNode,
isVNode,
} from 'vue'
import { isString } from '@vue/shared'
import {
PatchFlags,
isFragment,
isValidElementNode,
} from '@element-plus/utils/vnode'
import { isArray, isNumber } from '@element-plus/utils/util'
import {
buildProps,
componentSize,
definePropType,
} from '@element-plus/utils/props'
import Item from './item.vue'
import { useSpace } from './use-space'
import type { VNode, StyleValue, ExtractPropTypes, VNodeChild } from 'vue'
import type { AlignItemsProperty } from 'csstype'
export const spaceProps = buildProps({
direction: {
type: String,
values: ['horizontal', 'vertical'],
default: 'horizontal',
},
class: {
type: definePropType<string | string[] | Record<string, boolean>>([
String,
Object,
Array,
]),
default: '',
},
style: {
type: definePropType<StyleValue>([String, Array, Object]),
default: '',
},
alignment: {
type: definePropType<AlignItemsProperty>(String),
default: 'center',
},
prefixCls: {
type: String,
},
spacer: {
type: definePropType<VNodeChild>([Object, String, Number, Array]),
default: null,
validator: (val: unknown) => isVNode(val) || isNumber(val) || isString(val),
},
wrap: {
type: Boolean,
default: false,
},
fill: {
type: Boolean,
default: false,
},
fillRatio: {
type: Number,
default: 100,
},
size: {
type: [String, Array, Number],
values: componentSize,
validator: (val: unknown): val is [number, number] | number => {
return (
isNumber(val) ||
(isArray(val) && val.length === 2 && val.every((i) => isNumber(i)))
)
},
},
} as const)
export type SpaceProps = ExtractPropTypes<typeof spaceProps>
export default defineComponent({
name: 'ElSpace',
props: spaceProps,
setup(props, { slots }) {
const { classes, containerStyle, itemStyle } = useSpace(props)
return () => {
const { spacer, prefixCls, direction } = props
const children = renderSlot(slots, 'default', { key: 0 }, () => [])
// retrieve the children out via a simple for loop
// the edge case here is that when users uses directives like <v-for>, <v-if>
// we need to go one layer deeper
if ((children.children ?? []).length === 0) return null
// loop the children, if current children is rendered via `renderList` or `<v-for>`
if (isArray(children.children)) {
let extractedChildren: VNode[] = []
children.children.forEach((child, loopKey) => {
if (isFragment(child)) {
if (isArray(child.children)) {
child.children.forEach((nested, key) => {
extractedChildren.push(
createVNode(
Item,
{
style: itemStyle.value,
prefixCls,
key: `nested-${key}`,
},
{
default: () => [nested],
},
PatchFlags.PROPS | PatchFlags.STYLE,
['style', 'prefixCls']
)
)
})
}
// if the current child is valid vnode, then append this current vnode
// to item as child node.
} else if (isValidElementNode(child)) {
extractedChildren.push(
createVNode(
Item,
{
style: itemStyle.value,
prefixCls,
key: `LoopKey${loopKey}`,
},
{
default: () => [child],
},
PatchFlags.PROPS | PatchFlags.STYLE,
['style', 'prefixCls']
)
)
}
})
if (spacer) {
// track the current rendering index, when encounters the last element
// then no need to add a spacer after it.
const len = extractedChildren.length - 1
extractedChildren = extractedChildren.reduce<VNode[]>(
(acc, child, idx) => {
const children = [...acc, child]
if (idx !== len) {
children.push(
createVNode(
'span',
// adding width 100% for vertical alignment,
// when the spacer inherit the width from the
// parent, this span's width was not set, so space
// might disappear
{
style: [
itemStyle.value,
direction === 'vertical' ? 'width: 100%' : null,
],
key: idx,
},
[
// if spacer is already a valid vnode, then append it to the current
// span element.
// otherwise, treat it as string.
isVNode(spacer)
? spacer
: createTextVNode(spacer as string, PatchFlags.TEXT),
],
PatchFlags.STYLE
)
)
}
return children
},
[]
)
}
// spacer container.
return createVNode(
'div',
{
class: classes.value,
style: containerStyle.value,
},
extractedChildren,
PatchFlags.STYLE | PatchFlags.CLASS
)
}
return children.children
}
},
})

View File

@ -0,0 +1,84 @@
import { ref, computed, watchEffect } from 'vue'
import { isNumber } from '@element-plus/utils/util'
import type { SpaceProps } from './space'
import type { CSSProperties, StyleValue } from 'vue'
import type { ComponentSize } from '@element-plus/utils/types'
const SIZE_MAP: Record<ComponentSize, number> = {
mini: 4,
small: 8,
medium: 12,
large: 16,
}
export function useSpace(props: SpaceProps) {
const classes = computed(() => [
'el-space',
`el-space--${props.direction}`,
props.class,
])
const horizontalSize = ref(0)
const verticalSize = ref(0)
const containerStyle = computed<StyleValue>(() => {
const wrapKls: CSSProperties =
props.wrap || props.fill
? { flexWrap: 'wrap', marginBottom: `-${verticalSize.value}px` }
: {}
const alignment: CSSProperties = {
alignItems: props.alignment,
}
return [wrapKls, alignment, props.style]
})
const itemStyle = computed<StyleValue>(() => {
const itemBaseStyle: CSSProperties = {
paddingBottom: `${verticalSize.value}px`,
marginRight: `${horizontalSize.value}px`,
}
const fillStyle: CSSProperties = props.fill
? { flexGrow: 1, minWidth: `${props.fillRatio}%` }
: {}
return [itemBaseStyle, fillStyle]
})
watchEffect(() => {
const { size = 'small', wrap, direction: dir, fill } = props
// when the specified size have been given
if (Array.isArray(size)) {
const [h = 0, v = 0] = size
horizontalSize.value = h
verticalSize.value = v
} else {
let val: number
if (isNumber(size)) {
val = size
} else {
val = SIZE_MAP[size] || SIZE_MAP.small
}
if ((wrap || fill) && dir === 'horizontal') {
horizontalSize.value = verticalSize.value = val
} else {
if (dir === 'horizontal') {
horizontalSize.value = val
verticalSize.value = 0
} else {
verticalSize.value = val
horizontalSize.value = 0
}
}
}
})
return {
classes,
containerStyle,
itemStyle,
}
}

View File

@ -1,147 +0,0 @@
import { ref, computed, watch, isVNode } from 'vue'
import { isValidComponentSize } from '@element-plus/utils/validators'
import { isNumber, isArray, isString } from '@element-plus/utils/util'
import type { PropType, ExtractPropTypes, CSSProperties, VNodeChild } from 'vue'
import type { ComponentSize } from '@element-plus/utils/types'
const SizeMap: Record<ComponentSize, number> = {
mini: 4,
small: 8,
medium: 12,
large: 16,
}
export const defaultProps = {
direction: {
type: String as PropType<'horizontal' | 'vertical'>,
default: 'horizontal',
},
class: {
type: [String, Object, Array],
default: '',
},
style: {
type: [String, Array, Object] as PropType<
string | Array<any> | CSSProperties
>,
},
alignment: {
type: String as PropType<''>,
default: 'center',
},
prefixCls: {
type: String,
},
spacer: {
type: [Object, String, Number] as PropType<VNodeChild>,
default: null,
validator: (val: unknown) => {
return isVNode(val) || isNumber(val) || isString(val)
},
},
wrap: {
type: Boolean,
default: false,
},
fill: {
type: Boolean,
default: false,
},
fillRatio: {
type: Number,
default: 100,
},
size: {
type: [String, Array, Number] as PropType<
ComponentSize | [number, number] | number
>,
validator: (val: unknown) => {
return (
isValidComponentSize(val as string) || isNumber(val) || isArray(val)
)
},
},
}
export function useSpace(props: ExtractPropTypes<typeof defaultProps>) {
const classes = computed(() => [
'el-space',
`el-space--${props.direction}`,
props.class,
])
const horizontalSize = ref(0)
const verticalSize = ref(0)
watch(
() => [props.size, props.wrap, props.direction, props.fill],
([size = 'small', wrap, dir, fill]) => {
// when the specified size have been given
if (isArray(size)) {
const [h = 0, v = 0] = size
horizontalSize.value = h
verticalSize.value = v
} else {
let val: number
if (isNumber(size)) {
val = size as number
} else {
val = SizeMap[size as string] || SizeMap.small
}
if ((wrap || fill) && dir === 'horizontal') {
horizontalSize.value = verticalSize.value = val
} else {
if (dir === 'horizontal') {
horizontalSize.value = val
verticalSize.value = 0
} else {
verticalSize.value = val
horizontalSize.value = 0
}
}
}
},
{ immediate: true }
)
const containerStyle = computed(() => {
const wrapKls: CSSProperties =
props.wrap || props.fill
? { flexWrap: 'wrap', marginBottom: `-${verticalSize.value}px` }
: null
const alignment: CSSProperties = {
alignItems: props.alignment,
}
return [wrapKls, alignment, props.style] as Array<CSSProperties>
})
const itemStyle = computed(() => {
const itemBaseStyle = {
paddingBottom: `${verticalSize.value}px`,
marginRight: `${horizontalSize.value}px`,
}
const fillStyle = props.fill
? { flexGrow: 1, minWidth: `${props.fillRatio}%` }
: null
return [itemBaseStyle, fillStyle] as Array<CSSProperties>
})
return {
classes,
containerStyle,
itemStyle,
}
}

View File

@ -32,7 +32,7 @@ type BuildPropOption<T, D, R, V, C> = {
type IfUnknown<T, V> = [unknown] extends T ? V : T
type BuildPropType<T, V, C> =
export type BuildPropType<T, V, C> =
| (T extends PropWrapper<unknown>
? T[typeof wrapperKey]
: [V] extends [never]

View File

@ -144,8 +144,8 @@ export {
extend,
}
export const isBool = (val: unknown) => typeof val === 'boolean'
export const isNumber = (val: unknown) => typeof val === 'number'
export const isBool = (val: unknown): val is boolean => typeof val === 'boolean'
export const isNumber = (val: unknown): val is number => typeof val === 'number'
export const isHTMLElement = (val: unknown) => toRawType(val).startsWith('HTML')
export function rafThrottle<T extends AnyFunction<any>>(

View File

@ -35,8 +35,8 @@ export enum PatchFlags {
BAIL = -2,
}
export const isFragment = (node: VNodeChild) =>
(node as VNode).type === Fragment
export const isFragment = (node: unknown): node is VNode =>
isVNode(node) && node.type === Fragment
export const isText = (node: VNodeChild) => (node as VNode).type === Text
@ -64,8 +64,8 @@ function getChildren(node: VNode, depth: number): undefined | VNode {
* determine if the element is a valid element type rather than fragments and comment e.g. <template> v-if
* @param node {VNode} node to be tested
*/
export const isValidElementNode = (node: VNodeChild) =>
!(isFragment(node) || isComment(node))
export const isValidElementNode = (node: unknown): node is VNode =>
isVNode(node) && !isFragment(node) && !isComment(node)
export const getFirstValidNode = (
nodes: VNodeChild,