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

后端 未结 4 1399
栀梦
栀梦 2020-12-16 18:22

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 c

相关标签:
4条回答
  • 2020-12-16 18:38

    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>);
    }
    
    0 讨论(0)
  • 2020-12-16 18:43

    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.

    0 讨论(0)
  • 2020-12-16 18:52

    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.

    0 讨论(0)
  • 2020-12-16 18:57

    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) {
            ...
            }
        ...
        }
    ...
    
    0 讨论(0)
提交回复
热议问题