@freshworks/crayons
Version:
Crayons Web Components library
539 lines (538 loc) • 18.9 kB
JavaScript
/* 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"
}]; }
}