Skip to content

Commit

Permalink
fix(pro:tag-select): tag data edit and create should be validated (#1931
Browse files Browse the repository at this point in the history
)

add `tagLabelValidator` prop
  • Loading branch information
sallerli1 authored May 20, 2024
1 parent 2d69a7c commit fe649cc
Show file tree
Hide file tree
Showing 10 changed files with 187 additions and 31 deletions.
14 changes: 14 additions & 0 deletions packages/pro/tag-select/demo/Validate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
order: 3
title:
zh: 校验标签label输入
en: Validate tag label input
---

## zh

使用 `tagLabelValidator` 校验标签的输入,可以用于校验创建的标签输入以及标签修改的内容是否合法,并显示错误提示。

## en

Validate tag label input via `tagLabelValidator`, in doing so, created tag input and tag label edit input is validated and error meassages will be disaplayed.
79 changes: 79 additions & 0 deletions packages/pro/tag-select/demo/Validate.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<template>
<IxProTagSelect
v-model:value="selectedValue"
placeholder="请选择标签"
:dataSource="tagSelectData"
:tagLabelValidator="tagLabelValidator"
:onTagDataRemove="handleTagDataRemove"
:onTagDataAdd="handleTagDataAdd"
:onTagDataChange="handleTagDataChange"
style="width: 336px"
>
<template #removeConfirmTitle="{ label }"> 确定删除“{{ label }}”标签吗? </template>
<template #removeConfirmContent> 操作说明 </template>
</IxProTagSelect>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { TagSelectData } from '@idux/pro/tag-select'
const selectedValue = ref<string[]>([])
const tagSelectData = ref<TagSelectData[]>([
{
key: 'emphasis',
label: '重点关注',
color: 'yellow',
},
{
key: 'alarm',
label: '告警标签',
color: 'blue',
},
{
key: 'track',
label: '持续追踪',
color: 'blue',
},
{
key: 'care-and-track',
label: '持续关注并追踪',
color: 'green',
},
{
key: 'keeps-alarm',
label: '持续告警',
color: 'red',
},
])
const tagLabelValidator = (input: string) => {
if (!input) {
return '不能为空'
}
if (/[%+-.]/.test(input)) {
return '不能输入特殊字符'
}
return
}
const handleTagDataRemove = (data: TagSelectData) => {
tagSelectData.value = tagSelectData.value.filter(d => d.key !== data.key)
}
const handleTagDataAdd = (data: TagSelectData) => {
tagSelectData.value.push(data)
}
const handleTagDataChange = (data: TagSelectData) => {
const index = tagSelectData.value.findIndex(d => d.key === data.key)
if (index < 0) {
return
}
tagSelectData.value.splice(index, 1, data)
}
</script>

<style scoped lang="less"></style>
2 changes: 2 additions & 0 deletions packages/pro/tag-select/docs/Api.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
| `size` | 设置选择器大小 | `'sm' \| 'md' \| 'lg'` | `md` | - | - |
| `status` | 手动指定校验状态 | `valid \| invalid \| validating` | - | - | - |
| `suffix` | 设置后缀图标 | `string \| #suffix` | `down` | - | - |
| `tagLabelValidator` | 标签创建或编辑的输入校验 | `(input: string) => string \| undefined` | - | 返回非空 `string` 为校验不合法,返回为提示信息 |
| `onClear` | 清除图标被点击后的回调 | `(evt: MouseEvent) => void` | - | - | - |
| `onChange` | 选中值发生改变后的回调 | `(value: (string \| number \| symbol)[] \| undefined, oldValue: (string \| number \| symbol)[] \| undefined) => void` | - | - | - |
| `onFocus` | 获取焦点后的回调 | `(evt: FocusEvent) => void` | - | - | - |
Expand Down Expand Up @@ -66,6 +67,7 @@ interface TagSelectData {

| 名称 | 说明 | 参数类型 | 备注 |
| --- | --- | --- | --- |
| `alert` | 自定义告警提示 | `{ input: string, inputValidateError: string \| undefined }` | - |
| `clearIcon` | 自定义清除图标 | - | - |
| `suffix` | 自定义选择框的后缀 | - | - |
| `selectedLabel` | 自定义选中标签的文本内容 | `TagSelectData` | - |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,13 @@ export function usePanelActiveState(
): PanelActiveStateContext {
const [activeValue, setActiveValue] = useState<VKey | undefined>(undefined)

const { inputValue, inputFullyMatched, filteredData, dataMaxExceeded } = tagDataContext
const { tagCreateEnabled, filteredData } = tagDataContext
const { selectedValue } = selectedStateContext

const mergedOptions = computed<PanelOption[]>(() => {
const options: PanelOption[] = [...filteredData.value]

if (inputValue.value && !inputFullyMatched.value && !dataMaxExceeded.value) {
if (tagCreateEnabled.value) {
options.push({ key: creationDataKey })
}

Expand Down
20 changes: 17 additions & 3 deletions packages/pro/tag-select/src/composables/useTagData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ export interface TagDataContext {
filteredData: ComputedRef<MergedTagData[]>
dataMaxExceeded: ComputedRef<boolean>
inputValue: ComputedRef<string | undefined>
inputFullyMatched: ComputedRef<boolean>
inputValidateError: ComputedRef<string | undefined>
tagCreateEnabled: ComputedRef<boolean>
setInputValue: (input: string | undefined) => void

getTagDataByKey: (key: VKey) => MergedTagData | undefined
Expand Down Expand Up @@ -69,7 +70,19 @@ export function useTagData(props: ProTagSelectProps, tagColorContext: TagColorCo
})

const inputFullyMatched = computed(
() => inputValue.value && filteredData.value.findIndex(data => data.label === inputValue.value) > -1,
() => !!inputValue.value && filteredData.value.findIndex(data => data.label === inputValue.value) > -1,
)

const inputValidateError = computed(() => {
if (inputValue.value && !inputFullyMatched.value && !dataMaxExceeded.value) {
return props.tagLabelValidator?.(inputValue.value)
}

return undefined
})

const tagCreateEnabled = computed(
() => !!inputValue.value && !inputFullyMatched.value && !dataMaxExceeded.value && !inputValidateError.value,
)

const getTagDataByKey = (key: VKey) => {
Expand Down Expand Up @@ -108,7 +121,8 @@ export function useTagData(props: ProTagSelectProps, tagColorContext: TagColorCo
filteredData,
dataMaxExceeded,
inputValue,
inputFullyMatched,
tagCreateEnabled,
inputValidateError,
setInputValue,

getTagDataByKey,
Expand Down
44 changes: 36 additions & 8 deletions packages/pro/tag-select/src/content/TagDataEditPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import type { TagSelectColor } from '../types'

import { type PropType, defineComponent, inject, onMounted, ref, watch } from 'vue'

import { useState } from '@idux/cdk/utils'
import { IxFormItem } from '@idux/components/form'
import { IxIcon } from '@idux/components/icon'
import { type InputInstance, IxInput } from '@idux/components/input'
import { useThemeToken } from '@idux/pro/theme'
Expand All @@ -19,9 +21,11 @@ import { proTagSelectContext } from '../token'
export default defineComponent({
props: {
data: { type: Object as PropType<MergedTagData>, required: true },
visible: Boolean,
},
setup(props) {
const {
props: proTagSelectProps,
locale,
mergedPrefixCls,
mergedTagSelectColors,
Expand All @@ -37,9 +41,26 @@ export default defineComponent({
const { globalHashId, hashId } = useThemeToken('proTagSelect')

const inputRef = ref<InputInstance>()
const [inputValue, setInputValue] = useState('')
const [inputValidateError, setInputValidateError] = useState<string | undefined>(undefined)
watch(
[() => props.visible, () => props.data.label],
() => {
setInputValue(props.data.label)
},
{
immediate: true,
},
)

const handleInputChange = (input: string) => {
handleTagDataLabelInput(input, props.data)
setInputValue(input)
const validateError = proTagSelectProps.tagLabelValidator?.(input)
setInputValidateError(validateError)

if (!validateError) {
handleTagDataLabelInput(input, props.data)
}
}

const handleDeleteClick = () => {
Expand Down Expand Up @@ -99,13 +120,20 @@ export default defineComponent({
return (
<div class={[prefixCls, globalHashId.value, hashId.value]} onMousedown={handlePanelMousedown}>
<div class={`${prefixCls}-input`}>
<IxInput
ref={inputRef}
class={`${prefixCls}-input-inner`}
value={props.data.label}
size="sm"
onChange={handleInputChange}
/>
<IxFormItem
messageTooltip
message={inputValidateError.value}
status={inputValidateError.value ? 'invalid' : 'valid'}
>
<IxInput
ref={inputRef}
class={`${prefixCls}-input-inner`}
value={inputValue.value}
size="sm"
status={inputValidateError.value ? 'invalid' : 'valid'}
onChange={handleInputChange}
/>
</IxFormItem>
</div>
<div class={`${prefixCls}-delete`} onClick={handleDeleteClick}>
<IxIcon class={`${prefixCls}-delete-icon`} name="delete" />
Expand Down
6 changes: 4 additions & 2 deletions packages/pro/tag-select/src/content/TagSelectOption.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ export default defineComponent({
changeActiveValue,
} = inject(proTagSelectContext)!

const editPanelVisible = computed(() => editPanelOpened.value && dataToEdit.value?.key === props.data.key)

const classes = computed(() => {
const prefixCls = `${mergedPrefixCls.value}-option`

Expand Down Expand Up @@ -75,7 +77,7 @@ export default defineComponent({
}

const renderOverlayContent = () => {
return <TagDataEditPanel data={props.data} />
return <TagDataEditPanel data={props.data} visible={editPanelVisible.value} />
}

return () => {
Expand All @@ -93,7 +95,7 @@ export default defineComponent({
<IxControlTriggerOverlay
placement="bottomEnd"
trigger="manual"
visible={editPanelOpened.value && dataToEdit.value?.key === props.data.key}
visible={editPanelVisible.value}
v-slots={{ content: renderOverlayContent }}
>
<div
Expand Down
42 changes: 30 additions & 12 deletions packages/pro/tag-select/src/content/TagSelectPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,13 @@ export default defineComponent({
mergedPrefixCls,
filteredData,
dataMaxExceeded,
tagCreateEnabled,
inputValue,
inputFullyMatched,
inputValidateError,
setEditPanelOpened,
} = inject(proTagSelectContext)!

const showEmpty = computed(() => !filteredData.value.length && (!inputValue.value || dataMaxExceeded.value))
const showCreatOption = computed(() => inputValue.value && !inputFullyMatched.value && !dataMaxExceeded.value)

const handleMousedown = (evt: MouseEvent) => {
setEditPanelOpened(false)
Expand All @@ -41,25 +41,43 @@ export default defineComponent({
}
}

const renderAlert = () => {
if (slots.alert) {
return slots.alert({ input: inputValue.value, inputValidateError: inputValidateError.value })
}

if (dataMaxExceeded.value) {
return (
slots.maxExceededAlert?.() ?? (
<IxAlert icon="info-circle" type="offline">
{locale.maxExceededAlert.replace('${0}', toString(props.tagDataLimit ?? 0))}
</IxAlert>
)
)
}

if (inputValidateError.value) {
return (
<IxAlert icon="info-circle" type="offline">
{inputValidateError.value}
</IxAlert>
)
}

return
}

return () => {
const prefixCls = `${mergedPrefixCls.value}-panel`

return (
<div class={[prefixCls, `${common.prefixCls}-scroll-min`]} onMousedown={handleMousedown}>
{dataMaxExceeded.value && (
<div class={`${prefixCls}-alert`}>
{slots.maxExceededAlert?.() ?? (
<IxAlert icon="info-circle">
{locale.maxExceededAlert.replace('${0}', toString(props.tagDataLimit ?? 0))}
</IxAlert>
)}
</div>
)}
{renderAlert()}
{filteredData.value.map(data => (
<TagSelectOption data={data} v-slots={slots} />
))}
{showEmpty.value && <div class={`${prefixCls}-empty`}>{locale.empty}</div>}
{showCreatOption.value && <TagSelectCreationOption />}
{tagCreateEnabled.value && <TagSelectCreationOption />}
</div>
)
}
Expand Down
5 changes: 1 addition & 4 deletions packages/pro/tag-select/src/token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import type { OperationsContext } from './composables/useOperations'
import type { OverlayStateContext } from './composables/useOverlayState'
import type { PanelActiveStateContext } from './composables/usePanelActiveState'
import type { MergedTagData, TagDataContext } from './composables/useTagData'
import type { TagDataContext } from './composables/useTagData'
import type { TagEditContext } from './composables/useTagEdit'
import type { ProTagSelectProps, TagSelectColor } from './types'
import type { VKey } from '@idux/cdk/utils'
Expand All @@ -28,10 +28,7 @@ export interface ProTagSelectContext
focus: (options?: FocusOptions) => void
mergedTagSelectColors: ComputedRef<TagSelectColor[]>
selectedValue: ComputedRef<VKey[] | undefined>
mergedData: ComputedRef<MergedTagData[]>
maxExceeded: ComputedRef<boolean>
inputValue: ComputedRef<string | undefined>
inputFullyMatched: ComputedRef<boolean>
mergedPrefixCls: ComputedRef<string>
locale: ProTagSelectLocale
}
Expand Down
2 changes: 2 additions & 0 deletions packages/pro/tag-select/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export const proTagSelectProps = {
maxTags: { type: [Number, String] as PropType<number | 'responsive'>, default: Number.MAX_SAFE_INTEGER },
tagsLimit: { type: Number, default: Number.MAX_SAFE_INTEGER },
tagDataLimit: { type: Number, default: Number.MAX_SAFE_INTEGER },
tagLabelValidator: Function as PropType<(label: string) => string | undefined>,
overlayClassName: { type: String, default: undefined },
overlayContainer: {
type: [String, HTMLElement, Function] as PropType<OverlayContainerType>,
Expand Down Expand Up @@ -85,6 +86,7 @@ export const proTagSelectProps = {
} as const

export interface ProTagSelectSlots {
alert: Slot
clearIcon: Slot
suffix: Slot
selectedLabel: Slot<TagSelectData>
Expand Down

0 comments on commit fe649cc

Please sign in to comment.