Skip to content Skip to sidebar Skip to footer

How Can I Show Customized Error Messaged From Server Side Validation In React Admin Package?

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

Solution 1:

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.

importReactfrom'react';
importPropTypesfrom'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';
importButtonfrom'material-ui/Button';
importTextFieldfrom'material-ui/TextField';
import { CircularProgress } from'material-ui/Progress';
import { CREATE, translate } from'ra-core';
import { dataProvider } from'../../providers'; // <-- Make sure to import yours!constrenderInput = ({
    meta: { touched, error } = {},
    input: { ...inputProps },
    ...props
}) => (
    <TextFielderror={!!(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
 */constsubmit = 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,
        };
        thrownewSubmissionError(errorObject);
    });

constMyForm = ({ isLoading, handleSubmit, error, translate }) => (
    <formonSubmit={handleSubmit(submit)}><div><div><Fieldname="email"component={renderInput}label="Email"disabled={isLoading}
                /></div></div><CardActions><Buttonvariant="raised"type="submit"color="primary"disabled={isLoading}
            >
                {isLoading && <CircularProgresssize={25}thickness={2} />}
                Signin
            </Button>
            {error && <strong>General error: {translate(error)}</strong>}
        </CardActions></form>
);
MyForm.propTypes = {
    ...propTypes,
    classes: PropTypes.object,
    redirectTo: PropTypes.string,
};

constmapStateToProps = 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;
        },
    })
);

exportdefaultenhance(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.

Solution 2:

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.

Solution 3:

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.

constsleep = ms => newPromise(resolve =>setTimeout(resolve, ms));
constsimpleMemoize = fn => {
  let lastArg;
  let lastResult;
  returnarg => {
    if (arg !== lastArg) {
      lastArg = arg;
      lastResult = fn(arg);
    }
    return lastResult;
  };
};

Then the actual validation code

const usernameAvailable = simpleMemoize(asyncvalue => {
  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];

constUserNameInput = (props) => {
    return (
        <TextInputlabel="User Name"source="username"variant='outlined'validate={validateAbbrev}
        ></TextInput>);
}

Solution 4:

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.

importReact, { Children, Component } from'react';
importPropTypesfrom'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';
importFormInputfrom'ra-ui-materialui/lib/form/FormInput';
importToolbarfrom'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";

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

constsanitizeRestProps = ({
   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' }
 * }
 */constsubmit = (data, resource) => {
    let actionType = data.id ? UPDATE : CREATE;

    returndataProvider(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`);
        }

        thrownewSubmissionError(errorObject);
    });
};

exportclassCustomFormextendsComponent {
    handleSubmitWithRedirect(redirect = this.props.redirect) {
        returnthis.props.handleSubmit(data => {
            returnsubmit(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`));

                returnthis.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)}
            ><divclassName={classes.form}key={version}>
                    {Children.map(children, input => {
                        return (
                            <FormInputbasePath={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-forminvalid: 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 submissionshowNotification: 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 validationreduxForm({
        form: 'record-form',
        destroyOnUnmount: false,
        enableReinitialize: true,
    }),
    withStyles(styles)
);

exportdefaultenhance(CustomForm);

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

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

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

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

        switch(type) {
        ...
        }
    ...
    }
...

Post a Comment for "How Can I Show Customized Error Messaged From Server Side Validation In React Admin Package?"