UNPKG

@arudovwen/react-form-builder

Version:
622 lines (611 loc) 23.7 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends")); var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var _react = _interopRequireDefault(require("react")); var _reactDom = _interopRequireDefault(require("react-dom")); var _fbemitter = require("fbemitter"); var _reactIntl = require("react-intl"); var _formValidator = _interopRequireDefault(require("./form-validator")); var _formElements = _interopRequireDefault(require("./form-elements")); var _multiColumn = require("./multi-column"); var _fieldset = require("./fieldset"); var _customElement = _interopRequireDefault(require("./form-elements/custom-element")); var _registry = _interopRequireDefault(require("./stores/registry")); var _ToggleFieldsContainer = _interopRequireDefault(require("./ToggleFieldsContainer")); require("./styles/final.css"); const { Image, Checkboxes, Signature, Download, Camera, FileUpload } = _formElements.default; // ---- validators (hoisted) ---- const EMAIL_RE = // eslint-disable-next-line no-useless-escape /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}(\.\d{1,3}){3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; const validateEmail = email => EMAIL_RE.test(String(email || "").trim()); const validatePhone = phone => /^(\+)?([0-9]{1,4}[\s\-()]?){2,}$/.test(String(phone || "").trim()); class ReactForm extends _react.default.Component { constructor(props) { super(props); (0, _defineProperty2.default)(this, "form", null); (0, _defineProperty2.default)(this, "inputs", {}); // map field_name -> ref/component (0, _defineProperty2.default)(this, "emitter", new _fbemitter.EventEmitter()); (0, _defineProperty2.default)(this, "onChangeDebounceId", null); (0, _defineProperty2.default)(this, "_getDefaultValue", item => this.state.answers?.[item.field_name]); (0, _defineProperty2.default)(this, "_getOptionKeyValue", option => this.props.option_key_value === "value" ? option?.value : option?.key); (0, _defineProperty2.default)(this, "handleRenderSubmit", () => { const name = this.props.action_name || this.props.actionName || "Submit"; return this.props.submitButton || /*#__PURE__*/_react.default.createElement("input", { type: "submit", className: "btn btn-big", value: name }); }); (0, _defineProperty2.default)(this, "handleRenderBack", () => { const name = this.props.back_name || this.props.backName || "Cancel"; if (!this.props.back_action) return null; return this.props.backButton || /*#__PURE__*/_react.default.createElement("a", { href: this.props.back_action, className: "btn btn-default btn-cancel btn-big" }, name); }); this.state = { answers: this._convert(props.answer_data) || {} // canonical answers }; this.getDataById = this.getDataById.bind(this); this.handleBlur = this.handleBlur.bind(this); this.handleChange = this.handleChange.bind(this); this.handleSubmit = this.handleSubmit.bind(this); } componentWillUnmount() { if (this.onChangeDebounceId) clearTimeout(this.onChangeDebounceId); } // Convert array<{name,value}> to object _convert(answers) { if (Array.isArray(answers)) { const result = {}; answers.forEach(x => { if (x?.name?.indexOf("tags_") > -1) { result[x.name] = (x.value || []).map(y => y.value); } else { result[x.name] = x.value; } }); return result; } return answers || {}; } // answers accessor with variable overlays (read-only variables) _answersWithVariables() { const { answers } = this.state; const { variables = {}, data = [] } = this.props; if (!variables || !data?.length) return answers; const overlay = { ...answers }; for (const item of data) { if (!item) continue; if (item.readOnly && item.variableKey && variables[item.variableKey] != null) { overlay[item.field_name] = variables[item.variableKey]; } } return overlay; } _optionsDefaultValue(item) { const defaultValue = this._getDefaultValue(item); if (defaultValue) return defaultValue; const def = []; (item.options || []).forEach(option => { const key = option?.key; if (key && this.state.answers?.[`option_${key}`]) { def.push(key); } }); return def; } _getItemValue(item, ref, trimValue) { // Return uniform { element, value } const $ = { element: item.element, value: "" }; if (!item || !ref) return $; switch (item.element) { case "Rating": $.value = ref?.inputField?.current?.state?.rating ?? ""; return $; case "Tags": $.value = ref?.inputField?.current?.state?.value ?? ""; return $; case "DatePicker": case "Base64FileViewer": $.value = ref?.state?.value ?? ""; return $; case "Camera": $.value = ref?.state?.img ?? ""; return $; case "TableInput": $.value = ref?.state?.tableData ?? ""; return $; case "RadioButton": $.value = ref?.state?.selectedValue ?? ""; return $; case "DynamicMultiInput": case "DataGridInput": case "CascadeSelect": case "CustomDatePicker": case "ArithmeticInput": case "CustomSelect": $.value = ref?.state?.dataList ?? ""; return $; case "FileUpload": case "MultiFileUpload": case "AzureFileUpload": $.value = ref?.state?.fileUpload ?? []; return $; default: { // Native input path: prefer an explicit ref from child if provided const inputRef = ref?.inputField?.current; if (inputRef) { const dom = inputRef; // current is a DOM node for native inputs let v = dom.value; if (trimValue && typeof v === "string") v = v.trim(); $.value = v; return $; } // Fallback (legacy): try findDOMNode on child wrapper const dom = _reactDom.default.findDOMNode(ref?.inputField?.current || ref); if (dom && dom.value != null) { $.value = trimValue && typeof dom.value === "string" ? dom.value.trim() : dom.value; } return $; } } } _isIncorrect(item) { if (!item?.canHaveAnswer) return false; const ref = this.inputs[item.field_name]; if (!ref) return false; if (item.element === "Checkboxes" || item.element === "RadioButtons") { for (const option of item.options || []) { const child = ref.options?.[`child_ref_${option.key}`]; const dom = child && _reactDom.default.findDOMNode(child); const checked = !!dom?.checked; const hasCorrect = Object.prototype.hasOwnProperty.call(option || {}, "correct"); if (hasCorrect && !checked || !hasCorrect && checked) return true; } return false; } const $item = this._getItemValue(item, ref, true); if (item.element === "Rating") { return String($item.value) !== String(item.correct); } return String($item.value || "").toLowerCase() !== String(item.correct || "").trim().toLowerCase(); } _isInvalid(item) { if (!item?.required) return false; const ref = this.inputs[item.field_name]; if (!ref) return true; if (item.element === "Checkboxes" || item.element === "RadioButtons") { let checked = 0; for (const option of item.options || []) { const child = ref.options?.[`child_ref_${option.key}`]; const dom = child && _reactDom.default.findDOMNode(child); if (dom?.checked) checked += 1; } return checked < 1; } const $item = this._getItemValue(item, ref, false); if (item.element === "Rating") return Number($item.value) === 0; if (item.element === "MultiFileUpload") return ($item.value || []).some(dt => !dt?.fileData); return !$item.value || $item.value?.length < 1; } _collect(item, trimValue) { if (!item?.field_name) return null; const ref = this.inputs[item.field_name]; const itemData = { id: item.id, name: item.field_name, custom_name: item.custom_name || item.field_name }; if (item.element === "Checkboxes" || item.element === "RadioButtons") { const checked = []; for (const option of item.options || []) { const child = this.inputs[item.field_name]?.options?.[`child_ref_${option.key}`]; const dom = child && _reactDom.default.findDOMNode(child); if (dom?.checked) checked.push(this._getOptionKeyValue(option)); } itemData.value = checked; return itemData; } if (!ref) return null; itemData.value = this._getItemValue(item, ref, trimValue).value; return itemData; } _collectFormData(items, trimValue) { const out = []; for (const item of items || []) { const d = this._collect(item, trimValue); if (d) out.push(d); } return out; } _getSignatureImg(item) { const ref = this.inputs[item.field_name]; const canvas = ref?.canvas?.current; if (!canvas) return; const base64 = canvas.toDataURL().replace("data:image/png;base64,", ""); const isEmpty = canvas.isEmpty(); const input = ref?.inputField?.current && _reactDom.default.findDOMNode(ref.inputField.current); if (input) input.value = isEmpty ? "" : base64; } // ---- events ---- handleSubmit(e) { e.preventDefault(); let errors = []; if (!this.props.skip_validations) { errors = this.validateForm(); this.emitter.emit("formValidation", errors); } if (errors.length < 1) { const data = this._collectFormData(this.props.data, true); if (this.props.onSubmit) { this.props.onSubmit(data); } else { const $form = _reactDom.default.findDOMNode(this.form); $form && $form.submit && $form.submit(); } } } handleBlur() { // snapshot answers on blur (trimmed) const data = this._collectFormData(this.props.data, true); const next = {}; data.forEach(item => next[item.name] = item.value); this.setState({ answers: next }, () => { this.props.onBlur?.(data); }); } handleChange() { // snapshot answers on change (untrimmed), debounce parent onChange const data = this._collectFormData(this.props.data, false); const next = {}; data.forEach(item => next[item.name] = item.value); this.setState({ answers: next }); if (this.onChangeDebounceId) clearTimeout(this.onChangeDebounceId); this.onChangeDebounceId = setTimeout(() => { this.props.onChange?.(data); }, 120); } // ---- validation ---- validateForm() { const errors = []; const { intl, display_short } = this.props; const data_items = display_short ? (this.props.data || []).filter(i => i?.alternateForm === true) : this.props.data || []; for (const item of data_items) { if (!item) continue; if (item.element === "Signature") this._getSignatureImg(item); if (this._isInvalid(item)) { errors.push(`${item.label} ${intl.formatMessage({ id: "message.is-required" })}!`); } if (item.element === "EmailInput") { const ref = this.inputs[item.field_name]; const emailValue = this._getItemValue(item, ref).value; if (emailValue && !validateEmail(emailValue)) { errors.push(`${item.label} ${intl.formatMessage({ id: "message.invalid-email" })}`); } } if (item.element === "PhoneNumber") { const ref = this.inputs[item.field_name]; const phoneValue = this._getItemValue(item, ref).value; if (phoneValue && !validatePhone(phoneValue)) { errors.push(`${item.label} ${intl.formatMessage({ id: "message.invalid-phone-number" })}`); } } if (this.props.validateForCorrectness && this._isIncorrect(item)) { errors.push(`${item.label} ${intl.formatMessage({ id: "message.was-answered-incorrectly" })}!`); } } return errors; } // ---- rendering ---- getDataById(id) { return (this.props.data || []).find(x => x?.id === id); } getInputElement(item) { if (!item) return null; if (item.custom) return this.getCustomElement(item); const Input = _formElements.default[item.element]; const ro = this.props.read_only || item?.readOnly; // Pass live answers (with variable overlays) so dependent fields update const liveAnswers = this._answersWithVariables(); return /*#__PURE__*/_react.default.createElement("div", { key: `form_${item.id}` }, /*#__PURE__*/_react.default.createElement(_ToggleFieldsContainer.default, { data: item, toggleVisibility: item?.toggleVisibility, fields: item?.visibilityFields || [], results: liveAnswers }, /*#__PURE__*/_react.default.createElement(Input, { handleChange: this.handleChange, ref: c => this.inputs[item.field_name] = c, mutable: true, data: item, read_only: ro, defaultValue: this._getDefaultValue(item), resultData: liveAnswers, apiBaseUrl: this.props.apiBaseUrl || "https://api.dev.gateway.kusala.com.ng" }))); } getContainerElement(item, Element) { const controls = (item.childItems || []).map(x => x ? this.getInputElement(this.getDataById(x)) : /*#__PURE__*/_react.default.createElement("div", { key: `empty_${item.id}` }, "\xA0")); return /*#__PURE__*/_react.default.createElement(Element, { mutable: true, key: `form_${item.id}`, data: item, controls: controls, apiBaseUrl: this.props.apiBaseUrl || "https://api.dev.gateway.kusala.com.ng" }); } getSimpleElement(item) { const Element = _formElements.default[item.element]; return /*#__PURE__*/_react.default.createElement(Element, { mutable: true, key: `form_${item.id}`, data: item, apiBaseUrl: this.props.apiBaseUrl || "https://api.dev.gateway.kusala.com.ng" }); } getCustomElement(item) { const { intl } = this.props; if (!item.component || typeof item.component !== "function") { item.component = _registry.default.get(item.key); if (!item.component) { console.error(`${item.element} ${intl.formatMessage({ id: "message.was-not-registered" })}`); } } const inputProps = item.forwardRef ? { handleChange: this.handleChange, defaultValue: this._getDefaultValue(item), ref: c => this.inputs[item.field_name] = c } : {}; const liveAnswers = this._answersWithVariables(); return /*#__PURE__*/_react.default.createElement("div", { key: `form_${item.id}` }, /*#__PURE__*/_react.default.createElement(_ToggleFieldsContainer.default, { data: item, toggleVisibility: item?.toggleVisibility, fields: item?.visibilityFields || [], results: liveAnswers }, /*#__PURE__*/_react.default.createElement(_customElement.default, (0, _extends2.default)({ mutable: true, read_only: this.props.read_only || item?.readOnly, data: item, resultData: liveAnswers, apiBaseUrl: this.props.apiBaseUrl || "https://api.dev.gateway.kusala.com.ng" }, inputProps)))); } render() { const displayItems = this.props.display_short ? (this.props.data || []).filter(i => i?.alternateForm === true) : this.props.data || []; const liveAnswers = this._answersWithVariables(); const items = displayItems.filter(x => x && !x.parentId).map((item, indd) => { switch (item.element) { case "TextInput": case "DynamicInput": case "AmountInput": case "ArithmeticInput": case "DocumentSelect": case "CascadeSelect": case "DynamicMultiInput": case "DataGridInput": case "CustomSelect": case "CustomDatepicker": case "TableInput": case "EmailInput": case "PhoneNumber": case "NumberInput": case "PasswordInput": case "MultiFileUpload": case "AzureFileUpload": case "Base64FileViewer": case "TextArea": case "Dropdown": case "SmartAdaptorDropdown": case "DatePicker": case "RadioButtons": case "RadioButton": case "Rating": case "Tags": case "Range": return this.getInputElement(item); case "CustomElement": return this.getCustomElement(item); case "MultiColumnRow": return this.getContainerElement(item, _multiColumn.MultiColumnRow); case "ThreeColumnRow": return this.getContainerElement(item, _multiColumn.ThreeColumnRow); case "TwoColumnRow": return this.getContainerElement(item, _multiColumn.TwoColumnRow); case "FieldSet": return this.getContainerElement(item, _fieldset.FieldSet); case "Signature": return /*#__PURE__*/_react.default.createElement("div", { key: indd }, /*#__PURE__*/_react.default.createElement(_ToggleFieldsContainer.default, { data: item, toggleVisibility: item?.toggleVisibility, fields: item?.visibilityFields || [], results: liveAnswers }, /*#__PURE__*/_react.default.createElement(Signature, { key: `form_${item.id}`, ref: c => this.inputs[item.field_name] = c, read_only: this.props.read_only || item?.readOnly, mutable: true, data: item, defaultValue: this._getDefaultValue(item), apiBaseUrl: this.props.apiBaseUrl || "https://api.dev.gateway.kusala.com.ng" }))); case "Checkboxes": return /*#__PURE__*/_react.default.createElement("div", { key: indd }, /*#__PURE__*/_react.default.createElement(_ToggleFieldsContainer.default, { data: item, toggleVisibility: item?.toggleVisibility, fields: item?.visibilityFields || [], results: liveAnswers }, /*#__PURE__*/_react.default.createElement(Checkboxes, { key: `form_${item.id}`, ref: c => this.inputs[item.field_name] = c, read_only: this.props.read_only, handleChange: this.handleChange, mutable: true, data: item, defaultValue: this._optionsDefaultValue(item), apiBaseUrl: this.props.apiBaseUrl || "https://api.dev.gateway.kusala.com.ng" }))); case "Image": return /*#__PURE__*/_react.default.createElement("div", { key: indd }, /*#__PURE__*/_react.default.createElement(_ToggleFieldsContainer.default, { data: item, toggleVisibility: item?.toggleVisibility, fields: item?.visibilityFields || [], results: liveAnswers }, /*#__PURE__*/_react.default.createElement(Image, { key: `form_${item.id}`, ref: c => this.inputs[item.field_name] = c, handleChange: this.handleChange, mutable: true, data: item, defaultValue: this._getDefaultValue(item), apiBaseUrl: this.props.apiBaseUrl || "https://api.dev.gateway.kusala.com.ng" }))); case "Download": return /*#__PURE__*/_react.default.createElement("div", { key: indd }, /*#__PURE__*/_react.default.createElement(_ToggleFieldsContainer.default, { data: item, toggleVisibility: item?.toggleVisibility, fields: item?.visibilityFields || [], results: liveAnswers }, /*#__PURE__*/_react.default.createElement(Download, { key: `form_${item.id}`, download_path: this.props.download_path, mutable: true, data: item, apiBaseUrl: this.props.apiBaseUrl || "https://api.dev.gateway.kusala.com.ng" }))); case "Camera": return /*#__PURE__*/_react.default.createElement("div", { key: indd }, /*#__PURE__*/_react.default.createElement(_ToggleFieldsContainer.default, { data: item, toggleVisibility: item?.toggleVisibility, fields: item?.visibilityFields || [], results: liveAnswers }, /*#__PURE__*/_react.default.createElement(Camera, { key: `form_${item.id}`, ref: c => this.inputs[item.field_name] = c, read_only: this.props.read_only || item?.readOnly, mutable: true, data: item, defaultValue: this._getDefaultValue(item), apiBaseUrl: this.props.apiBaseUrl || "https://api.dev.gateway.kusala.com.ng" }))); case "FileUpload": return /*#__PURE__*/_react.default.createElement("div", { key: indd }, /*#__PURE__*/_react.default.createElement(_ToggleFieldsContainer.default, { data: item, toggleVisibility: item?.toggleVisibility, fields: item?.visibilityFields || [], results: liveAnswers }, /*#__PURE__*/_react.default.createElement(FileUpload, { key: `form_${item.id}`, ref: c => this.inputs[item.field_name] = c, read_only: this.props.read_only || item?.readOnly, mutable: true, data: item, defaultValue: this._getDefaultValue(item), apiBaseUrl: this.props.apiBaseUrl || "https://api.dev.gateway.kusala.com.ng" }))); default: return this.getSimpleElement(item); } }); return /*#__PURE__*/_react.default.createElement("div", null, /*#__PURE__*/_react.default.createElement(_formValidator.default, { emitter: this.emitter }), /*#__PURE__*/_react.default.createElement("div", { className: "react-form-builder-form" }, /*#__PURE__*/_react.default.createElement("form", { encType: "multipart/form-data", ref: c => this.form = c, action: this.props.form_action, onBlur: this.handleBlur, onChange: this.handleChange, onSubmit: this.handleSubmit, method: this.props.form_method }, this.props.authenticity_token && /*#__PURE__*/_react.default.createElement("div", { style: { display: "none" } }, /*#__PURE__*/_react.default.createElement("input", { name: "utf8", type: "hidden", value: "\u2713" }), /*#__PURE__*/_react.default.createElement("input", { name: "authenticity_token", type: "hidden", value: this.props.authenticity_token }), /*#__PURE__*/_react.default.createElement("input", { name: "task_id", type: "hidden", value: this.props.task_id })), /*#__PURE__*/_react.default.createElement("div", { id: "form-items", className: "" }, items), /*#__PURE__*/_react.default.createElement("div", { className: "mt-6 btn-toolbar" }, !this.props.hide_actions && this.handleRenderSubmit(), !this.props.hide_actions && this.props.back_action && this.handleRenderBack())))); } } ReactForm.defaultProps = { validateForCorrectness: false }; var _default = exports.default = (0, _reactIntl.injectIntl)(ReactForm);