UNPKG

@freshworks/crayons

Version:
539 lines (538 loc) 18.9 kB
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ import { Component, Prop, State, Element, h, Method, Watch, } from '@stencil/core'; import { v4 as uuidv4 } from 'uuid'; import { validateYupSchema, prepareDataForValidation, yupToFormErrors, generateDynamicInitialValues, generateDynamicValidationSchema, serializeForm, translateErrors, } from './form-util'; import { debounce } from '../../utils'; export class Form { constructor() { /** * Initial field values of the form. It is an object with keys pointing to field name */ this.initialValues = {}; /** * Schema to render Dynamic Form. Contains an array of fields pointing to each form control. * Please see the usage reference for examples. */ this.formSchema = {}; /** * YUP based validation schema for handling validation */ this.validationSchema = {}; /** Tells Form to validate the form on each input's onInput event */ this.validateOnInput = true; /** Tells Form to validate the form on each input's onBlur event */ this.validateOnBlur = true; /** The number of milliseconds to delay before doing validation on Input */ this.wait = 200; /** * Id to uniquely identify the Form. If not set, a random Id will be generated. */ this.formId = uuidv4(); this.values = {}; this.touched = {}; this.errors = {}; this.prevValues = {}; this.handleSubmit = async (event) => { event === null || event === void 0 ? void 0 : event.preventDefault(); event === null || event === void 0 ? void 0 : event.stopPropagation(); let isValid = false, touchedState = {}; await this.handleValidation(); const keys = [...Object.keys(this.values), ...Object.keys(this.errors)]; keys.forEach((k) => (touchedState = Object.assign(Object.assign({}, touchedState), { [k]: true }))); // on clicking submit, mark all fields as touched this.touched = Object.assign(Object.assign({}, this.touched), touchedState); isValid = !this.errors || Object.keys(this.errors).length === 0; if (!isValid) { this.setFocusOnError(); } let serializedValues = Object.assign({}, this.values); if (this.formSchema && Object.keys(this.formSchema).length > 0) { serializedValues = serializeForm(serializedValues, this.fields); } this.prevValues = this.values; const translatedErrors = await translateErrors(this.errors, this.fields); return { values: serializedValues, errors: translatedErrors, isValid }; }; this.handleReset = async (event) => { event === null || event === void 0 ? void 0 : event.preventDefault(); event === null || event === void 0 ? void 0 : event.stopPropagation(); this.values = this.formInitialValues; this.prevValues = this.values; let touchedState = {}; const initialValuesKeys = Object.keys(this.initialValues); initialValuesKeys.forEach((k) => (touchedState = Object.assign(Object.assign({}, touchedState), { [k]: true }))); this.touched = touchedState; if (initialValuesKeys && initialValuesKeys.length > 0) { await this.handleValidation(); this.setFocusOnError(); } }; this.handleValidation = async () => { let validationErrors = {}; // run validations against validationSchema if present if (this.formValidationSchema && Object.keys(this.formValidationSchema).length) { const pr = validateYupSchema(prepareDataForValidation(this.values), this.formValidationSchema); try { await pr; validationErrors = {}; // reset errors if no errors from validation } catch (err) { validationErrors = yupToFormErrors(err); } } // run validations with validate function if passed as prop and merge with the errors from the above step if (this.validate && typeof this.validate === 'function') { try { validationErrors = Object.assign(Object.assign({}, validationErrors), ((await this.validate(this.values)) || {})); } catch (err) { console.error(`Error in calling validate function ${err.message}`); validationErrors = Object.assign({}, validationErrors); } } this.errors = validationErrors; }; this.handleInput = async (event) => { const details = event.detail; if (!details || !details.name) return; const { name, value, meta } = details; this.values = Object.assign(Object.assign({}, this.values), { [name]: meta && 'checked' in meta ? meta.checked : value }); if (meta && meta.shouldValidate === false) { return; } /** Validate, if user wants to validateOnInput */ if (this.validateOnInput) { this.touched = Object.assign(Object.assign({}, this.touched), { [name]: true }); await this.handleValidation(); } }; this.handleBlur = async (event) => { var _a, _b; const details = event.detail; if (!details || !details.name) return; const { name } = details; /** Validate, if user wants to validateOnBlur */ if (this.validateOnBlur) { this.touched = Object.assign(Object.assign({}, this.touched), { [name]: true }); if (((_a = this.prevValues) === null || _a === void 0 ? void 0 : _a[name]) !== ((_b = this.values) === null || _b === void 0 ? void 0 : _b[name])) { // validate only if the previous value is different from the current value await this.handleValidation(); } } }; this.setFocus = (field) => { var _a; const control = (_a = this.controls) === null || _a === void 0 ? void 0 : _a.find((control) => control.name === field); control === null || control === void 0 ? void 0 : control.setFocus(); }; this.setFocusOnError = () => { const firstErrorField = Object.keys(this.errors || {}) .sort((a, b) => { var _a, _b, _c, _d; return ((_b = (_a = this.fields) === null || _a === void 0 ? void 0 : _a[a]) === null || _b === void 0 ? void 0 : _b.position) - ((_d = (_c = this.fields) === null || _c === void 0 ? void 0 : _c[b]) === null || _d === void 0 ? void 0 : _d.position); }) .find((field) => { return this.touched[field] === true; }); if (firstErrorField) this.setFocus(firstErrorField); }; this.composedUtils = () => { const inputProps = (field) => ({ value: this.values[field], }); const radioProps = (field) => ({ value: this.values[field], }); const checkboxProps = (field) => ({ checked: !!this.values[field], }); const selectProps = (field, inputType) => ({ value: inputType === 'multi_select' ? this.values[field] || [] : this.values[field] || '', }); const formProps = { action: 'javascript:void(0);', onSubmit: this.handleSubmit, onReset: this.handleReset, }; return { inputProps, selectProps, checkboxProps, radioProps, formProps, }; }; } async componentWillLoad() { this.debouncedHandleInput = debounce(this.handleInput, this, this.wait); this.handleInputListener = this.el.addEventListener('fwInput', this.debouncedHandleInput); this.handleBlurListener = this.el.addEventListener('fwBlur', this.handleBlur); this.handleChangeListener = this.el.addEventListener('fwChange', this.handleInput); await this.handleFormSchemaAndInitialValuesChange(this.formSchema, this.initialValues); } async formSchemaHandler(formSchema) { await this.handleFormSchemaAndInitialValuesChange(formSchema, this.initialValues); } async initialValuesHandler(initialValues) { await this.handleFormSchemaAndInitialValuesChange(this.formSchema, initialValues); } async handleFormSchemaAndInitialValuesChange(formSchema, initialValues) { var _a; this.fields = (_a = formSchema === null || formSchema === void 0 ? void 0 : formSchema.fields) === null || _a === void 0 ? void 0 : _a.reduce((acc, field) => { return Object.assign(Object.assign({}, acc), { [field.name]: field }); }, {}); this.formValidationSchema = generateDynamicValidationSchema(formSchema, this.validationSchema) || {}; this.formInitialValues = generateDynamicInitialValues(formSchema, initialValues) || {}; this.values = this.formInitialValues; this.prevValues = this.values; const initialValuesKeys = Object.keys(initialValues); for (const field of Object.keys(this.formInitialValues)) { this.errors[field] = null; if (initialValuesKeys === null || initialValuesKeys === void 0 ? void 0 : initialValuesKeys.includes(field)) this.touched[field] = true; else this.touched[field] = false; } await this.handleValidation(); } // get Form Controls and pass props to children componentDidLoad() { this.controls = this.getFormControls(); this.passPropsToChildren(this.controls); // adding a timeout since this lifecycle method is called before its child in React apps. // Bug with react wrapper. setTimeout(() => { this.setFocusOnError(); }, 10); } // pass props to form-control children componentWillUpdate() { if (!this.controls || !this.controls.length) { this.controls = this.getFormControls(); } this.passPropsToChildren(this.controls); } handleSlotChange() { this.controls = this.getFormControls(); } disconnectedCallback() { var _a, _b, _c, _d, _e, _f; (_b = (_a = this.el) === null || _a === void 0 ? void 0 : _a.removeEventListener) === null || _b === void 0 ? void 0 : _b.call(_a, 'fwBlur', this.handleBlurListener); (_d = (_c = this.el) === null || _c === void 0 ? void 0 : _c.removeEventListener) === null || _d === void 0 ? void 0 : _d.call(_c, 'fwInput', this.handleInputListener); (_f = (_e = this.el) === null || _e === void 0 ? void 0 : _e.removeEventListener) === null || _f === void 0 ? void 0 : _f.call(_e, 'fwChange', this.handleChangeListener); } getFormControls() { const children = Array.from([ ...this.el.shadowRoot.querySelectorAll('*'), ...this.el.querySelectorAll('*'), ]).filter((el) => ['fw-form-control'].includes(el.tagName.toLowerCase())); return children; } passPropsToChildren(controls) { controls === null || controls === void 0 ? void 0 : controls.forEach((control) => { this.passPropsToChild(control); }); } passPropsToChild(control) { const error = this.errors[control.name]; const touched = this.touched[control.name]; control.controlProps = this.composedUtils(); control.error = error || ''; control.touched = touched || false; } async setFieldValue(field, value, shouldValidate = true) { this.values = Object.assign(Object.assign({}, this.values), { [field]: value }); if (shouldValidate) { this.touched = Object.assign(Object.assign({}, this.touched), { [field]: true }); await this.handleValidation(); } } async setFieldErrors(errorObj) { var _a; (_a = Object.entries(errorObj)) === null || _a === void 0 ? void 0 : _a.forEach(([field, value]) => { this.errors = Object.assign(Object.assign({}, this.errors), { [field]: value }); this.touched = Object.assign(Object.assign({}, this.touched), { [field]: true }); }); this.setFocusOnError(); } async doSubmit(e) { return this.handleSubmit(e); } async doReset(e) { this.handleReset(e); } render() { var _a, _b; const utils = this.composedUtils(); return (h("form", Object.assign({ id: `form-${this.formId}` }, utils.formProps), this.formSchema && Object.keys(this.formSchema).length > 0 ? ((_b = (_a = this.formSchema) === null || _a === void 0 ? void 0 : _a.fields) === null || _b === void 0 ? void 0 : _b.sort((a, b) => a.position - b.position).map((field) => { return (h("fw-form-control", { key: field.name, name: field.name, type: field.type, label: field.label, required: field.required, hint: field.hint, placeholder: field.placeholder, choices: field.choices, fieldProps: field, controlProps: utils })); })) : (h("slot", { onSlotchange: () => this.handleSlotChange() })))); } static get is() { return "fw-form"; } static get encapsulation() { return "shadow"; } static get properties() { return { "initialValues": { "type": "any", "mutable": false, "complexType": { "original": "any", "resolved": "any", "references": {} }, "required": false, "optional": true, "docs": { "tags": [], "text": "Initial field values of the form. It is an object with keys pointing to field name" }, "attribute": "initial-values", "reflect": false, "defaultValue": "{}" }, "validate": { "type": "any", "mutable": false, "complexType": { "original": "any", "resolved": "any", "references": {} }, "required": false, "optional": true, "docs": { "tags": [], "text": "Validate the form's values with an async function.\nShould return a Promise which resolves to an errors object.\nThe keys in the errors object must match with the field names." }, "attribute": "validate", "reflect": false }, "formSchema": { "type": "any", "mutable": false, "complexType": { "original": "any", "resolved": "any", "references": {} }, "required": false, "optional": true, "docs": { "tags": [], "text": "Schema to render Dynamic Form. Contains an array of fields pointing to each form control.\nPlease see the usage reference for examples." }, "attribute": "form-schema", "reflect": false, "defaultValue": "{}" }, "validationSchema": { "type": "any", "mutable": false, "complexType": { "original": "any", "resolved": "any", "references": {} }, "required": false, "optional": true, "docs": { "tags": [], "text": "YUP based validation schema for handling validation" }, "attribute": "validation-schema", "reflect": false, "defaultValue": "{}" }, "validateOnInput": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": true, "docs": { "tags": [], "text": "Tells Form to validate the form on each input's onInput event" }, "attribute": "validate-on-input", "reflect": false, "defaultValue": "true" }, "validateOnBlur": { "type": "boolean", "mutable": false, "complexType": { "original": "boolean", "resolved": "boolean", "references": {} }, "required": false, "optional": true, "docs": { "tags": [], "text": "Tells Form to validate the form on each input's onBlur event" }, "attribute": "validate-on-blur", "reflect": false, "defaultValue": "true" }, "wait": { "type": "number", "mutable": false, "complexType": { "original": "number", "resolved": "number", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "The number of milliseconds to delay before doing validation on Input" }, "attribute": "wait", "reflect": false, "defaultValue": "200" }, "formId": { "type": "any", "mutable": false, "complexType": { "original": "any", "resolved": "any", "references": {} }, "required": false, "optional": false, "docs": { "tags": [], "text": "Id to uniquely identify the Form. If not set, a random Id will be generated." }, "attribute": "form-id", "reflect": false, "defaultValue": "uuidv4()" } }; } static get states() { return { "values": {}, "touched": {}, "errors": {}, "formValidationSchema": {}, "formInitialValues": {} }; } static get methods() { return { "setFieldValue": { "complexType": { "signature": "(field: string, value: any, shouldValidate?: boolean) => Promise<void>", "parameters": [{ "tags": [], "text": "" }, { "tags": [], "text": "" }, { "tags": [], "text": "" }], "references": { "Promise": { "location": "global" } }, "return": "Promise<void>" }, "docs": { "text": "", "tags": [] } }, "setFieldErrors": { "complexType": { "signature": "(errorObj: FormErrors<FormValues>) => Promise<void>", "parameters": [{ "tags": [], "text": "" }], "references": { "Promise": { "location": "global" }, "FormErrors": { "location": "import", "path": "./form-declaration" }, "FormValues": { "location": "import", "path": "./form-declaration" } }, "return": "Promise<void>" }, "docs": { "text": "", "tags": [] } }, "doSubmit": { "complexType": { "signature": "(e: any) => Promise<FormSubmit>", "parameters": [{ "tags": [], "text": "" }], "references": { "Promise": { "location": "global" }, "FormSubmit": { "location": "import", "path": "./form-declaration" } }, "return": "Promise<FormSubmit>" }, "docs": { "text": "", "tags": [] } }, "doReset": { "complexType": { "signature": "(e: any) => Promise<void>", "parameters": [{ "tags": [], "text": "" }], "references": { "Promise": { "location": "global" } }, "return": "Promise<void>" }, "docs": { "text": "", "tags": [] } } }; } static get elementRef() { return "el"; } static get watchers() { return [{ "propName": "formSchema", "methodName": "formSchemaHandler" }, { "propName": "initialValues", "methodName": "initialValuesHandler" }]; } }