【问题标题】:how can I show customized error messaged from server side validation in React Admin package?如何在 React Admin 包中显示来自服务器端验证的自定义错误消息?
【发布时间】:2018-10-18 14:45:39
【问题描述】:

有没有办法使用https://github.com/marmelab/react-admin 包执行服务器端表单验证?

这是 AdminCreate 组件的代码。它向api发送创建请求。如果一切正常,Api 会返回状态码 422 或状态码 200 的验证错误。

    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>;

}
}

所以问题是,如何将每个字段的错误与从服务器发送的错误对象分开显示?这是错误对象的示例:

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

提前谢谢你!

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>
        );
    }
}

现在我有以下代码,它显示验证错误。但问题是,成功后我无法执行重定向。有什么想法吗?

【问题讨论】:

  • 我也在寻找一种方法来做到这一点。当我找到它时会回来的。
  • 我将不胜感激
  • 如果我没看错,您的解决方案是创建一个自定义 &lt;SimpleForm&gt;,直接使用 dataProvider 来捕获错误并显示它?
  • 我找到了迄今为​​止最好的解决方案:stackoverflow.com/a/54697878/2862728
  • @Rad80 是的,这太难了!我会尽快上传到这里。

标签: javascript reactjs react-admin


【解决方案1】:

如果您使用SimpleForm,您可以按照a comment in issue 97 中的建议将asyncValidateasyncBlurFields 一起使用。我没有使用 SimpleForm,所以我只能告诉你。

我使用了一个简单的form。您也可以在那里使用服务器端验证。这就是我的做法。一个完整且有效的示例。

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);

如果代码需要进一步解释,请在下面发表评论,我会尝试详细说明。

我希望能够通过调度具有 onSuccess 和 onFailure 副作用的操作来执行 REST 请求的操作,如 here 所述,但我无法将其与 SubmissionError 一起工作。

【讨论】:

  • 非常感谢!你拯救了我的一天:)
  • 更新问题,请查看
  • 你真的应该发布一个新问题。它提高了核心问题的质量,以及为使用 react-admin 的开发人员提供答案的价值。
  • 我找到了迄今为​​止最好的解决方案:stackoverflow.com/a/54697878/2862728
【解决方案2】:

这里是官方回购的另一种解决方案。 https://github.com/marmelab/react-admin/pull/871 需要在 DataProvider 中导入 HttpError(message, status, body) 并抛出。 然后在 errorSaga 中将 body 解析为 redux-form 结构。 而已。 享受吧。

【讨论】:

    【解决方案3】:

    react-admin 3.8.1 找到了一个似乎运行良好的可行解决方案。

    这是参考代码

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

    示例:

    首先根据需要制作辅助函数。

    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;
      };
    };
    

    然后是实际的验证码

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

    最后把它连接到你的领域:

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

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

    【讨论】:

      【解决方案4】:

      除了 Christiaan Westerbeek 的回答。 我只是用 Christian 的一些提示重新创建了一个 SimpleForm 组件。 一开始,我尝试使用所需的服务器端验证功能扩展 SimpleForm,但存在一些问题(例如未将上下文绑定到其 handleSubmitWithRedirect 方法),所以我只是创建了我的 CustomForm 以在我需要的每个地方使用它。

      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);
      

      为了更好地理解我的catch 回调:在我的数据提供者中,我做了这样的事情

      ...
          if (response.status !== 200) {
               return Promise.reject(response);
          }
      
          return response.json().then((json => {
      
              if (json.state === 0) {
                  return Promise.reject(json.errors);
              }
      
              switch(type) {
              ...
              }
          ...
          }
      ...
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-03-03
        • 1970-01-01
        • 1970-01-01
        • 2017-12-26
        • 1970-01-01
        相关资源
        最近更新 更多