UNPKG

@talend/react-forms

Version:

React forms library based on json schema form.

449 lines (441 loc) 15.1 kB
function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); } function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; } function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; } function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } import { Component } from 'react'; import { withTranslation } from 'react-i18next'; import PropTypes from 'prop-types'; import { Form } from '@talend/design-system'; import { tv4 } from '@talend/json-schema-form-core'; import { I18N_DOMAIN_FORMS } from '../constants'; import '../translate'; import { WidgetContext } from './context'; import customFormats from './customFormats'; import Buttons from './fields/Button/Buttons.component'; import { DefaultFormTemplate, TextModeFormTemplate } from './FormTemplate'; import getLanguage from './lang'; import merge from './merge'; import { addError, removeError } from './utils/errors'; import { getValue, mutateValue } from './utils/properties'; import { formPropTypes } from './utils/propTypes'; import { validateAll, validateSingle } from './utils/validation'; import widgets from './utils/widgets'; import Widget from './Widget'; import theme from './UIForm.module.scss'; import { jsx as _jsx } from "react/jsx-runtime"; export class UIFormComponent extends Component { constructor(props) { super(props); const { jsonSchema, uiSchema } = props; const state = {}; if (Object.keys(jsonSchema).length) { _extends(state, merge(jsonSchema, uiSchema)); } state.widgets = { ...state.widgets, ...props.widgets }; this.state = state; this.onChange = this.onChange.bind(this); this.onFinish = this.onFinish.bind(this); this.onSubmit = this.onSubmit.bind(this); this.onTrigger = this.onTrigger.bind(this); this.onActionClick = this.onActionClick.bind(this); this.focusFirstError = this.focusFirstError.bind(this); this.setFormRef = this.setFormRef.bind(this); // control the tv4 language here. const language = getLanguage(props.t); const languageId = props.id || '@talend'; if (props.language != null) { _extends(language, props.language); // Force update of language by id even if already set tv4.addLanguage(languageId, language); tv4.language(languageId); } if (!tv4.language(languageId)) { tv4.addLanguage(languageId, language); tv4.language(languageId); // set it } const allFormats = _extends(customFormats(props.t), props.customFormats); tv4.addFormat(allFormats); } /** * Update the state with the new schema. * @param jsonSchema * @param uiSchema */ componentDidUpdate(prevProps) { if (!this.props.jsonSchema || !this.props.uiSchema || this.props.jsonSchema === prevProps.jsonSchema && this.props.uiSchema === prevProps.uiSchema) { return; } const { jsonSchema, uiSchema } = this.props; if (Object.keys(jsonSchema).length) { const merged = merge(jsonSchema, uiSchema); this.setState({ ...merged, widgets: { ...merged.widgets, ...this.props.widgets } }); } } /** * Fire onChange callback while interacting with form fields * @param event The event that triggered the callback * @param schema The payload field schema * @param value The payload new value */ onChange(event, { schema, value }) { const newProperties = mutateValue(this.props.properties, schema, value); this.props.onChange(event, { schema, value, oldProperties: this.props.properties, properties: newProperties, formData: newProperties }); } /** * Perform validation and triggers when user has finished to change a value * @param event The event that triggered the callback * @param schema The payload field schema * @param value The new value, provided if not already taken into account * @param deepValidation Validate the subItems * Most of the time, this value is not provided. It will be taken from props.properties * This allows to perform triggers/validation while changing a value * (ex: add an element in array) * @param widgetChangeErrors Change errors hook, allows any widget to manipulate the errors map * (ex: shift the errors in array elements on remove) */ onFinish(event, { schema, value }, { deepValidation = false, widgetChangeErrors } = {}) { // get property value let newValue; if (value !== undefined) { newValue = value; } else { newValue = getValue(this.props.properties, schema); } // validate value. This validation can be deep if schema is an object or an array const widgetErrors = validateSingle(schema, newValue, this.props.properties, this.props.customValidation, deepValidation, event); const hasErrors = Object.values(widgetErrors).find(Boolean); const { t } = this.props; // update errors map let errors = Object.entries(widgetErrors).reduce((accu, [errorKey, errorValue]) => { const errorSchema = { key: errorKey }; let errorMsg = errorValue; if (errorValue && errorValue.startsWith('CUSTOM_ERROR')) { errorMsg = t(errorValue, { defaultValue: getLanguage(t)[errorValue] }); } return errorMsg ? addError(accu, errorSchema, errorMsg) : removeError(accu, errorSchema); }, this.props.errors); // widget error modifier if (widgetChangeErrors) { errors = widgetChangeErrors(errors); } // commit errors this.props.setErrors(event, errors); if (!hasErrors && schema.triggers && schema.triggers.length) { let formData = this.props.properties; if (value !== undefined) { formData = mutateValue(formData, schema, value); } let propertyName = schema.key.join('.'); if (this.props.moz) { schema.key.forEach((key, index) => { if (index !== schema.key.length - 1) { formData = formData[key]; } }); propertyName = schema.key[schema.key.length - 1]; this.onTrigger(event, { formData, formId: this.props.id, propertyName, value }); } else { const trigger = schema.triggers.find(triggerItem => triggerItem.onEvent === undefined); if (trigger) { this.onTrigger(event, { trigger, schema, properties: formData, errors, value }); } } } } /** * Triggers an onTrigger callback that is allowed to modify the form * @param event The event that triggered the callback * @param payload The trigger payload * trigger The type of trigger * schema The field schema */ onTrigger(event, payload) { const { onTrigger } = this.props; if (!onTrigger) { return null; } if (this.props.moz) { return onTrigger(payload.formData, payload.formId, payload.propertyName, payload.value); } if (!payload.trigger) { throw new Error('onTrigger payload do not have required trigger property'); } return onTrigger(event, { properties: this.props.properties, errors: this.props.errors, ...payload }); } onActionClick(actionOnClick) { if (typeof actionOnClick === 'function') { return (event, data) => actionOnClick(event, { ...data, formData: this.props.properties, properties: this.props.properties }); } return () => {}; } /** * Triggers submit callback if form is valid * @param event the submit event */ onSubmit(event) { if (this.props.onSubmit) { event.preventDefault(); } const { mergedSchema } = this.state; const { properties, customValidation } = this.props; const newErrors = validateAll(mergedSchema, properties, customValidation); Object.entries(this.props.errors).filter(entry => entry[0] in newErrors).reduce((accu, [key, value]) => { // eslint-disable-next-line no-param-reassign accu[key] = value; return accu; }, newErrors); const errors = Object.entries(newErrors).filter(entry => entry[1]).reduce((accu, [key, value]) => { // eslint-disable-next-line no-param-reassign accu[key] = value; return accu; }, {}); const isValid = !Object.keys(errors).length; this.props.setErrors(event, errors, !isValid ? this.focusFirstError : undefined); if (this.props.onSubmit && isValid) { if (this.props.moz) { this.props.onSubmit(null, { formData: properties }); } else { this.props.onSubmit(event, properties, mergedSchema); } } return isValid; } setFormRef(element) { this.formRef = element; } focusFirstError() { if (!this.formRef) { return; } const elementWithError = this.formRef.querySelector('[aria-invalid="true"]'); if (elementWithError) { elementWithError.focus(); } } render() { const { onSubmitEnter, onSubmitLeave, properties } = this.props; let actions = this.props.actions || [{ bsStyle: 'primary', label: 'Submit', type: 'submit', widget: 'button', position: 'right' }]; if (!this.state.mergedSchema) { return null; } if (onSubmitEnter) { actions = actions.map(action => { if (action.type === 'submit') { return { ...action, onMouseEnter: event => onSubmitEnter(event, properties), onMouseLeave: onSubmitLeave }; } return action; }); } const formTemplate = this.props.displayMode === 'text' ? TextModeFormTemplate : DefaultFormTemplate; const widgetsRenderer = () => this.state.mergedSchema.map((nextSchema, index) => /*#__PURE__*/_jsx(Widget, { id: this.props.id, idSeparator: this.props.idSeparator, onChange: this.onChange, onFinish: this.onFinish, onTrigger: this.onTrigger, schema: nextSchema, properties: this.props.properties, errors: this.props.errors, templates: this.props.templates, widgets: this.state.widgets, displayMode: this.props.displayMode, updating: this.props.updating }, index)); const buttonsRenderer = () => { if (actions.length === 0) { return null; } const buttons = /*#__PURE__*/_jsx(Buttons, { id: `${this.props.id}-actions`, onTrigger: this.onTrigger, schema: { items: actions }, onClick: this.onActionClick, getComponent: this.props.getComponent }); if (this.props.anchorButtonsToFooter) { return /*#__PURE__*/_jsx("div", { "data-drawer-absolute-footer-buttons": true, className: theme['drawer-absolute-footer-buttons'], children: buttons }); } return buttons; }; const Element = this.props.as; return /*#__PURE__*/_jsx(Element, { acceptCharset: this.props.acceptCharset, action: this.props.action, autoComplete: this.props.autoComplete, encType: this.props.encType, id: this.props.id, method: this.props.method, name: this.props.name, noValidate: this.props.as === Form ? this.props.noHtml5Validate : undefined, onReset: this.props.onReset, onSubmit: this.onSubmit, target: this.props.target, ref: this.setFormRef, children: /*#__PURE__*/_jsx(WidgetContext.Provider, { value: { ...widgets, ...this.state.widgets }, children: formTemplate({ children: this.props.children, widgetsRenderer, buttonsRenderer }) }) }); } } _defineProperty(UIFormComponent, "displayName", 'TalendUIForm'); const I18NUIForm = withTranslation(I18N_DOMAIN_FORMS)(UIFormComponent); if (process.env.NODE_ENV !== 'production') { I18NUIForm.propTypes = { ...formPropTypes, /** Form definition: Json schema that specify the data model */ jsonSchema: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types /** Form definition: UI schema that specify how to render the fields */ uiSchema: PropTypes.array.isRequired, // eslint-disable-line react/forbid-prop-types /** * Form definition: Form fields values. * Note that it should contains @definitionName for triggers. */ properties: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types /** Form definition: The forms errors { [fieldKey]: errorMessage } */ errors: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types /** * Actions buttons to display at the bottom of the form. * If not provided, a single submit button is displayed. */ actions: PropTypes.arrayOf(Buttons.propTypes.schema), /** * Tag used to render the form (defaults to "form") */ as: PropTypes.string, /** * User callback: Custom validation function. * Prototype: function customValidation(schema, value, properties) * Return format : errorMessage String | falsy * This is triggered on fields that has their uiSchema > customValidation : true */ customValidation: PropTypes.func, /* * Form definition: Custom formats */ customFormats: PropTypes.object, /** * User callback: Trigger * Prototype: function onTrigger(event, { trigger, schema, properties }) */ onTrigger: PropTypes.func, /** Custom templates */ templates: PropTypes.object, /** Custom widgets */ widgets: PropTypes.object, /** Display mode: example 'text' */ displayMode: PropTypes.string, /** State management impl: The change callback */ onChange: PropTypes.func.isRequired, /** State management impl: Set All fields validations errors */ setErrors: PropTypes.func, getComponent: PropTypes.func, onSubmitEnter: PropTypes.func, onSubmitLeave: PropTypes.func }; UIFormComponent.propTypes = I18NUIForm.propTypes; } I18NUIForm.defaultProps = { ...I18NUIForm.defaultProps, noHtml5Validate: true, properties: {}, as: Form }; UIFormComponent.defaultProps = I18NUIForm.defaultProps; export default I18NUIForm; //# sourceMappingURL=UIForm.component.js.map