非同期フォーム検証ライブラリとコードの100未満の行を作成する方法[翻訳済み]フックを反応させます

フォームの検証は非常にトリッキーなものです。フォームを達成するための深い理解した後、あなたがに対処するための境界シーンが多数あります。幸いなことに、必要な計量提供し、多くのフォーム検証ライブラリーがあり、(注釈:、汚い無効、初期化され、自然のままの、などなど)とハンドラ、私たちは強力なフォームを実現するために。しかし、私は使用したいフックのAPIを反応させるの自分自身に挑戦するフォーム検証ライブラリを次のコードの100行を構築します。実験段階ではまだフックを反応させるのが、これですが証明フックが反応のフォーム検証を実現します。

私は、この書いたことを宣言しなければなりませんライブラリは確かに少ないコードの100行を超えています。私は、ライブラリを使用する方法を明確に説明する必要があるためしかし、このチュートリアルでは、コードの約200行が、あります。

:私は初心者チュートリアルフォームライブラリのほとんどは、3つのコアのトピックから不可分で見てきた非同期検証、フォームのリンク:いくつかのフォーム項目をチェックインする必要がある他のフォームの項目を変更したときに、トリガ、フォーム検証効率の最適化を。他の変数の練習の影響を無視して、私は、固定のチュートリアルシーンをお使いの方に非常にうんざりしています。実際の場面でしばしば逆効果なので、ので、私のチュートリアルでは、より現実のシーンをカバーしようとします。

私たちの目標は満たされる必要があります。

  • 同期チェック単一のテーブル項目、項目の変更を含む値テーブルは、変更がフォーム項目が依存している従います

  • 非同期単一のテーブル項目、項目の変更を含む値テーブルをチェックし、変更がフォーム項目に依存している続きます

  • フォームを送信する前に、個々の同期のすべてのテーブルをチェック

  • フォームを送信する前に、すべての非同期フォームの項目を確認してください

  • フォームが送信された場合は、非同期は、エラー・メッセージが返されますを示すために失敗し、提出してみてください

  • 、開発者機能への検証フォームを提供し、開発者は、このような時にチェック形式として、適切なタイミングでできるように、onBlurイベント

  • 単一のテーブル・アイテムの複数のキャリブレーションを可能にします

  • 禁止提出フォームの検証に合格しない場合

  • 唯一のエラーメッセージがある場合にのみ表示さや形を変更しようとすると、フォームのエラーメッセージが送信されます

登録フォームは、これらのシナリオをカバーするために、アカウントのユーザー名、パスワード、パスワードの二次確認が含まれていて私達は達成します。ここでは、このライブラリのバーを構築するために一緒に来て、シンプルなインターフェースです。

const form = useForm({
  onSubmit,
});

const usernameField = useField("username", form, {
  defaultValue: "",
  validations: [
    async formData => {
      await timeout(2000);
      return formData.username.length < 6 && "Username already exists";
    }
  ],
  fieldsToValidateOnChange: []
});
const passwordField = useField("password", form, {
  defaultValue: "",
  validations: [
    formData =>
      formData.password.length < 6 && "Password must be at least 6 characters"
  ],
  fieldsToValidateOnChange: ["password", "confirmPassword"]
});
const confirmPasswordField = useField("confirmPassword", form, {
  defaultValue: "",
  validations: [
    formData =>
      formData.password !== formData.confirmPassword &&
      "Passwords do not match"
  ],
  fieldsToValidateOnChange: ["password", "confirmPassword"]
});

// const { onSubmit, getFormData, addField, isValid, validateFields, submitted, submitting } = form
// const { name, value, onChange, errors, setErrors, pristine, validate, validating } = usernameField
复制代码

これは非常にシンプルなAPIですが、本当に私たちに多くの柔軟性を与えました。あなたは、このインターフェースが機能し、検証し、検証のような二つの名前が含まれ、実現している可能性があります。検証は、パラメータとして、フォームデータと、個々のテーブル名を持つ関数として定義されている検証問題ならば、エラーメッセージが返され、同時にそれはダミーの値(翻訳者注を返します。falseに変換することができます値)。一方、この関数のフォーム項目を実行し、エラーのフォーム項目のリストを更新するために、すべての検証機能を検証します。

最も重要なことは、我々は、送信されたフォームの変更とフォームの値に対処するためのフレームワークを必要としています。すべてのチェックが含まれています私たちの最初の試みは、唯一の状態のフォームを処理するために使用されます。

// 跳过样板代码: imports, ReactDOM, 等等.

export const useField = (name, form, { defaultValue } = {}) => {
  let [value, setValue] = useState(defaultValue);

  let field = {
    name,
    value,
    onChange: e => {
      setValue(e.target.value);
    }
  };
  // 注册表单项
  form.addField(field);
  return field;
};

export const useForm = ({ onSubmit }) => {
  let fields = [];

  const getFormData = () => {
    // 获得一个包含原始表单数据的 object
    return fields.reduce((formData, field) => {
      formData[field.name] = field.value;
      return formData;
    }, {});
  };

  return {
    onSubmit: async e => {
      e.preventDefault(); // 阻止默认表单提交
      return onSubmit(getFormData());
    },
    addField: field => fields.push(field),
    getFormData
  };
};

const Field = ({ label, name, value, onChange, ...other }) => {
  return (
    <FormControl className="field">
      <InputLabel htmlFor={name}>{label}</InputLabel>
      <Input value={value} onChange={onChange} {...other} />
    </FormControl>
  );
};

const App = props => {
  const form = useForm({
    onSubmit: async formData => {
      window.alert("Account created!");
    }
  });

  const usernameField = useField("username", form, {
    defaultValue: ""
  });
  const passwordField = useField("password", form, {
    defaultValue: ""
  });
  const confirmPasswordField = useField("confirmPassword", form, {
    defaultValue: ""
  });

  return (
    <div id="form-container">
      <form onSubmit={form.onSubmit}>
        <Field {...usernameField} label="Username" />
        <Field {...passwordField} label="Password" type="password" />
        <Field {...confirmPasswordField} label="Confirm Password" type="password" />
        <Button type="submit">Submit</Button>
      </form>
    </div>
  );
};
复制代码

理解することは非常に困難であるコードはありません。フォームの値は、私たちの唯一の懸念です。それは、フォームに自分の登録の最後に初期化される前に、各フォーム項目。私たちのonChange機能も非常に簡単です。ここでは最も複雑な機能は、そうであっても、それが軽減抽象構文と比較することはできません、getFormDataです。すべての表getFormDataトラバースし、フォームの値を表すために、プレーンオブジェクトを返します。最後に、フォーム送信時には、我々はページの再読み込みを防ぐためには、preventDefaultを呼び出す必要があることを言及する価値があります。

物事はうまくいっている、そして今、私たちは確認するために、それを追加する必要があります。フォーム項目の値を変更したり、提出フォームされたとき、私たちは特定のフォームの項目を検証するために必要なものではありません示されているが、すべてのフォームの項目を確認してください。

export const useField = (
  name,
  form,
  { defaultValue, validations = [] } = {}
) => {
  let [value, setValue] = useState(defaultValue);
  let [errors, setErrors] = useState([]);

  const validate = async () => {
    let formData = form.getFormData();
    let errorMessages = await Promise.all(
      validations.map(validation => validation(formData, name))
    );
    errorMessages = errorMessages.filter(errorMsg => !!errorMsg);
    setErrors(errorMessages);
    let fieldValid = errorMessages.length === 0;
    return fieldValid;
  };

  useEffect(
    () => {
      form.validateFields(); // 当 value 变化的时候校验表单项
    },
    [value]
  );

  let field = {
    name,
    value,
    errors,
    validate,
    setErrors,
    onChange: e => {
      setValue(e.target.value);
    }
  };
  // 注册表单项
  form.addField(field);
  return field;
};

export const useForm = ({ onSubmit }) => {
  let fields = [];

  const getFormData = () => {
    // 获得一个 object 包含原始表单数据
    return fields.reduce((formData, field) => {
      formData[field.name] = field.value;
      return formData;
    }, {});
  };

  const validateFields = async () => {
    let fieldsToValidate = fields;
    let fieldsValid = await Promise.all(
      fieldsToValidate.map(field => field.validate())
    );
    let formValid = fieldsValid.every(isValid => isValid === true);
    return formValid;
  };

  return {
    onSubmit: async e => {
      e.preventDefault(); // 阻止表单提交默认事件
      let formValid = await validateFields();
      return onSubmit(getFormData(), formValid);
    },
    addField: field => fields.push(field),
    getFormData,
    validateFields
  };
};

const Field = ({
  label,
  name,
  value,
  onChange,
  errors,
  setErrors,
  validate,
  ...other
}) => {
  let showErrors = !!errors.length;
  return (
    <FormControl className="field" error={showErrors}>
      <InputLabel htmlFor={name}>{label}</InputLabel>
      <Input
        id={name}
        value={value}
        onChange={onChange}
        onBlur={validate}
        {...other}
      />
      <FormHelperText component="div">
        {showErrors &&
          errors.map(errorMsg => <div key={errorMsg}>{errorMsg}</div>)}
      </FormHelperText>
    </FormControl>
  );
};

const App = props => {
  const form = useForm({
    onSubmit: async formData => {
      window.alert("Account created!");
    }
  });

  const usernameField = useField("username", form, {
    defaultValue: "",
    validations: [
      async formData => {
        await timeout(2000);
        return formData.username.length < 6 && "Username already exists";
      }
    ]
  });
  const passwordField = useField("password", form, {
    defaultValue: "",
    validations: [
      formData =>
        formData.password.length < 6 && "Password must be at least 6 characters"
    ]
  });
  const confirmPasswordField = useField("confirmPassword", form, {
    defaultValue: "",
    validations: [
      formData =>
        formData.password !== formData.confirmPassword &&
        "Passwords do not match"
    ]
  });

  return (
    <div id="form-container">
      <form onSubmit={form.onSubmit}>
        <Field {...usernameField} label="Username" />
        <Field {...passwordField} label="Password" type="password" />
        <Field {...confirmPasswordField} label="Confirm Password" type="password" />
        <Button type="submit">Submit</Button>
      </form>
    </div>
  );
};

复制代码

上記のコードは改良版で、一般的な外観は、アップを実行するようだが、ことをユーザーに配信十分ではありません。このバージョンは、隠しマーク状態(翻訳者注:旗)のエラー・メッセージの多くを失って、これらのエラーメッセージは、間違った時に表示されることがあります。例えば、ユーザは、フォームがすぐに確認し、適切なエラーメッセージが表示されたときに、入力情報の変更が終了していません。

最も基本的には、我々は、ユーザーがエラーメッセージを表示しない、フォーム項目の値を変更しない場合は、UIを知らせるために、いくつかの基本的なマークアップの状態を必要としています。さらに、これらの基盤に加えて、我々はまた、いくつかの追加のマークアップの状態を必要としています。

私たちは、フラグの状態は、ユーザーが提出されているフォームまたは非同期のフォーム項目であることのチェックの状態を記録するために、フォームやマークを提出録音しようとしている必要があります。あなたはまた、我々は、むしろ中のonChangeで呼び出すよりも、useEffect内validateFieldsを呼び出すために持っている理由を知りたいことがあります。setValueのは、非同期的に発生するので、我々は、我々は、コールバックを提供することはありません、それは約束を返さない、useEffectを必要としています。したがって、setValueの完了までの唯一の方法は、私たちはuseEffectによって、決定することができ値の変化を監視することです。

今、私たちはこれらのいわゆるマーク・ステート・バーを達成するために一緒に働きます。優れたUIと詳細情報を改善するためにそれらを使用してください。

export const useField = (
  name,
  form,
  { defaultValue, validations = [], fieldsToValidateOnChange = [name] } = {}
) => {
  let [value, setValue] = useState(defaultValue);
  let [errors, setErrors] = useState([]);
  let [pristine, setPristine] = useState(true);
  let [validating, setValidating] = useState(false);
  let validateCounter = useRef(0);

  const validate = async () => {
    let validateIteration = ++validateCounter.current;
    setValidating(true);
    let formData = form.getFormData();
    let errorMessages = await Promise.all(
      validations.map(validation => validation(formData, name))
    );
    errorMessages = errorMessages.filter(errorMsg => !!errorMsg);
    if (validateIteration === validateCounter.current) {
      // 最近一次调用
      setErrors(errorMessages);
      setValidating(false);
    }
    let fieldValid = errorMessages.length === 0;
    return fieldValid;
  };

  useEffect(
    () => {
      if (pristine) return; // 避免渲染完成后的第一次校验
      form.validateFields(fieldsToValidateOnChange);
    },
    [value]
  );

  let field = {
    name,
    value,
    errors,
    setErrors,
    pristine,
    onChange: e => {
      if (pristine) {
        setPristine(false);
      }
      setValue(e.target.value);
    },
    validate,
    validating
  };
  form.addField(field);
  return field;
};

export const useForm = ({ onSubmit }) => {
  let [submitted, setSubmitted] = useState(false);
  let [submitting, setSubmitting] = useState(false);
  let fields = [];

  const validateFields = async fieldNames => {
    let fieldsToValidate;
    if (fieldNames instanceof Array) {
      fieldsToValidate = fields.filter(field =>
        fieldNames.includes(field.name)
      );
    } else {
      // 如果 fieldNames 缺省,则验证所有表单项
      fieldsToValidate = fields;
    }
    let fieldsValid = await Promise.all(
      fieldsToValidate.map(field => field.validate())
    );
    let formValid = fieldsValid.every(isValid => isValid === true);
    return formValid;
  };

  const getFormData = () => {
    return fields.reduce((formData, f) => {
      formData[f.name] = f.value;
      return formData;
    }, {});
  };

  return {
    onSubmit: async e => {
      e.preventDefault();
      setSubmitting(true);
      setSubmitted(true); // 用户已经至少提交过一次表单
      let formValid = await validateFields();
      let returnVal = await onSubmit(getFormData(), formValid);
      setSubmitting(false);
      return returnVal;
    },
    isValid: () => fields.every(f => f.errors.length === 0),
    addField: field => fields.push(field),
    getFormData,
    validateFields,
    submitted,
    submitting
  };
};

const Field = ({
  label,
  name,
  value,
  onChange,
  errors,
  setErrors,
  pristine,
  validating,
  validate,
  formSubmitted,
  ...other
}) => {
  let showErrors = (!pristine || formSubmitted) && !!errors.length;
  return (
    <FormControl className="field" error={showErrors}>
      <InputLabel htmlFor={name}>{label}</InputLabel>
      <Input
        id={name}
        value={value}
        onChange={onChange}
        onBlur={() => !pristine && validate()}
        endAdornment={
          <InputAdornment position="end">
            {validating && <LoadingIcon className="rotate" />}
          </InputAdornment>
        }
        {...other}
      />
      <FormHelperText component="div">
        {showErrors &&
          errors.map(errorMsg => <div key={errorMsg}>{errorMsg}</div>)}
      </FormHelperText>
    </FormControl>
  );
};

const App = props => {
  const form = useForm({
    onSubmit: async (formData, valid) => {
      if (!valid) return;
      await timeout(2000); // 模拟网络延迟
      if (formData.username.length < 10) {
        //模拟服务端返回 400 
        usernameField.setErrors(["Make a longer username"]);
      } else {
        //模拟服务端返回 201 
        window.alert(
          `form valid: ${valid}, form data: ${JSON.stringify(formData)}`
        );
      }
    }
  });

  const usernameField = useField("username", form, {
    defaultValue: "",
    validations: [
      async formData => {
        await timeout(2000);
        return formData.username.length < 6 && "Username already exists";
      }
    ],
    fieldsToValidateOnChange: []
  });
  const passwordField = useField("password", form, {
    defaultValue: "",
    validations: [
      formData =>
        formData.password.length < 6 && "Password must be at least 6 characters"
    ],
    fieldsToValidateOnChange: ["password", "confirmPassword"]
  });
  const confirmPasswordField = useField("confirmPassword", form, {
    defaultValue: "",
    validations: [
      formData =>
        formData.password !== formData.confirmPassword &&
        "Passwords do not match"
    ],
    fieldsToValidateOnChange: ["password", "confirmPassword"]
  });

  let requiredFields = [usernameField, passwordField, confirmPasswordField];

  return (
    <div id="form-container">
      <form onSubmit={form.onSubmit}>
        <Field
          {...usernameField}
          formSubmitted={form.submitted}
          label="Username"
        />
        <Field
          {...passwordField}
          formSubmitted={form.submitted}
          label="Password"
          type="password"
        />
        <Field
          {...confirmPasswordField}
          formSubmitted={form.submitted}
          label="Confirm Password"
          type="password"
        />
        <Button
          type="submit"
          disabled={
            !form.isValid() ||
            form.submitting ||
            requiredFields.some(f => f.pristine)
          }
        >
          {form.submitting ? "Submitting" : "Submit"}
        </Button>
      </form>
    </div>
  );
};
复制代码

最後の試みは、我々は内部の多くのものを追加しました。提出し、検証して提出し、自然のまま:それは4マーク状態備えます。また、それが必要とするフォーム項目の値の変更の形を検証するときに宣言するvalidateFields渡し、fieldsToValidateOnChangeを添加。状態によってこれらのマーカーを読み込むために、エラーメッセージやアニメーションを表示し、送信ボタンを無効にするタイミングを制御するためのUI層で我々。

あなたは非常に特別なことをvalidateCounterを気づいているかもしれません。私たちは、現在のコールが完了する前に検証し、再び呼び出される可能性があるため、コール数は機能を検証記録する必要があります。このシナリオであれば、我々は、最新の時間のテーブルを更新するために、個々のエラー状態結果のみを使用して、現在の呼び出しの結果、およびコールを放棄すべきです。

すべての準備が整った後、これが私たちの果実です。

フックは、簡潔なフォーム検証ソリューションを提供して反応します。これは私がこのAPIの最初の試みを使用しているものです。少し欠陥が、私はまだそれが強力だと感じ。それは私が来て好きなように従っているので、このインタフェースは、少し奇妙です。これらの欠陥に加えて、しかし、その機能は非常に強力です。

私はまた、このようなuseEffectにおける方法で小道具の変化をチェックするフックですuseState更新状態が完了したときに示すために、コールバックメカニズム、などいくつかの機能を、不足していると思います。

追伸

このチュートリアルを使用して簡単に確保するために、私は意図的にいくつかのパラメータを処理し、チェックサムエラーや例外を省略しました。例えば、私が入ってくるフォームパラメータが実際にフォームオブジェクトでチェックしませんでした。私は間違いなく、その型をチェックし、詳細な例外をスローすることができれば良くなります。実際に、私はこのエラーのようなコードを書かれています。

Cannot read property ‘addField’ of undefined
复制代码

NPMパッケージが、また、適切なパラメータチェックサムエラーや例外処理にこのコードのリリース前に。私が言ったように、あなたが理解したい場合は、私が使用しているsuperstructはチェックパラメータが含まれている達成し、より堅牢なバージョンを

あなたは、変換エラーや改善のための他の領域がある見つけた場合は、へようこそデンバー翻訳プログラムは変更と翻訳PRは、また、対応するボーナスポイントを得ることができます。記事の冒頭固定リンクの記事は、 GitHubの上で、この記事内のリンク値下げです。


ナゲッツ翻訳プロジェクトは、技術的な記事のインターネットコミュニティの高品質な翻訳のためのソースでナゲッツに英語を共有する記事。カバーのコンテンツのAndroidiOSのフロントエンドバックエンドブロックチェーン製品設計人工知能などの分野は、あなたがより多くの高品質の翻訳を見たいと思って、集中し続ける下さいナゲッツ翻訳プログラムを公式マイクロブログは私たちはほとんどの列を知っています

おすすめ

転載: blog.csdn.net/weixin_34220179/article/details/91393714