From a81838c8a6ae06c46aad90e128585277d8325507 Mon Sep 17 00:00:00 2001 From: yunxia Date: Tue, 4 Jul 2023 20:16:10 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20form=E8=A1=A8=E5=8D=95=E5=BC=80?= =?UTF-8?q?=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/quarkd/src/form/demo.vue | 352 ++++++++++++++----------- packages/quarkd/src/form/doc.zh-CN.md | 347 +++++++++++++++--------- packages/quarkd/src/form/form-item.css | 42 +-- packages/quarkd/src/form/form-item.tsx | 57 ++-- packages/quarkd/src/form/index.tsx | 39 +-- packages/quarkd/src/textarea/style.css | 2 +- 6 files changed, 492 insertions(+), 347 deletions(-) diff --git a/packages/quarkd/src/form/demo.vue b/packages/quarkd/src/form/demo.vue index eb43e78a..dbfb0c03 100644 --- a/packages/quarkd/src/form/demo.vue +++ b/packages/quarkd/src/form/demo.vue @@ -1,50 +1,82 @@ @@ -147,54 +147,39 @@ import { createComponent } from "@/utils/create"; const { createDemo, translate } = createComponent("form"); import { useTranslate } from "@/sites/assets/util/useTranslate"; -import { ref, onMounted, onBeforeUnmount, onBeforeMount } from "vue"; +import { ref, onMounted, onBeforeMount } from "vue"; import "./index"; import Toast from "../toast"; export default createDemo({ setup() { const formData = ref({ - name: { - a: "b", - }, - password: "password", captcha: "1234", checkbox: ["apple"], radio: "", rate: "", stepper: "", - switch: "", + switch: false, textarea: "", - uploader: [ - { url: "https://img.yzcdn.cn/vant/leaf.jpg" }, - { url: "https://img.yzcdn.cn/vant/leaf.jpg" }, - ], - picker: "绍兴", + uploader: [], + picker: "", + }); + const form = ref({ + name: "", + password: "", }); const formRef = ref(null); - const form1 = ref(null); const pickerRef = ref(null); - const uploadRef = ref(null); onMounted(() => { const picker = pickerRef.value; picker.setColumns([ { defaultIndex: 0, - values: ["杭州", "嘉兴", "绍兴", "宁波", "湖州", "千岛湖"], + values: translate("cities"), }, ]); - const uploader = uploadRef.value; - uploader.setPreview([ - "https://img.yzcdn.cn/vant/leaf.jpg", - "https://img.yzcdn.cn/vant/leaf.jpg", - ]); - - const form1Ref = form1.value; - form1Ref.setModel(formData.value); - }); - onBeforeUnmount(() => { - console.log("onBeforeUnmount"); + formRef.value?.setModel(form.value); }); onBeforeMount(() => { useTranslate({ @@ -204,24 +189,41 @@ export default createDemo({ rule: "自定义校验规则", items: "表单项大全", }, - labels: ["姓名", "密码", "年龄", "手机号"], - vegetable: "蔬菜:", - vegetables: ["黄瓜", "生姜"], - fruit: "水果:", - fruits: ["苹果", "香蕉"], - items: ["开灯:", "打分:", "步进器:", "上传:", "picker 选择器:"], - placeholder: "请输入文本", + label: { + name: "姓名", + password: "密码", + age: "年龄", + checkbox: "复选框", + radio: "单选框", + switch: "开关", + rate: "评分", + stepper: "步进器", + textarea: "文本域", + uploader: "文件上传", + picker: "选择器", + }, + placeholders: { + pattern: "正则校验", + validator: "函数校验", + asyncValidator: "异步校验", + }, + cities: ["杭州", "嘉兴", "绍兴", "宁波", "湖州", "千岛湖"], submit: "提交", + reset: "重置", error: { - timePicker: "请选择时间", - age: "不能小于18岁", name: "请输入姓名", - phone: "请输入正确的手机号", - formItem: "请检查表单项", - items: "当前表单所有的值", - console: "请在控制台查看表单值", + password: "请输入密码", + age: "不能小于18岁", + errorMsg: "请输入正确内容", + errorPwd: "密码不能为123456", + }, + pickerTitle: "请选择城市", + enum: { + apple: "苹果", + banana: "香蕉", + square: "方形", + circle: "圆形", }, - time: ["上午", "下午"], }, "en-US": { title: { @@ -229,48 +231,67 @@ export default createDemo({ rule: "Custom Rules", items: "Form Items", }, - labels: ["Name", "Password", "Age", "Phone"], - vegetable: "Vegetables:", - vegetables: ["Cucumber", "Ginger"], - fruit: "Fruit:", - fruits: ["Apple", "Banana"], - items: ["Switch:", "Rate:", "Stepper:", "Upload:", "Picker:"], - placeholder: "Please enter text", + label: { + name: "Name", + password: "Password", + age: "Age", + checkbox: "Checkbox", + radio: "Radio", + switch: "Swicth", + rate: "Rate", + stepper: "Stepper", + textarea: "Textarea", + uploader: "Uploader", + picker: "Picker", + }, + placeholders: { + pattern: "Use pattern", + validator: "Use validator", + asyncValidator: "Use async validator", + }, + cities: [ + "Hangzhou", + "Jiaxing", + "Shaoxing", + "Ningbo", + "Huzhou", + "Qiandaohu", + ], submit: "Submit", + reset: "Reset", error: { - timePicker: "Please select time", + name: "Name is required", + password: "Password is required", age: "Must not be younger than 18", - phone: "please enter a valid phone number", - formItem: "Please check the form item", - items: "All values of the current form", - console: "Please check the form value in the console", - name: "Please input name", + errorMsg: "Error message", + errorPwd: "Password can not be 123456", + }, + pickerTitle: "Please choose city", + enum: { + apple: "apple", + banana: "banana", + square: "square", + circle: "circle", }, - time: ["a.m.", "p.m."], }, }); }); - const submitHandler = async () => { - form1.value.validate((valid, errorMsg) => { - console.log("submitHandler", valid, errorMsg); + const submit = () => { + formRef.value.validate((valid, errorMsg) => { + console.log("submit", valid, errorMsg); }); - - // const valid = await form1.value.validate(); - // console.log(valid); }; - const resetHandler = () => { - form1.value.resetFields(); + const reset = () => { + formRef.value.resetFields(); }; const onCheckboxChange = ({ detail }) => { - console.log("onCheckboxChange", detail.value, formData.value); formData.value.checkbox = detail.value; }; const onRadioChange = ({ detail }) => { - console.log("onRadioChange", detail.value, formData.value); formData.value.radio = detail.value; }; @@ -281,7 +302,6 @@ export default createDemo({ }; const confirm = ({ detail }) => { - console.log(formData.value); formData.value.picker = detail.value.map((i) => i.value).join(" "); pickerVisible.value = false; }; @@ -292,21 +312,55 @@ export default createDemo({ }); }; + const ruleFormRef = ref(null); + const ruleForm = ref({ + name: "", + password: "", + age: "", + }); + + const ruleFormSubmit = async () => { + const valid = await ruleFormRef.value.validate(); + console.log(valid); + }; + const validatorPassword = (rule, val, callback) => { + if (!val) { + callback(new Error(translate("error.errorMsg"))); + } else if (val === "123456") { + callback(new Error(translate("error.errorPwd"))); + } else { + callback(); + } + }; + const asyncValidator = (rule, value) => { + return new Promise((resolve, reject) => { + if (value < 18) { + reject(translate("error.age")); + } else { + resolve(); + } + }); + }; + return { formRef, formData, - form1, pickerRef, translate, close, confirm, - submitHandler, onCheckboxChange, pickerVisible, - resetHandler, onRadioChange, - uploadRef, validateField, + submit, + reset, + form, + ruleFormRef, + ruleForm, + ruleFormSubmit, + validatorPassword, + asyncValidator, }; }, }); diff --git a/packages/quarkd/src/form/doc.zh-CN.md b/packages/quarkd/src/form/doc.zh-CN.md index eddaa603..ccdbca8e 100644 --- a/packages/quarkd/src/form/doc.zh-CN.md +++ b/packages/quarkd/src/form/doc.zh-CN.md @@ -8,35 +8,65 @@ ```tsx import "quarkd/lib/form"; +import "quarkd/lib/form-item"; ``` ### 基本用法 -配合 name 字段,设置表单项的值 +配合 prop 字段,设置表单项的值 ```html - - -
- -
-
提交
-
+ + + + + + + + +
+ 提交 + 重置 +
``` ```js -this.$refs.form1.setRules = ([ - { name: 'name', required: true }, - { name: 'password', required: true, type: 'password' } - ]); - submit1() { - this.$refs.form1.submit().then((value) => { - console.log(value, '当前表单所有的值'); - }).catch(err => { - Toast.text(err) - }); +export default { + data() { + return { + form: { + name: "", + password: "", + }, + }; }, + mounted() { + this.$refs.formRef.setModel(this.form); + }, + methods: { + submit() { + this.$refs.formRef.validate((valid, errorMsg) => { + console.log("submit", valid, errorMsg); + }); + }, + reset() { + this.$refs.formRef.resetFields(); + }, + }, +}; ``` ### 自定义校验规则 @@ -44,148 +74,203 @@ this.$refs.form1.setRules = ([ 只对 field 组件有用,支持 required 、validator 自定义事件 ```html - - -
- -
-
提交
-
+ + + + + + + + + + + +
+ + {{ translate("submit") }} + +
``` ```js - this.$refs.form2.setRules([ - { - name: 'age', - required: true, - message: '不能小于18岁', - validator: (value) => value >= 18 - }, - { - name: 'phone', - required: true, - message: '请输正确的手机号', - validator: (value) => /^1[3456789]\d{9}$/g.test(value) - } - ]); - - submit2() { - this.$refs.form2.submit().then((value) => { - console.log(value, '当前表单所有的值'); - }).catch(err => { - Toast.text(err) +export default { + const validatorPassword = (rule, val, callback) => { + if (!val) { + callback(new Error("请输入密码")); + } else if (val === "123456") { + callback(new Error("密码不能为123456")); + } else { + callback(); + } + }; + const asyncValidator = (rule, value) => { + return new Promise((resolve, reject) => { + if (value < 18) { + reject("不能小于18岁"); + } else { + resolve(); + } }); + }; + data() { + return { + ruleForm: { + name: "", + password: "", + age: "", + }, + validatorPassword, + asyncValidator, + } }, + mounted() { + this.$refs.ruleForm.setModel(this.ruleForm); + }, + methods: { + submit() { + this.$refs.ruleForm.validate((valid, errorMsg) => { + console.log("submit", valid, errorMsg); + }); + }, + } +} ``` ### 表单项大全 ```html - - -
-
- -
-
-
- 蔬菜: - 黄瓜 - 生姜 -
-
-
- 水果: - - 苹果 - 香蕉 + + + + 苹果 + 香蕉 + + + + + 方形 + 圆形 -
-
-
- 开灯: - -
-
-
- 打分: - -
-
-
- 步进器: - -
-
-
- 上传: - -
-
-
- picker 选择器 - + + + + + + + + + + + + + + + + + + -
-
-
-
提交
-
+ ``` ```js - submit3() { - this.$refs.form3.submit().then((value) => { - Toast.text('请在控制台查看表单值'); - console.log(value, '当前表单所有的值'); - }); - }, - click() { - this.open = true; +export default { + data() { + return { + pickerVisible: false, + form: { + checkbox: [] + radio: '', + switch: false, + rate: "", + stepper: "", + textarea: "", + uploader: [], + picker: "", + }, + } + }, + mounted() { + this.$refs.pickerRef.setColumns([ + { + defaultIndex: 0, + values: ["杭州", "嘉兴", "绍兴", "宁波", "湖州", "千岛湖"], + }, + ]); + }, + methods: { + onCheckboxChange({ detail }) { + this.formData.checkbox = detail.value; }, - close() { - this.open = false; + onRadioChange({ detail }) { + this.formData.radio = detail.value; }, confirm({ detail }) { - this.datepicker = detail.value.map((i) => i.value).join(' '); - this.open = false; + this.form.picker = detail.value.map((i) => i.value).join(" "); + this.pickerVisible = false; + }, + close() { + this.pickerVisible = false; } + } +} ``` ## API -### Method +### Form Props -| 名称 | 说明 | 类型 | -| -------- | ---------------------------------- | ----------------------------- | -| submit | 提交并校验表单获取所有组件的 value | `() => Promise` | -| setRules | 只对 field 组件有效 | `(rule: Rule[])=>void` | +| 参数 | 说明 | 类型 | 默认值 | +| -------------------- | ---------------------------------------- | --------------- | ------- | +| validatefirst | 是否在某一项校验不通过时停止校验 | `boolean` | `false` | +| hidemessage | 是否隐藏校验错误信息 | `boolean` | `false` | +| hiderequiredasterisk | 是否隐藏必填字段的标签旁边的红色星号 | `boolean` | `false` | +| labelwidth | 默认 value 值 | `string` | - | +| labelsuffix | 表单域标签的后缀 | `string` | | +| labelposition | 表单域标签的位置,则需要设置 label-width | `letf \| right` | `left` | -### 类型定义 +### Form Methods -```js -type Rule = { - name: string // 需要校验的 field 组件的 name 属性 - required?: boolean // 是否必填 - message?: string // 错误信息 - validator?: (value: string | number) => boolean; // 校验规则 -}; -``` +| 名称 | 说明 | 类型 | +| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | +| validate | 对整个表单进行校验的方法,参数为一个回调函数。该回调函数会在校验结束后被调用,并传入两个参数:是否校验成功和未通过校验的字段。若不传入回调函数,则会返回一个 promise | `Function(callback: Function(boolean, object))` | +| validateField | 对部分表单字段进行校验的方法 | `Function(props: array \| string, callback: Function(errorMessage: string))` | +| resetFields | 对整个表单进行重置,将所有字段值重置为初始值并移除校验结果 | | +| clearValidate | 移除表单项的校验结果。传入待移除的表单项的 prop 属性或者 prop 组成的数组,如不传则移除整个表单的校验结果 | `Function(props: array \| string)` | +| setModel | 设置表单数据对象 | `(model: object) => void` | + +### FormItem Props + +| 参数 | 说明 | 类型 | 默认值 | +| -------------------- | ---------------------------------------------------------------------------- | --------- | ------- | +| prop | 表单域 model 字段,在使用 validate、resetFields 方法的情况下,该属性是必填的 | `string` | | +| label | 标签文本 | `string` | `false` | +| labelwidth | 表单域标签的的宽度,例如 '50px'。 | `string` | | +| hidemessage | 是否隐藏校验错误信息 | `boolean` | `false` | +| hiderequiredasterisk | 是否隐藏必填字段的标签旁边的红色星号 | `boolean` | `false` | diff --git a/packages/quarkd/src/form/form-item.css b/packages/quarkd/src/form/form-item.css index 8064eb1f..9721ea36 100644 --- a/packages/quarkd/src/form/form-item.css +++ b/packages/quarkd/src/form/form-item.css @@ -7,31 +7,24 @@ display: flex; padding: 10px 0; margin: 0 16px; - line-height: 24px; } -.quark-form-item__prefix { - position: relative; +.quark-form-item__label { flex-shrink: 0; - /* width: var(--cell-title-width); */ - white-space: var(--cell-title-white-space, wrap); - font-size: var(--cell-title-font-size, 14px); + display: flex; + margin-right: 12px; + /* white-space: var(--cell-title-white-space, wrap); font-weight: var(--cell-title-font-weight); - text-overflow: var(--cell-title-text-overflow); -} - -.quark-form-item__asterisk { - color: #f00; - position: absolute; - left: 0px; - transform: translateX(-50%); + text-overflow: var(--cell-title-text-overflow); */ + font-size: var(--cell-title-font-size, 14px); + color: #5A6066; + line-height: 24px; } -.quark-form-item__label { - display: flex; - margin-right: 16px; +.quark-form-item__label span { + display: inline-block; } -.quark-form-item__label::before { +.is-required .quark-form-item__label::before { content: '*'; color: #f00; margin-right: 4px; @@ -45,11 +38,24 @@ flex: 1; } +.quark-form-item__main-content { + min-height: 24px; + display: flex; + align-items: center; +} + .quark-form-item_error-msg { color: #f00; font-size: 12px; + line-height: 24px; } .quark-form-item__suffix { + margin-left: 4px; flex-shrink: 0; +} + +.quark-form-item__is-link { + display: flex; + align-items: center; } \ No newline at end of file diff --git a/packages/quarkd/src/form/form-item.tsx b/packages/quarkd/src/form/form-item.tsx index b057373e..26face1e 100644 --- a/packages/quarkd/src/form/form-item.tsx +++ b/packages/quarkd/src/form/form-item.tsx @@ -21,11 +21,11 @@ export interface Rule { } interface IFormProps { - hiderequiredasterisk: boolean; - hidemessage: boolean; - labelwidth: string; - labelsuffix: string; - labelposition: "left" | "right"; + hideRequiredAsterisk: boolean; + hideMessage: boolean; + labelWidth: string; + labelSuffix: string; + labelPosition: "left" | "right"; } @customElement({ @@ -40,7 +40,7 @@ class QuarkFormItem extends QuarkElement { label = ""; @property({ type: String }) - labelwidth = ""; + labelWidth = ""; @property({ type: Boolean }) islink = false; @@ -51,9 +51,6 @@ class QuarkFormItem extends QuarkElement { @property({ type: Boolean }) hiderequiredasterisk: false; // 是否隐藏必填 * - @property({ type: Boolean }) - required: false; - formRef: any = createRef(); rules: IRuleItem[] = []; @@ -72,13 +69,14 @@ class QuarkFormItem extends QuarkElement { @state() formProps: IFormProps = { - hiderequiredasterisk: false, - hidemessage: false, - labelwidth: "", - labelsuffix: "", - labelposition: "left", + hideRequiredAsterisk: false, + hideMessage: false, + labelWidth: "", + labelSuffix: "", + labelPosition: "left", }; + @state() formModel = null; defaultSlotRef: any = createRef(); @@ -208,16 +206,11 @@ class QuarkFormItem extends QuarkElement { }; onFieldChange = debounce(() => { - console.log("onFieldChange-1"); if (this.validateDisabled) return; - console.log("onFieldChange-2"); this.validate(); }, 200); onFieldBlur() { - console.log("onFieldBlur-1"); - if (this.validateDisabled) return; - console.log("onFieldBlur-2"); this.validate(); } @@ -226,7 +219,6 @@ class QuarkFormItem extends QuarkElement { this.prop && this.rules && Array.isArray(this.rules) && - this.rules.length > 0 && this.rules.some((rule) => rule.required) ); } @@ -238,15 +230,18 @@ class QuarkFormItem extends QuarkElement { ? classNames.push("is-validating") : null; - if (this.formProps.hiderequiredasterisk) { + if (this.formProps.hideRequiredAsterisk) { classNames.push("is-no-asterisk"); } + if (this.isRequired()) { + classNames.push("is-required"); + } return classNames.join(" "); } errorMessageRender() { return ( - !this.formProps.hidemessage && + !this.formProps.hideMessage && !this.hidemessage && this.validateState === "error" && this.validateMessage && ( @@ -256,9 +251,9 @@ class QuarkFormItem extends QuarkElement { } labelStyle() { const ret: any = {}; - if (this.formProps.labelwidth) { - ret.width = this.labelwidth || this.formProps.labelwidth; - ret.textAlign = this.formProps.labelposition; + if (this.formProps.labelWidth) { + ret.width = this.labelWidth || this.formProps.labelWidth; + ret.textAlign = this.formProps.labelPosition; } return ret; } @@ -285,13 +280,11 @@ class QuarkFormItem extends QuarkElement { return (
-
-
- - {this.label} - {this.formProps.labelsuffix} - -
+
+ + {this.label} + {this.formProps.labelSuffix} +
diff --git a/packages/quarkd/src/form/index.tsx b/packages/quarkd/src/form/index.tsx index 10761f55..2e13e638 100644 --- a/packages/quarkd/src/form/index.tsx +++ b/packages/quarkd/src/form/index.tsx @@ -48,7 +48,7 @@ class QuarkForm extends QuarkElement { model: { [key: string]: any } | null = null; - handleRightSlotChange = () => { + onSlotChange = () => { if (this.slotRef.current) { this.formItems = this.slotRef.current .assignedNodes() @@ -59,11 +59,11 @@ class QuarkForm extends QuarkElement { el.setFormModel(this.model); } el.setFormProps({ - hidemessage: this.hidemessage, - labelwidth: this.labelwidth, - hiderequiredasterisk: this.hiderequiredasterisk, - labelsuffix: this.labelsuffix, - labelposition: this.labelposition, + hideMessage: this.hidemessage, + labelWidth: this.labelwidth, + hideRequiredAsterisk: this.hiderequiredasterisk, + labelSuffix: this.labelsuffix, + labelPosition: this.labelposition, }); }); } @@ -124,13 +124,27 @@ class QuarkForm extends QuarkElement { }); }; - clearValidate() { + clearValidate(props: string[] | string = "") { this.formItems.forEach((item) => { - item.clearValidate(); + if (props) { + if (Array.isArray(props) && props.indexOf(item.prop) > -1) { + item.clearValidate(); + } + if (item.prop === props) { + item.clearValidate(); + } + } else { + item.clearValidate(); + } }); } resetFields() { + if (!this.model) { + console.warn("[Quark Warn]please setModel!"); + return; + } + this.formItems.forEach((item) => { item.resetField(); }); @@ -140,17 +154,10 @@ class QuarkForm extends QuarkElement { this.model = model; }; - componentDidMount(): void { - console.log("componentDidMount"); - } - render() { return (
- +
); } diff --git a/packages/quarkd/src/textarea/style.css b/packages/quarkd/src/textarea/style.css index 8aef1701..c1b27017 100644 --- a/packages/quarkd/src/textarea/style.css +++ b/packages/quarkd/src/textarea/style.css @@ -3,7 +3,7 @@ } :host .quark-textarea { - padding: 12px; + /* padding: 12px; */ background: rgb(255, 255, 255); border: 1px solid var(--textarea-border-color); display: block;