Skip to content
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

feat(form): The Readonly attribute of Form is added for the form read-only function #4176

Merged
merged 12 commits into from
May 24, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/checkbox/_example/group.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<t-checkbox key="a" value="选项一">选项一</t-checkbox>
<t-checkbox key="b" label="选项二" value="选项二" />
<t-checkbox key="c" label="选项三" value="选项三" :disabled="true" />
<t-checkbox key="c" label="选项四" value="选项四" :readonly="true" />
</t-checkbox-group>

<br />
Expand All @@ -37,6 +38,7 @@ const options2 = [
// eslint-disable-next-line @typescript-eslint/no-unused-vars
{ value: '选项二', label: (h) => <div>选项二</div> },
{ value: '选项三', label: '选项三' },
{ value: '设为只读', label: '设为只读', readonly: true },
];

const value1 = ref(['选项一']);
Expand Down
5 changes: 5 additions & 0 deletions src/checkbox/checkbox-group-props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ export default {
type: Boolean,
default: undefined,
},
/** 是否禁用组件,默认为 false。优先级:Form.readonly < CheckboxGroup.readonly < Checkbox.readonly */
readonly: {
type: Boolean,
default: undefined,
},
/** 是否启用懒加载。子组件 Checkbox 数据量大时建议开启;加载复杂内容或大量图片时建议开启 */
lazyLoad: Boolean,
/** 支持最多选中的数量 */
Expand Down
16 changes: 14 additions & 2 deletions src/checkbox/checkbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { CheckboxGroupInjectionKey } from './constants';
import useCheckboxLazyLoad from './hooks/useCheckboxLazyLoad';
import useKeyboardEvent from './hooks/useKeyboardEvent';
import { useDisabled } from '../hooks/useDisabled';
import { useReadonly } from '../hooks/useReadonly';

export default defineComponent({
name: 'TCheckbox',
Expand Down Expand Up @@ -84,6 +85,17 @@ export default defineComponent({
return checkboxGroupData?.value.disabled;
});
const isDisabled = useDisabled({ beforeDisabled, afterDisabled });
// Checkbox.readonly > CheckboxGroup.readonly > Form.readonly
const beforeReadonly = computed(() => {
if (!props.checkAll && !tChecked.value && checkboxGroupData?.value.maxExceeded) {
return true;
uyarn marked this conversation as resolved.
Show resolved Hide resolved
}
return null;
});
const afterReadonly = computed(() => {
return checkboxGroupData?.value.readonly;
});
const isReadonly = useReadonly({ beforeReadonly, afterReadonly });

const tIndeterminate = ref(false);
watch(
Expand Down Expand Up @@ -113,7 +125,7 @@ export default defineComponent({
);

const handleChange = (e: Event) => {
if (props.readonly) return;
if (isReadonly.value) return;
const checked = !tChecked.value;
setInnerChecked(checked, { e });
if (checkboxGroupData?.value.handleCheckboxChange) {
Expand Down Expand Up @@ -148,7 +160,7 @@ export default defineComponent({
tabindex="-1"
class={`${COMPONENT_NAME.value}__former`}
disabled={isDisabled.value}
readonly={props.readonly}
readonly={isReadonly.value}
indeterminate={tIndeterminate.value}
name={tName.value}
value={props.value ? props.value : undefined}
Expand Down
1 change: 1 addition & 0 deletions src/checkbox/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export interface CheckboxGroupInjectData {
isCheckAll: boolean;
maxExceeded: boolean;
disabled: boolean;
readonly: boolean;
indeterminate: boolean;
checkedValues: TdCheckboxGroupProps['value'];
handleCheckboxChange: (data: { checked: boolean; e: Event; option: TdCheckboxProps }) => void;
Expand Down
36 changes: 33 additions & 3 deletions src/checkbox/group.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,22 @@ export default defineComponent({
return n.length;
});

/**
* 计算是否所有选项都被选中。
* 此函数不接受参数,但依赖于外部的 `optionList` 和 `innerValue` 变量。
*
* @returns {boolean} 如果所有符合条件的选项都被选中,则返回 `true`;否则返回 `false`。
*/
const isCheckAll = computed<boolean>(() => {
const optionItems = optionList.value.filter((item) => !item.disabled && !item.checkAll).map((t) => t.value);
// 筛选出非禁用、非只读且不设置为“全选”的选项,并提取其值
const optionItems = optionList.value
.filter((item) => !item.disabled && !item.readonly && !item.checkAll)
.map((t) => t.value);

// 计算当前选中值与筛选后的选项值的交集
const intersectionValues = intersection(optionItems, innerValue.value);

// 判断交集的长度是否等于所有选项值的长度,以确定是否所有选项都被选中
return intersectionValues.length === optionItems.length;
});

Expand All @@ -52,16 +65,32 @@ export default defineComponent({
});
});

/**
* 获取所有复选框的值。
* 此函数遍历 `optionList` 中的项,忽略被标记为 `checkAll`、`disabled` 或 `readonly` 的项,
* 并收集非这些状态的项的值到一个 Set 集合中。如果达到最大限制 `maxExceeded`,则停止遍历。
*
* @returns {CheckboxGroupValue} 返回一个数组,包含所有非 `checkAll`、`disabled`、`readonly` 状态复选框的值。
*/
const getAllCheckboxValue = (): CheckboxGroupValue => {
const val = new Set<TdCheckboxProps['value']>();

// 遍历选项列表,忽略特定状态的项,并收集有效值
for (let i = 0, len = optionList.value.length; i < len; i++) {
const item = optionList.value[i];

// 如果项被标记为检查所有、禁用或只读,则跳过当前循环迭代
if (item.checkAll) continue;
if (item.disabled) continue;
val.add(item.value);
if (item.readonly) continue;

val.add(item.value); // 添加非排除状态项的值到集合中

// 如果已达到最大限制,则终止循环
if (maxExceeded.value) break;
}
return [...val];

return [...val]; // 从 Set 集合转换为数组并返回
};

const onCheckAllChange = (checked: boolean, context: { e: Event; source?: 't-checkbox' }) => {
Expand Down Expand Up @@ -128,6 +157,7 @@ export default defineComponent({
checkedValues: innerValue.value || [],
maxExceeded: maxExceeded.value,
disabled: props.disabled,
readonly: props.readonly,
indeterminate: indeterminate.value,
handleCheckboxChange,
onCheckedChange,
Expand Down
7 changes: 5 additions & 2 deletions src/checkbox/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ export default {
type: Boolean,
default: undefined,
},
/** 是否组件只读。如果父组件存在 CheckboxGroup,默认值由 CheckboxGroup.disabled 控制。优先级:Checkbox.readonly > CheckboxGroup.readonly > Form.readonly */
readonly: {
type: Boolean,
default: undefined,
},
/** 是否为半选 */
indeterminate: Boolean,
/** 主文案 */
Expand All @@ -43,8 +48,6 @@ export default {
type: String,
default: '',
},
/** 只读状态 */
readonly: Boolean,
/** 多选框的值 */
value: {
type: [String, Number, Boolean] as PropType<TdCheckboxProps['value']>,
Expand Down
6 changes: 6 additions & 0 deletions src/form/_usage/props.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@
"defaultValue": false,
"options": []
},
{
"name": "readonly",
"type": "Boolean",
"defaultValue": false,
"options": []
},
{
"name": "labelAlign",
"type": "enum",
Expand Down
1 change: 1 addition & 0 deletions src/form/form.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
colon | Boolean | false | 是否在表单标签字段右侧显示冒号 | N
data | Object | {} | 表单数据。TS 类型:`FormData` | N
disabled | Boolean | undefined | 是否禁用整个表单 | N
readonly | Boolean | undefined | 是否整个表单只读(* 实验功能,暂未开发完成) | N
uyarn marked this conversation as resolved.
Show resolved Hide resolved
errorMessage | Object | - | 表单错误信息配置,示例:`{ idcard: '请输入正确的身份证号码', max: '字符长度不能超过 ${max}' }`。TS 类型:`FormErrorMessage` | N
formControlledComponents | Array | - | 允许表单统一控制禁用状态的自定义组件名称列表。默认会有组件库的全部输入类组件:TInput、TInputNumber、TCascader、TSelect、TOption、TSwitch、TCheckbox、TCheckboxGroup、TRadio、TRadioGroup、TTreeSelect、TDatePicker、TTimePicker、TUpload、TTransfer、TSlider。对于自定义组件,组件内部需要包含可以控制表单禁用状态的变量 `formDisabled`。示例:`['CustomUpload', 'CustomInput']`。TS 类型:`Array<string>` | N
labelAlign | String | right | 表单字段标签对齐方式:左对齐、右对齐、顶部对齐。可选项:left/right/top | N
Expand Down
7 changes: 5 additions & 2 deletions src/form/form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import props from './props';
import { FormInjectionKey, FormItemContext, useCLASSNAMES } from './const';
import { FormResetEvent, FormSubmitEvent } from '../common';

import { FormDisabledProvider } from './hooks';
import { FormDisabledProvider, FormReadonlyProvider } from './hooks';
import { usePrefixClass, useTNodeJSX } from '../hooks';

type Result = FormValidateResult<TdFormProps['data']>;
Expand All @@ -30,10 +30,13 @@ export default defineComponent({

setup(props, { expose }) {
const renderContent = useTNodeJSX();
const { disabled } = toRefs(props);
const { disabled, readonly } = toRefs(props);
provide<FormDisabledProvider>('formDisabled', {
disabled,
});
provide<FormReadonlyProvider>('formReadonly', {
readonly,
});

const formRef = ref<HTMLFormElement>(null);
const children = ref<FormItemContext[]>([]);
Expand Down
25 changes: 25 additions & 0 deletions src/form/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ export interface FormDisabledProvider {
disabled: Ref<TdFormProps['disabled']>;
}

export interface FormReadonlyProvider {
readonly: Ref<TdFormProps['readonly']>;
}

/**
* 用于实现 form 的全局禁用状态hook
* @returns
Expand All @@ -15,3 +19,24 @@ export function useFormDisabled(extend?: Ref<boolean>) {
const { disabled } = inject<FormDisabledProvider>('formDisabled', Object.create(null));
return computed(() => propsDisabled.value || disabled?.value || extend?.value || false);
}

/**
* 创建一个计算属性,用于判断表单是否应为只读状态。
* 此函数考虑了多个来源来决定表单的只读状态:
* 1. 组件的 `readonly` 属性;
* 2. 通过 `formReadonly` 命名空间注入的只读状态;
* 3. 可选的 `extend` 参数,用于进一步扩展只读状态的判断逻辑。
*
* @param extend - 一个可选的 Ref<boolean>,用于扩展判断表单是否只读的逻辑。如果提供,它的值将被考虑在内。
* @returns 返回一个计算属性,该属性根据上述条件决定其值,最终确定表单是否应处于只读状态。
*/
export function useFormReadonly(extend?: Ref<boolean>) {
// 获取当前实例
const ctx = getCurrentInstance();
// 计算属性,用于获取组件的 `readonly` 属性值
const propsReadonly = computed(() => ctx.props.readonly as boolean);
// 从 `formReadonly` 命名空间注入的只读状态
const { readonly } = inject<FormReadonlyProvider>('formReadonly', Object.create(null));
// 计算最终的只读状态,优先级从高到低为:组件的 `readonly` 属性、注入的 `readonly` 状态、`extend` 参数的值,最后是默认的 `false`
return computed(() => propsReadonly.value || readonly?.value || extend?.value || false);
}
5 changes: 5 additions & 0 deletions src/form/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ export default {
type: Boolean,
default: undefined,
},
/** 是否整个表单只读 */
readonly: {
type: Boolean,
default: undefined,
},
/** 表单错误信息配置,示例:`{ idcard: '请输入正确的身份证号码', max: '字符长度不能超过 ${max}' }` */
errorMessage: {
type: Object as PropType<TdFormProps['errorMessage']>,
Expand Down
4 changes: 4 additions & 0 deletions src/form/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ export interface TdFormProps<FormData extends Data = Data> {
* 是否禁用整个表单
*/
disabled?: boolean;
/**
* 是否整个表单只读
*/
readonly?: boolean;
/**
* 表单错误信息配置,示例:`{ idcard: '请输入正确的身份证号码', max: '字符长度不能超过 ${max}' }`
*/
Expand Down
36 changes: 36 additions & 0 deletions src/hooks/useReadonly.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Ref, inject, computed, getCurrentInstance } from 'vue';
import isBoolean from 'lodash/isBoolean';
import { TdFormProps } from '../form/type';

export interface FormReadonlyProvider {
readonly: Ref<TdFormProps['readonly']>;
}

export interface ReadonlyContext {
beforeReadonly?: Ref<boolean>;
afterReadonly?: Ref<boolean>;
}

/**
* 用于实现组件全局只读状态的hook
* 优先级:(beforeReadonly) > Component.readonly > ComponentGroup.readonly(afterReadonly) > Form.readonly
* @returns
*/
export function useReadonly(context?: ReadonlyContext) {
const currentInstance = getCurrentInstance();
const componentReadonly = computed(() => currentInstance.props.readonly as boolean);

const formReadonly = inject<FormReadonlyProvider>('formReadonly', Object.create(null));

return computed(() => {
if (isBoolean(context?.beforeReadonly.value)) return context.beforeReadonly.value;
uyarn marked this conversation as resolved.
Show resolved Hide resolved
// Component
if (isBoolean(componentReadonly.value)) return componentReadonly.value;
// ComponentGroup
if (isBoolean(context?.afterReadonly.value)) return context.afterReadonly.value;
// Form
if (isBoolean(formReadonly.readonly?.value)) return formReadonly.readonly.value;

return false;
});
}
7 changes: 4 additions & 3 deletions src/input/input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
CloseCircleFilledIcon as TdCloseCircleFilledIcon,
} from 'tdesign-icons-vue-next';
import props from './props';
import { useFormDisabled } from '../form/hooks';
import { useFormDisabled, useFormReadonly } from '../form/hooks';
import { useConfig, usePrefixClass, useCommonClassName } from '../hooks/useConfig';
import { useGlobalIcon } from '../hooks/useGlobalIcon';
import { useTNodeJSX } from '../hooks/tnode';
Expand Down Expand Up @@ -53,6 +53,7 @@ export default defineComponent({
CloseCircleFilledIcon: TdCloseCircleFilledIcon,
});
const disabled = useFormDisabled();
const readonly = useFormReadonly();
const COMPONENT_NAME = usePrefixClass('input');
const INPUT_WRAP_CLASS = usePrefixClass('input__wrap');
const INPUT_TIPS_CLASS = usePrefixClass('input__tips');
Expand Down Expand Up @@ -83,13 +84,13 @@ export default defineComponent({
getValidAttrs({
autofocus: props.autofocus,
disabled: disabled.value,
readonly: props.readonly,
readonly: readonly.value,
placeholder: tPlaceholder.value,
maxlength: (!props.allowInputOverMax && props.maxlength) || undefined,
name: props.name || undefined,
type: renderType.value,
autocomplete: props.autocomplete ?? (globalConfig.value.autocomplete || undefined),
unselectable: props.readonly ? 'on' : undefined,
unselectable: readonly.value ? 'on' : undefined,
}),
);

Expand Down
44 changes: 44 additions & 0 deletions test/unit/snap/__snapshots__/csr.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -31589,6 +31589,28 @@ exports[`csr snapshot test > csr test ./src/checkbox/_example/group.vue 1`] = `
</span>

</label>
<label
class="t-checkbox"
tabindex="0"
>

<input
class="t-checkbox__former"
readonly=""
tabindex="-1"
type="checkbox"
value="选项四"
/>
<span
class="t-checkbox__input"
/>
<span
class="t-checkbox__label"
>
选项四
</span>

</label>

</div>
</div>
Expand Down Expand Up @@ -31708,6 +31730,28 @@ exports[`csr snapshot test > csr test ./src/checkbox/_example/group.vue 1`] = `
</span>

</label>
<label
class="t-checkbox"
tabindex="0"
>

<input
class="t-checkbox__former"
readonly=""
tabindex="-1"
type="checkbox"
value="设为只读"
/>
<span
class="t-checkbox__input"
/>
<span
class="t-checkbox__label"
>
设为只读
</span>

</label>

</div>
</div>
Expand Down