diff --git a/packages/forms/src/UIForm/UIForm.component.js b/packages/forms/src/UIForm/UIForm.component.js index da91edad702..fff5defa5d2 100644 --- a/packages/forms/src/UIForm/UIForm.component.js +++ b/packages/forms/src/UIForm/UIForm.component.js @@ -1,25 +1,20 @@ import React, { PropTypes } from 'react'; -import { reduxForm } from 'redux-form'; import { merge } from 'talend-json-schema-form-core'; import Widget from './Widget'; -import { validate, validateAll } from './utils/validation'; -import { mutateValue } from './utils/properties'; const TRIGGER_AFTER = 'after'; -class UIForm extends React.Component { +export default class UIForm extends React.Component { constructor(props) { super(props); - const { jsonSchema, uiSchema, properties } = props.data; + const { jsonSchema, uiSchema } = props; this.state = { mergedSchema: merge(jsonSchema, uiSchema), - properties: { ...properties }, - validations: {}, }; console.log(this.state.mergedSchema) - this.consolidate = this.consolidate.bind(this); + this.onChange = this.onChange.bind(this); this.submit = this.submit.bind(this); } @@ -27,40 +22,16 @@ class UIForm extends React.Component { * Update the state with the new schema. * @param jsonSchema * @param uiSchema - * @param properties */ - componentWillReceiveProps({ jsonSchema, uiSchema, properties }) { - if (!jsonSchema || !uiSchema || !properties) { + componentWillReceiveProps({ jsonSchema, uiSchema }) { + if (!jsonSchema || !uiSchema) { return; } this.setState({ mergedSchema: merge(jsonSchema, uiSchema), - properties: { ...properties }, }); } - /** - * Consolidate form with the new value. - * - it updates the validation on the modified field. - * - it triggers onChange / onTrigger callbacks - * @param event The change event - * @param schema The schema of the changed field - * @param value The new field value - */ - consolidate(event, schema, value) { - this.setState( - (prevState) => { - const properties = mutateValue(prevState.properties, schema.key, value); - const validations = { - ...prevState.validations, - [schema.key]: validate(schema, value, properties, this.props.validation), - }; - return { properties, validations }; - }, - () => this.handleChangesCallbacks(schema, value) - ); - } - /** * Triggers the onTrigger and onChange if needed * - onChange : at each field change @@ -68,60 +39,31 @@ class UIForm extends React.Component { * @param schema The field schema * @param value The new value */ - handleChangesCallbacks(schema, value) { - const { onChange, onTrigger } = this.props; - - if (onChange) { - onChange({ - jsonSchema: this.props.data.jsonSchema, // original jsonSchema - uiSchema: this.props.data.uiSchema, // original uiSchema - properties: this.state.properties, // current properties values - }); - } + onChange(event, schema, value) { + const { onChange, onTrigger, properties } = this.props; + onChange(schema, value, properties); const { key, triggers } = schema; if (onTrigger && triggers && triggers.indexOf(TRIGGER_AFTER) !== -1) { onTrigger( - this.state.properties, // current properties values + this.props.properties, // current properties values key[key.length - 1], // field name value // field value ); } } - /** - * Triggers a validation and update state. - * @returns {boolean} true if the form is valid, false otherwise - */ - isValid() { - const validations = validateAll( - this.state.mergedSchema, - this.state.properties, - this.props.validation - ); - - const isValid = Object.keys(validations).every(key => validations[key].valid); - if (!isValid) { - this.setState({ validations }); - } - return isValid; - } - /** * Triggers submit callback if form is valid * @param event the submit event */ submit(event) { event.preventDefault(); - if (this.isValid()) { - this.props.onSubmit(event, this.state.properties); - } + this.props.onSubmit(event, this.state.mergedSchema, this.props.properties); } render() { - const { formName } = this.props; - const { properties, validations } = this.state; - + const { errors, formName, properties } = this.props; return (
{ @@ -129,10 +71,10 @@ class UIForm extends React.Component { )) } @@ -144,19 +86,14 @@ class UIForm extends React.Component { if (process.env.NODE_ENV !== 'production') { UIForm.propTypes = { - /** Form schema configuration */ - data: PropTypes.shape({ - /** Json schema that specify the data model */ - jsonSchema: PropTypes.object, - /** UI schema that specify how to render the fields */ - uiSchema: PropTypes.array, - /** Form fields values. Note that it should contains @definitionName for triggers. */ - properties: PropTypes.object, - }), + /** The forms errors { [fieldKey]: errorMessage } */ + errors: PropTypes.object, // eslint-disable-line react/forbid-prop-types /** The form name that will be used to create ids */ formName: PropTypes.string, - /** The change callback. It takes */ - onChange: PropTypes.func, + /** Json schema that specify the data model */ + jsonSchema: PropTypes.object, // eslint-disable-line react/forbid-prop-types + /** The change callback */ + onChange: PropTypes.func.isRequired, /** Form submit callback */ onSubmit: PropTypes.func.isRequired, /** @@ -165,16 +102,9 @@ if (process.env.NODE_ENV !== 'production') { * This is executed on changes on fields with uiSchema > triggers : ['after'] */ onTrigger: PropTypes.func, - /** - * Custom validation function. - * Prototype: function validation(properties, fieldName, value) - * Return format : { valid: true|false, error: { message: 'my validation message' } } - * This is triggered on fields that has their uiSchema > customValidation : true - */ - validation: PropTypes.func, + /** Form fields values. Note that it should contains @definitionName for triggers. */ + properties: PropTypes.object, // eslint-disable-line react/forbid-prop-types + /** UI schema that specify how to render the fields */ + uiSchema: PropTypes.array, // eslint-disable-line react/forbid-prop-types }; } - -export default reduxForm({ - form: 'form', // a unique name for this form -})(UIForm); diff --git a/packages/forms/src/UIForm/UIForm.container.js b/packages/forms/src/UIForm/UIForm.container.js new file mode 100644 index 00000000000..c715f4338bb --- /dev/null +++ b/packages/forms/src/UIForm/UIForm.container.js @@ -0,0 +1,135 @@ +import React, { PropTypes } from 'react'; +import UIFormComponent from './UIForm.component'; + +import { modelReducer, validationReducer } from './reducers'; +import { mutateValue, validateAll } from './actions'; + +export default class UIForm extends React.Component { + constructor(props) { + super(props); + this.state = { + properties: { ...props.data.properties }, + errors: {}, + }; + + this.onChange = this.onChange.bind(this); + this.onSubmit = this.onSubmit.bind(this); + } + + /** + * Update the properties. + */ + componentWillReceiveProps({ properties }) { + if (!properties) { + return; + } + + this.setState({ + properties: { ...properties }, + }); + } + + /** + * Update the model and validation + * If onChange is provided, it is triggered + * @param schema The schema + * @param value The new value + * @param properties The values + */ + onChange(schema, value, properties) { + const action = mutateValue(schema, value, properties, this.props.validation); + this.setState( + { + properties: modelReducer(this.state.properties, action), + errors: validationReducer(this.state.errors, action), + }, + () => { + if (this.props.onChange) { + this.props.onChange({ + jsonSchema: this.props.data.jsonSchema, + uiSchema: this.props.data.uiSchema, + properties: this.state.properties, + }); + } + } + ); + } + + /** + * Triggers submit callback if form is valid + * @param event the submit event + * @param schema the schema + * @param properties the properties values + */ + onSubmit(event, schema, properties) { + event.preventDefault(); + if (this.isValid(schema, properties)) { + this.props.onSubmit(event, properties); + } + } + + /** + * Triggers a validation and update state. + * @returns {boolean} true if the form is valid, false otherwise + */ + isValid(schema, properties) { + const action = validateAll(schema, properties, this.props.validation); + const errors = validationReducer(this.state.errors, action); + const isValid = !Object.keys(errors).length; + + if (!isValid) { + this.setState({ errors }); + } + return isValid; + } + + render() { + const { data, ...restProps } = this.props; + const { properties, errors } = this.state; + + return ( + + ); + } +} + +if (process.env.NODE_ENV !== 'production') { + UIForm.propTypes = { + /** Form schema configuration */ + data: PropTypes.shape({ + /** Json schema that specify the data model */ + jsonSchema: PropTypes.object, + /** UI schema that specify how to render the fields */ + uiSchema: PropTypes.array, + /** Form fields values. Note that it should contains @definitionName for triggers. */ + properties: PropTypes.object, + }), + /** The form name that will be used to create ids */ + formName: PropTypes.string, + /** The change callback. It takes */ + onChange: PropTypes.func, + /** Form submit callback */ + onSubmit: PropTypes.func.isRequired, + /** + * Tigger > after callback. + * Prototype: function onTrigger(properties, fieldName, value) + * This is executed on changes on fields with uiSchema > triggers : ['after'] + */ + onTrigger: PropTypes.func, + /** + * Custom validation function. + * Prototype: function validation(properties, fieldName, value) + * Return format : { valid: true|false, error: { message: 'my validation message' } } + * This is triggered on fields that has their uiSchema > customValidation : true + */ + validation: PropTypes.func, + }; +} diff --git a/packages/forms/src/UIForm/Widget/Widget.component.js b/packages/forms/src/UIForm/Widget/Widget.component.js index 15cdda6409c..f6751b21629 100644 --- a/packages/forms/src/UIForm/Widget/Widget.component.js +++ b/packages/forms/src/UIForm/Widget/Widget.component.js @@ -4,11 +4,11 @@ import { sfPath } from 'talend-json-schema-form-core'; import widgets from '../utils/widgets'; import { getValue } from '../utils/properties'; -export default function Widget({ formName, onChange, properties, schema, validations }) { +export default function Widget({ errors, formName, onChange, properties, schema }) { const { key, type, validationMessage } = schema; const id = sfPath.name(key, '-', formName); - const { error, valid } = validations[key] || {}; - const errorMessage = validationMessage || (error && error.message); + const error = errors[key]; + const errorMessage = validationMessage || error; const WidgetImpl = widgets[type]; return WidgetImpl ? ( @@ -17,11 +17,11 @@ export default function Widget({ formName, onChange, properties, schema, validat key={id} errorMessage={errorMessage} formName={formName} - isValid={valid} + isValid={!error} onChange={onChange} properties={properties} schema={schema} - validations={validations} + errors={errors} value={getValue(properties, key)} /> ) : null; @@ -29,6 +29,7 @@ export default function Widget({ formName, onChange, properties, schema, validat if (process.env.NODE_ENV !== 'production') { Widget.propTypes = { + errors: PropTypes.object, // eslint-disable-line react/forbid-prop-types formName: PropTypes.string, onChange: PropTypes.func, schema: PropTypes.shape({ @@ -37,6 +38,5 @@ if (process.env.NODE_ENV !== 'production') { validationMessage: PropTypes.string, }).isRequired, properties: PropTypes.object, // eslint-disable-line react/forbid-prop-types - validations: PropTypes.object, // eslint-disable-line react/forbid-prop-types }; } diff --git a/packages/forms/src/UIForm/actions/constants.js b/packages/forms/src/UIForm/actions/constants.js new file mode 100644 index 00000000000..fbcfb784cb5 --- /dev/null +++ b/packages/forms/src/UIForm/actions/constants.js @@ -0,0 +1,2 @@ +export const MUTATE_VALUE = 'MUTATE_VALUE'; +export const VALIDATE_ALL = 'VALIDATE_ALL'; diff --git a/packages/forms/src/UIForm/actions/index.js b/packages/forms/src/UIForm/actions/index.js new file mode 100644 index 00000000000..b1760ba1ed8 --- /dev/null +++ b/packages/forms/src/UIForm/actions/index.js @@ -0,0 +1,3 @@ +export { mutateValue } from './model.actions'; +export { validateAll } from './validation.actions'; +export * from './constants'; diff --git a/packages/forms/src/UIForm/actions/model.actions.js b/packages/forms/src/UIForm/actions/model.actions.js new file mode 100644 index 00000000000..c1d895687eb --- /dev/null +++ b/packages/forms/src/UIForm/actions/model.actions.js @@ -0,0 +1,11 @@ +import { MUTATE_VALUE } from './constants'; + +export function mutateValue(schema, value, properties, customValidationFn) { + return { + type: MUTATE_VALUE, + customValidationFn, + properties, + schema, + value, + }; +} diff --git a/packages/forms/src/UIForm/actions/validation.actions.js b/packages/forms/src/UIForm/actions/validation.actions.js new file mode 100644 index 00000000000..03f2f226b15 --- /dev/null +++ b/packages/forms/src/UIForm/actions/validation.actions.js @@ -0,0 +1,10 @@ +import { VALIDATE_ALL } from './constants'; + +export function validateAll(schema, properties, customValidationFn) { + return { + type: VALIDATE_ALL, + schema, + properties, + customValidationFn, + }; +} diff --git a/packages/forms/src/UIForm/fieldsets/Tabs.js b/packages/forms/src/UIForm/fieldsets/Tabs.js index ed5aeba565c..6a35bc5e1df 100644 --- a/packages/forms/src/UIForm/fieldsets/Tabs.js +++ b/packages/forms/src/UIForm/fieldsets/Tabs.js @@ -2,7 +2,7 @@ import React, { PropTypes } from 'react'; import { Tabs as RBTabs, Tab as RBTab } from 'react-bootstrap'; import Fieldset from './Fieldset'; -import { isValid } from '../utils/validation'; +import isValid from '../utils/validation'; import theme from './Tabs.scss'; export default function Tabs(props) { @@ -12,7 +12,7 @@ export default function Tabs(props) { return ( {tabs.map((tabSchema, index) => { - const tabClassName = isValid(tabSchema, restProps.validations) ? + const tabClassName = isValid(tabSchema, restProps.errors) ? null : theme['has-error']; return ( @@ -32,6 +32,7 @@ export default function Tabs(props) { if (process.env.NODE_ENV !== 'production') { Tabs.propTypes = { + errors: PropTypes.object, // eslint-disable-line react/forbid-prop-types schema: PropTypes.shape({ items: PropTypes.arrayOf( React.PropTypes.shape({ diff --git a/packages/forms/src/UIForm/index.js b/packages/forms/src/UIForm/index.js index 370245b7ae7..38ee3ef5fbd 100644 --- a/packages/forms/src/UIForm/index.js +++ b/packages/forms/src/UIForm/index.js @@ -1,3 +1,8 @@ -import UIForm from './UIForm.component'; +import UIForm from './UIForm.container'; +import { modelReducer, validationReducer } from './reducers'; +export const formReducers = { + model: modelReducer, + errors: validationReducer, +}; export default UIForm; diff --git a/packages/forms/src/UIForm/reducers/index.js b/packages/forms/src/UIForm/reducers/index.js new file mode 100644 index 00000000000..e71c9a3a814 --- /dev/null +++ b/packages/forms/src/UIForm/reducers/index.js @@ -0,0 +1,4 @@ +import modelReducer from './model.reducer'; +import validationReducer from './validations.reducer'; + +export { modelReducer, validationReducer }; diff --git a/packages/forms/src/UIForm/reducers/model.reducer.js b/packages/forms/src/UIForm/reducers/model.reducer.js new file mode 100644 index 00000000000..3f6a5577d21 --- /dev/null +++ b/packages/forms/src/UIForm/reducers/model.reducer.js @@ -0,0 +1,35 @@ +import { MUTATE_VALUE } from '../actions'; + +/** + * Mutate the properties, setting the value in the path identified by key + * @param {object} properties The original properties store + * @param {array} key The key chain (array of strings) to identify the path + * @param {any} value The value to set + * @returns {object} The new mutated properties store. + */ +function mutateValue(properties, key, value) { + if (!key.length) { + return value; + } + + const nextKey = key[0]; + const restKeys = key.slice(1); + return { + ...properties, + [nextKey]: mutateValue(properties[nextKey], restKeys, value), + }; +} + +/** + * Form model change reducer + * @param state The model + * @param action The action to perform + */ +export default function modelConsolidation(state = {}, action) { + switch (action.type) { + case MUTATE_VALUE: + return mutateValue(state, action.schema.key, action.value); + default: + return state; + } +} diff --git a/packages/forms/src/UIForm/reducers/validations.reducer.js b/packages/forms/src/UIForm/reducers/validations.reducer.js new file mode 100644 index 00000000000..d780cfab039 --- /dev/null +++ b/packages/forms/src/UIForm/reducers/validations.reducer.js @@ -0,0 +1,92 @@ +import { validate } from 'talend-json-schema-form-core'; +import { MUTATE_VALUE, VALIDATE_ALL } from '../actions'; +import { getValue } from '../utils/properties'; + +/** + * Validate values. + * @param schema The merged schema. + * @param value The value. + * @param properties The values. + * @param customValidationFn A custom validation function + * that is applied on schema.customValidation = true + * @returns {object} The validation result. + */ +function validateValue(schema, value, properties, customValidationFn) { + const staticResult = validate(schema, value); + if (staticResult.valid && schema.customValidation && customValidationFn) { + return customValidationFn(properties, schema, value); + } + return staticResult.valid ? null : staticResult.error.message; +} + +/** + * Validate values. + * @param mergedSchema The merged schema array. + * @param properties The values. + * @param customValidationFn A custom validation function + * that is applied on schema.customValidation = true + * @returns {object} The validation result by field. + */ +function validateAll(mergedSchema, properties, customValidationFn) { + const results = {}; + mergedSchema.forEach((schema) => { + const { key, items } = schema; + if (key) { + const value = getValue(properties, key); + const error = validateValue(schema, value, properties, customValidationFn); + if (error) { + results[key] = error; + } + } + if (items) { + const subResults = validateAll(items, properties); + Object.assign(results, subResults); + } + }); + return results; +} + +/** + * Omit a property from an object + * @param errors The object + * @param key The key to omit + */ +function omit(errors, key) { + if (!key) { + return errors; + } + const result = {}; + Object.keys(errors) + .filter(nextKey => nextKey !== key) + .forEach((nextKey) => { + result[nextKey] = errors[nextKey]; + }); + return result; +} + +/** + * Form validations reducer + * @param state The errors { propertyKey: errorMessage } + * @param action The action to perform + */ +export default function validations(state = {}, action) { + switch (action.type) { + case MUTATE_VALUE: { + const { schema, value, properties, customValidationFn } = action; + const error = validateValue(schema, value, properties, customValidationFn); + if (error) { + return { + ...state, + [action.schema.key]: error, + }; + } + return omit(state, action.schema.key.toString()); + } + case VALIDATE_ALL: { + const { schema, properties, customValidationFn } = action; + return validateAll(schema, properties, customValidationFn); + } + default: + return state; + } +} diff --git a/packages/forms/src/UIForm/utils/properties.js b/packages/forms/src/UIForm/utils/properties.js index 0dfd46622f0..64ebc758345 100644 --- a/packages/forms/src/UIForm/utils/properties.js +++ b/packages/forms/src/UIForm/utils/properties.js @@ -13,23 +13,3 @@ export function getValue(properties, key) { properties ); } - -/** - * Mutate the properties, setting the value in the path identified by key - * @param {object} properties The original properties store - * @param {array} key The key chain (array of strings) to identify the path - * @param {any} value The value to set - * @returns {object} The new mutated properties store. - */ -export function mutateValue(properties, key, value) { - if (!key.length) { - return value; - } - - const nextKey = key[0]; - const restKeys = key.slice(1); - return { - ...properties, - [nextKey]: mutateValue(properties[nextKey], restKeys, value), - }; -} diff --git a/packages/forms/src/UIForm/utils/validation.js b/packages/forms/src/UIForm/utils/validation.js index 443d075463f..0dfb6f97c04 100644 --- a/packages/forms/src/UIForm/utils/validation.js +++ b/packages/forms/src/UIForm/utils/validation.js @@ -1,66 +1,21 @@ -import { validate as staticValidate } from 'talend-json-schema-form-core'; - -import { getValue } from './properties'; - -/** - * Validate values. - * @param schema The merged schema. - * @param value The value. - * @param properties The values. - * @param customValidationFn A custom validation function - * that is applied on schema.customValidation = true - * @returns {object} The validation result. - */ -export function validate(schema, value, properties, customValidationFn) { - const staticResult = staticValidate(schema, value); - if (staticResult.valid && schema.customValidation && customValidationFn) { - return customValidationFn(properties, schema, value); - } - return staticResult; -} - -/** - * Validate values. - * @param mergedSchema The merged schema array. - * @param properties The values. - * @param customValidationFn A custom validation function - * that is applied on schema.customValidation = true - * @returns {object} The validation result by field. - */ -export function validateAll(mergedSchema, properties, customValidationFn) { - const validations = {}; - mergedSchema.forEach((schema) => { - const { key, items } = schema; - if (key) { - const value = getValue(properties, key); - validations[key] = validate(schema, value, properties, customValidationFn); - } - if (items) { - const subValidations = validateAll(items, properties); - Object.assign(validations, subValidations); - } - }); - return validations; -} - /** * Check if a schema value is valid. * It is invalid if : - * - the schema is an invalid field (validations[key] = { valid: false }) + * - the schema is an invalid field (errors[key] is falsy) * - the schema has items (ex: fieldset, tabs, ...), and at least one of them is invalid * @param schema The schema - * @param validations The validations results + * @param errors The errors * @returns {boolean} true if it is invalid, false otherwise */ -export function isValid(schema, validations) { +export default function isValid(schema, errors) { const { key, items } = schema; - if (key && validations[key] && !validations[key].valid) { + if (key && errors[key]) { return false; } if (items) { for (const itemSchema of items) { - if (!isValid(itemSchema, validations)) { + if (!isValid(itemSchema, errors)) { return false; } } diff --git a/packages/forms/stories/index.js b/packages/forms/stories/index.js index d1e2a456509..e9abc810f6c 100644 --- a/packages/forms/stories/index.js +++ b/packages/forms/stories/index.js @@ -67,12 +67,8 @@ sampleFilenames onSubmit={action('Submit')} validation={(properties, schema, value) => { action('customValidation')(properties, schema, value); - return { - valid: value.length < 5, - error: { - message: 'Custom validation : The value should be less than 5 chars', - }, - }; + return value.length >= 5 && + 'Custom validation : The value should be less than 5 chars'; }} />