diff --git a/src/classes/Form.ts b/src/classes/Form.ts index 47258c5..b5ba4a8 100644 --- a/src/classes/Form.ts +++ b/src/classes/Form.ts @@ -668,7 +668,9 @@ export default class Form extends EventEmitter implements FormDependence { * */ validate(): boolean { const result = this.dependencies.reduce((acc, dep) => { - if (typeof dep.validate === "function") acc = acc && !!dep.validate(); + const depValidationResult = (typeof dep.validate === "function") ? dep.validate() : true; + console.log("Dep validation result:", depValidationResult) + acc = acc && !!depValidationResult; return acc; }, true); diff --git a/src/local-hooks/use-modify.ts b/src/local-hooks/use-modify.ts index fdefae8..ac8e919 100644 --- a/src/local-hooks/use-modify.ts +++ b/src/local-hooks/use-modify.ts @@ -11,8 +11,7 @@ export default function useModify(callbackModifyProps: () => ModifyParam) { return function execute(v: unknown): string { const arr = parse(callbackModifyProps()); // Getting array of handlers - - arr.forEach(callback => v = callback(v)); + try { arr.forEach(callback => v = callback(v)); } catch (e) { diff --git a/src/types/index.ts b/src/types/index.ts index c3588cb..a2e28ab 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -7,6 +7,7 @@ export type Value = Values | any; * @description Callback использующийся для валидации поля для ввода. * */ export type FormInputValidationCallback = (values: any) => boolean | string +export type ValidationError = string | false export type ValidationRule = () => boolean | string; diff --git a/src/widgets/field-wrap.vue b/src/widgets/field-wrap.vue index cf1bdda..7fe9218 100644 --- a/src/widgets/field-wrap.vue +++ b/src/widgets/field-wrap.vue @@ -14,9 +14,11 @@ diff --git a/src/widgets/form-field.vue b/src/widgets/form-field.vue index 41d7e40..7c40616 100644 --- a/src/widgets/form-field.vue +++ b/src/widgets/form-field.vue @@ -9,7 +9,7 @@ :disabled = "input?.disabled" :changed = "input?.changed" - :errors="input?.errors" + :errors="input?.errors || []" /> @@ -18,11 +18,13 @@ import {computed, onMounted, onUnmounted, reactive} from "vue"; import {getFieldType} from "../config/store"; import Form from "../classes/Form"; import {FormInputValidationCallback} from "../types"; +import STORE from "../../plugin/config/store"; interface IProps { name: string, type?: string, - validation?: FormInputValidationCallback[] + validation?: FormInputValidationCallback[] | FormInputValidationCallback, + required?: boolean } const props = defineProps() const parentForm = Form.getParentForm(); @@ -67,7 +69,9 @@ function useFormInput(name: string) { const InputDependency = { name, - validation() { + validate() { + + console.log("Input validation:", validation, input.value); const result = validation.reduce((acc: (string | boolean)[], guard) => { const guardResult = guard(input.value); if (guardResult !== true) acc.push(guardResult); @@ -75,7 +79,7 @@ function useFormInput(name: string) { }, []); input.errors = result; - return result.length + return result.length === 0 } } @@ -87,15 +91,28 @@ function useFormInput(name: string) { parentForm.unsubscribe(InputDependency); }) - function setValidation(array: FormInputValidationCallback[]) { - validation = array; + function setValidation(array?: FormInputValidationCallback[] | FormInputValidationCallback) { + validation = typeof array === 'function' ? [array] : (array || []); } return input; } -const input = useFormInput(props.name); +function mergeValidation() { + const arr:FormInputValidationCallback[] = []; + if (props.validation) { + if (typeof props.validation === 'function') arr.push(props.validation); + else arr.push(...props.validation) + } + + if (props.required) arr.unshift((v: any) => !!v || STORE.requiredMessage) + + return arr; +} +const input = useFormInput(props.name) +// @ts-ignore; +input?.setValidation(mergeValidation()) \ No newline at end of file diff --git a/tests/integrations/input-text.spec.ts b/tests/integrations/input-text.spec.ts index 9939ebc..df68efb 100644 --- a/tests/integrations/input-text.spec.ts +++ b/tests/integrations/input-text.spec.ts @@ -4,6 +4,9 @@ import Form from "../../src/classes/Form"; import {defineComponent} from "vue"; import {InputField} from "../../src//index"; import wait from "../wait"; +import {FormInputValidationCallback} from "@/types"; +import STORE from "../../src/config/store"; +import AppInputTextPretty from "./components/input-text/AppInputTextPretty.vue" describe("Input text", () => { test("Default empty input-text", async () => { @@ -176,4 +179,217 @@ describe("Input text", () => { const inputs = app.findAll('input'); expect(inputs.map(a => a.element.value)).toEqual(["T", "T"]) }) + + test("Input-text with validation", async () => { + const test:FormInputValidationCallback[] = [ + x => x !== 'Jack' + ]; + const component = defineComponent({ + template: `
+ +
`, + components: {InputField} + }) + const app = mount(EmptyApp, { + slots: { + default: component + }, + }); + const form = (app.vm as any).form as Form; + app.vm.loadingResource = true + await app.vm.$nextTick() + + expect(form.validate()).toBe(true); + form.setValues({ + username: "Jack" + }) + + await app.vm.$nextTick() + + expect(form.validate()).toBe(false); + form.setValues({ + username: "Jack-1" + }) + await app.vm.$nextTick() + expect(form.validate()).toBe(true) + }) + test("Input should has error effect when one of validation was rejected", async () => { + const ERROR_TEXT = 'Jack is rejected' + const test:FormInputValidationCallback[] = [ + x => x === 'Jack' ? true : 'Jack is rejected' + ]; + const component = defineComponent({ + template: `
`, + components: {InputField} + }) + const app = mount(EmptyApp, { + slots: { + default: component + }, + }); + const form = (app.vm as any).form as Form; + app.vm.loadingResource = true + await app.vm.$nextTick() + + const validateResult = form.validate() + expect(validateResult).toBe(false); + await app.vm.$nextTick(); + await wait() + expect(app.find('.container-input-text_error').exists()).toBe(true); + expect(app.text()).toBe(ERROR_TEXT); + + form.setValues({ + username: "Jack" + }) + form.validate() + await app.vm.$nextTick() + await wait() + + expect(app.find('.container-input-text_error').exists()).toBe(false); + expect(app.text()).toBe(""); + }) + test("Input that required param should has required label and should be validation reject if empty", async () => { + const component = defineComponent({ + template: `
`, + components: {InputField} + }) + const app = mount(EmptyApp, { + slots: { default: component }, + }); + const form = (app.vm as any).form as Form; + app.vm.loadingResource = true + await app.vm.$nextTick() + + expect(form.validate()).toBe(false); + + await wait(); + + expect(app.text()).toBe(STORE.requiredMessage); + form.setValues({ + username: "PP" + }) + expect(form.validate()).toBe(true); + await wait() + expect(app.text()).toBe("") + }) + test("If label is provided it should be visible", async () => { + const component = defineComponent({ + template: `
`, + components: {InputField} + }) + const app = mount(EmptyApp, { + slots: { default: component }, + }); + const form = (app.vm as any).form as Form; + app.vm.loadingResource = true + await app.vm.$nextTick() + + expect(app.text()).toBe("Your name"); + }); + test("Input that has modify should edit value after input", async () => { + // Функция для модифицирования номера автобуса, функция удаляет всё кроме букв и вставляет -, после первых двух. + // AABBCC -> AA-BBCC + const modify = (x: unknown) => { + if (typeof x !== 'string') return ''; + let str = x.replaceAll(/[^A-Z]/g, ''); + str = str.slice(0, 6); + if (str.length < 6) return str; + return `${str.slice(0, 2)}-${str.slice(2, 6)}` + } + const component = defineComponent({ + template: `
`, + components: { + InputField + } + }) + const app = mount(EmptyApp, { + slots: { + default: component + } + }) + const form = app.vm.form as Form; + app.vm.loadingResource = true; + await app.vm.$nextTick(); + + const input = app.get("input"); + await input.setValue("123") + expect(form.getValueByName('bus-number')).toBe("") + + await input.setValue("A1A2B3BC55") + expect(form.getValueByName('bus-number')).toBe("AABBC") + + await input.setValue("AABBCC") + expect(form.getValueByName('bus-number')).toBe("AA-BBCC") + }) + test("Input that has pretty should prettify just view-value, not value in form", async () => { + + const app = mount(AppInputTextPretty) + const form = app.vm.form as Form; + + //app.vm.loadingResource = true; + await app.vm.$nextTick(); + + const input = app.get("input"); + await input.setValue("jack") + await app.vm.$nextTick(); + await wait() + + expect(form.getValueByName('name')).toBe("jack") + expect(input.element.value).toBe('-jack-') + + + await input.setValue("a") + await wait() + expect(form.getValueByName('name')).toBe("a") + expect(input.element.value).toBe("-a-") + + await input.setValue("") + await wait() + expect(form.getValueByName('name')).toBe("") + expect(input.element.value).toBe("") + }) + test("Pretty and modify should be executed once", async () => { + const pretty = (x: any) => x; + const modify = (x: any) => x; + + const mockPretty = jest.fn(pretty) + const mockModify = jest.fn(modify) + + const component = defineComponent({ + // @ts-ignore + template: `
`, + components: { + InputField + } + }) + const app = mount(EmptyApp, { + slots: { + default: component + } + }) + const form = app.vm.form as Form; + app.vm.loadingResource = true; + await app.vm.$nextTick() + const input = app.get('input'); + + await input.setValue("123"); + await app.vm.$nextTick() + await wait() + + + expect(input.element.value).toBe("123") + expect(form.getValueByName('username')).toBe("123++++++++++") + expect(mockPretty.mock.calls.length).toBe(1) + expect(mockModify.mock.calls.length).toBe(1) + + + }) + test("Max length attr should limit the length of value", () => {}) + test("Placeholder in input should be shown if provided", () => {}) + test("Prefix should be shown if provided", () => {}) + test("Numeric attr should return only numeric value and reject entering chars", () => { + const form:any = null; + + expect(form.getValueByName('age')).toBe(24); + }) }) \ No newline at end of file