how can I show customized error messaged from server side validation in React Admin package?
Asked Answered
E

4

12

Is there any way to perform server side form validation using https://github.com/marmelab/react-admin package?

Here's the code for AdminCreate Component. It sends create request to api. Api returns validation error with status code 422 or status code 200 if everything is ok.

    export class AdminCreate extends Component {
  render() {
    return <Create {...this.props}>
        <SimpleForm>
          <TextInput source="name"  type="text" />
          <TextInput source="email" type="email"/>
          <TextInput source="password" type="password"/>
          <TextInput source="password_confirmation" type="password"/>
          <TextInput source="phone" type="tel"/>    
        </SimpleForm>
    </Create>;

}
}

So the question is, how can I show errors for each field separately from error object sent from server? Here is the example of error object:

{
errors: {name: "The name is required", email: "The email is required"},
message: "invalid data"
}

Thank you in advance!

class SimpleForm extends Component {
    handleSubmitWithRedirect = (redirect = this.props.redirect) =>
        this.props.handleSubmit(data => {
          dataProvider(CREATE, 'admins', { data: { ...data } }).catch(e => {
            throw new SubmissionError(e.body.errors)
          }).then(/* Here must be redirection logic i think  */);
        });

    render() {
        const {
            basePath,
            children,
            classes = {},
            className,
            invalid,
            pristine,
            record,
            resource,
            submitOnEnter,
            toolbar,
            version,
            ...rest
        } = this.props;

        return (
            <form
                className={classnames('simple-form', className)}
                {...sanitizeRestProps(rest)}
            >
                <div className={classes.form} key={version}>
                    {Children.map(children, input => (
                        <FormInput
                            basePath={basePath}
                            input={input}
                            record={record}
                            resource={resource}
                        />
                    ))}
                </div>
                {toolbar &&
                    React.cloneElement(toolbar, {
                        handleSubmitWithRedirect: this.handleSubmitWithRedirect,
                        invalid,
                        pristine,
                        submitOnEnter,
                    })}
            </form>
        );
    }
}

Now i have following code, and it's showing validation errors. But the problem is, i can't perform redirection after success. Any thoughts?

Emlyn answered 8/5, 2018 at 15:24 Comment(7)
I'm looking for a way to do this too. Will be back when I found it.Neopythagoreanism
I will be gratefulEmlyn
If I'm seeing this correctly your solution was create a custom <SimpleForm> that uses the dataProvider directly to catch the error and show it?Chipman
I found the best solution so far: https://mcmap.net/q/1010632/-show-server-side-validation-errors-after-failed-form-submitUprear
@llioor, that solution uses redux-form. Did you manage to make it work for react-final-form that react-admin now uses?Zsolway
@Zsolway Yes, it was SUPER HARD! I will try to upload it here soon.Uprear
@Uprear that would be a life saver! I also posted a new question since this one is about react-admin 2.x #60098115Zsolway
N
6

If you're using SimpleForm, you can use asyncValidate together with asyncBlurFields as suggested in a comment in issue 97. I didn't use SimpleForm, so this is all I can tell you about that.

I've used a simple form. And you can use server-side validation there as well. Here's how I've done it. A complete and working example.

import React from 'react';
import PropTypes from 'prop-types';
import { Field, propTypes, reduxForm, SubmissionError } from 'redux-form';
import { connect } from 'react-redux';
import compose from 'recompose/compose';
import { CardActions } from 'material-ui/Card';
import Button from 'material-ui/Button';
import TextField from 'material-ui/TextField';
import { CircularProgress } from 'material-ui/Progress';
import { CREATE, translate } from 'ra-core';
import { dataProvider } from '../../providers'; // <-- Make sure to import yours!

const renderInput = ({
    meta: { touched, error } = {},
    input: { ...inputProps },
    ...props
}) => (
    <TextField
        error={!!(touched && error)}
        helperText={touched && error}
        {...inputProps}
        {...props}
        fullWidth
    />
);

/**
 * Inspired by
 * - https://redux-form.com/6.4.3/examples/submitvalidation/
 * - https://marmelab.com/react-admin/Actions.html#using-a-data-provider-instead-of-fetch
 */
const submit = data =>
    dataProvider(CREATE, 'things', { data: { ...data } }).catch(e => {
        const payLoadKeys = Object.keys(data);
        const errorKey = payLoadKeys.length === 1 ? payLoadKeys[0] : '_error';
        // Here I set the error either on the key by the name of the field
        // if there was just 1 field in the payload.
        // The `Field` with the same `name` in the `form` wil have
        // the `helperText` shown.
        // When multiple fields where present in the payload, the error  message is set on the _error key, making the general error visible.
        const errorObject = {
            [errorKey]: e.message,
        };
        throw new SubmissionError(errorObject);
    });

const MyForm = ({ isLoading, handleSubmit, error, translate }) => (
    <form onSubmit={handleSubmit(submit)}>
        <div>
            <div>
                <Field
                    name="email"
                    component={renderInput}
                    label="Email"
                    disabled={isLoading}
                />
            </div>
        </div>
        <CardActions>
            <Button
                variant="raised"
                type="submit"
                color="primary"
                disabled={isLoading}
            >
                {isLoading && <CircularProgress size={25} thickness={2} />}
                Signin
            </Button>
            {error && <strong>General error: {translate(error)}</strong>}
        </CardActions>
    </form>
);
MyForm.propTypes = {
    ...propTypes,
    classes: PropTypes.object,
    redirectTo: PropTypes.string,
};

const mapStateToProps = state => ({ isLoading: state.admin.loading > 0 });

const enhance = compose(
    translate,
    connect(mapStateToProps),
    reduxForm({
        form: 'aFormName',
        validate: (values, props) => {
            const errors = {};
            const { translate } = props;
            if (!values.email)
                errors.email = translate('ra.validation.required');
            return errors;
        },
    })
);

export default enhance(MyForm);

If the code needs further explanation, drop a comment below and I'll try to elaborate.

I hoped to be able to do the action of the REST-request by dispatching an action with onSuccess and onFailure side effects as described here, but I couldn't get that to work together with SubmissionError.

Neopythagoreanism answered 10/5, 2018 at 16:12 Comment(4)
Thanks a lot! You saved my day :)Emlyn
Updated question, please check itEmlyn
You should really post a new question. It improves the quality of your core question as well as the value of the answers for developers working with react-admin.Neopythagoreanism
I found the best solution so far: https://mcmap.net/q/1010632/-show-server-side-validation-errors-after-failed-form-submitUprear
N
3

Here one more solution from official repo. https://github.com/marmelab/react-admin/pull/871 You need to import HttpError(message, status, body) in DataProvider and throw it. Then in errorSaga parse body to redux-form structure. That's it. Enjoy.

Niggard answered 19/7, 2018 at 15:14 Comment(0)
G
2

Found a working solution for react-admin 3.8.1 that seems to work well.

Here is the reference code

https://codesandbox.io/s/wy7z7q5zx5?file=/index.js:966-979

Example:

First make the helper functions as necessary.

const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
const simpleMemoize = fn => {
  let lastArg;
  let lastResult;
  return arg => {
    if (arg !== lastArg) {
      lastArg = arg;
      lastResult = fn(arg);
    }
    return lastResult;
  };
};

Then the actual validation code

const usernameAvailable = simpleMemoize(async value => {
  if (!value) {
    return "Required";
  }
  await sleep(400);
  if (
    ~["john", "paul", "george", "ringo"].indexOf(value && value.toLowerCase())
  ) {
    return "Username taken!";
  }
});

Finally wire it up to your field:

const validateUserName = [required(), maxLength(10), abbrevUnique];

const UserNameInput = (props) => {
    return (
        <TextInput
            label="User Name"
            source="username"
            variant='outlined'
            validate={validateAbbrev}
        >
        </TextInput>);
}
Goodtempered answered 22/8, 2020 at 3:25 Comment(0)
B
0

In addition to Christiaan Westerbeek's answer. I just recreating a SimpleForm component with some of Christian's hints. In begining i tried to extend SimpleForm with needed server-side validation functionality, but there were some issues (such as not binded context to its handleSubmitWithRedirect method), so i just created my CustomForm to use it in every place i need.

import React, { Children, Component } from 'react';
import PropTypes from 'prop-types';
import { reduxForm, SubmissionError } from 'redux-form';
import { connect } from 'react-redux';
import compose from 'recompose/compose';
import { withStyles } from '@material-ui/core/styles';
import classnames from 'classnames';
import { getDefaultValues, translate } from 'ra-core';
import FormInput from 'ra-ui-materialui/lib/form/FormInput';
import Toolbar  from 'ra-ui-materialui/lib/form/Toolbar';
import {CREATE, UPDATE} from 'react-admin';
import { showNotification as showNotificationAction } from 'react-admin';
import { push as pushAction } from 'react-router-redux';

import dataProvider from "../../providers/dataProvider";

const styles = theme => ({
    form: {
        [theme.breakpoints.up('sm')]: {
            padding: '0 1em 1em 1em',
        },
        [theme.breakpoints.down('xs')]: {
            padding: '0 1em 5em 1em',
        },
    },
});

const sanitizeRestProps = ({
   anyTouched,
   array,
   asyncValidate,
   asyncValidating,
   autofill,
   blur,
   change,
   clearAsyncError,
   clearFields,
   clearSubmit,
   clearSubmitErrors,
   destroy,
   dirty,
   dispatch,
   form,
   handleSubmit,
   initialize,
   initialized,
   initialValues,
   pristine,
   pure,
   redirect,
   reset,
   resetSection,
   save,
   submit,
   submitFailed,
   submitSucceeded,
   submitting,
   touch,
   translate,
   triggerSubmit,
   untouch,
   valid,
   validate,
   ...props
}) => props;

/*
 * Zend validation adapted catch(e) method.
 * Formatted as
 * e = {
 *    field_name: { errorType: 'messageText' }
 * }
 */
const submit = (data, resource) => {
    let actionType = data.id ? UPDATE : CREATE;

    return dataProvider(actionType, resource, {data: {...data}}).catch(e => {
        let errorObject = {};

        for (let fieldName in e) {
            let fieldErrors = e[fieldName];

            errorObject[fieldName] = Object.values(fieldErrors).map(value => `${value}\n`);
        }

        throw new SubmissionError(errorObject);
    });
};

export class CustomForm extends Component {
    handleSubmitWithRedirect(redirect = this.props.redirect) {
        return this.props.handleSubmit(data => {
            return submit(data, this.props.resource).then((result) => {
                let path;

                switch (redirect) {
                    case 'create':
                        path = `/${this.props.resource}/create`;
                        break;
                    case 'edit':
                        path = `/${this.props.resource}/${result.data.id}`;
                        break;
                    case 'show':
                        path = `/${this.props.resource}/${result.data.id}/show`;
                        break;
                    default:
                        path = `/${this.props.resource}`;
                }

                this.props.dispatch(this.props.showNotification(`${this.props.resource} saved`));

                return this.props.dispatch(this.props.push(path));
            });
        });
    }

    render() {
        const {
            basePath,
            children,
            classes = {},
            className,
            invalid,
            pristine,
            push,
            record,
            resource,
            showNotification,
            submitOnEnter,
            toolbar,
            version,
            ...rest
        } = this.props;

        return (
            <form
                // onSubmit={this.props.handleSubmit(submit)}
                className={classnames('simple-form', className)}
                {...sanitizeRestProps(rest)}
            >
                <div className={classes.form} key={version}>
                    {Children.map(children, input => {
                        return (
                            <FormInput
                                basePath={basePath}
                                input={input}
                                record={record}
                                resource={resource}
                            />
                        );
                    })}
                </div>
                {toolbar &&
                React.cloneElement(toolbar, {
                    handleSubmitWithRedirect: this.handleSubmitWithRedirect.bind(this),
                    invalid,
                    pristine,
                    submitOnEnter,
                })}
            </form>
        );
    }
}

CustomForm.propTypes = {
    basePath: PropTypes.string,
    children: PropTypes.node,
    classes: PropTypes.object,
    className: PropTypes.string,
    defaultValue: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),
    handleSubmit: PropTypes.func, // passed by redux-form
    invalid: PropTypes.bool,
    pristine: PropTypes.bool,
    push: PropTypes.func,
    record: PropTypes.object,
    resource: PropTypes.string,
    redirect: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
    save: PropTypes.func, // the handler defined in the parent, which triggers the REST submission
    showNotification: PropTypes.func,
    submitOnEnter: PropTypes.bool,
    toolbar: PropTypes.element,
    validate: PropTypes.func,
    version: PropTypes.number,
};

CustomForm.defaultProps = {
    submitOnEnter: true,
    toolbar: <Toolbar />,
};

const enhance = compose(
    connect((state, props) => ({
        initialValues: getDefaultValues(state, props),
        push: pushAction,
        showNotification: showNotificationAction,
    })),
    translate, // Must be before reduxForm so that it can be used in validation
    reduxForm({
        form: 'record-form',
        destroyOnUnmount: false,
        enableReinitialize: true,
    }),
    withStyles(styles)
);

export default enhance(CustomForm);

For better understanding of my catch callback: In my data provider i do something like this

...
    if (response.status !== 200) {
         return Promise.reject(response);
    }

    return response.json().then((json => {

        if (json.state === 0) {
            return Promise.reject(json.errors);
        }

        switch(type) {
        ...
        }
    ...
    }
...
Bratcher answered 10/6, 2018 at 16:15 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.