1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

追加ボタンで新しいフォームが表示される処理の実装

Posted at

React での新規生徒追加ページの実装解説

はじめに

この記事では、時間割アプリ制作にあたってReact を使用して実装した新規生徒追加ページのコードを抜粋して解説します。
このページには生徒の情報を入力するフォームが追加され、バリデーション機能や、追加・確定ボタンの機能が含まれています。
また最下部に各ページの最終的なコードを記載しています。

以下の内容を詳しく解説します。

  • フォームの動的追加
  • バリデーション(未入力項目のエラー表示)
  • 確定ボタンによる処理の流れ
  • 各コンポーネントの役割

コンポーネントの概要

まず、AddStudentPage コンポーネントがメインとなるページです。このコンポーネントには、複数のフォーム(生徒の情報を入力するためのフォーム)を管理するロジックが含まれています。
スクリーンショット 2024-10-07 14.05.36.png


AddStudentPage.jsx

1. フォームの追加機能

AddStudentPage.jsx
const [forms, setForms] = useState([{}]); // フォームのリストを管理
const handleAddForm = () => {
  setForms([...forms, {}]); // 新しい空のフォームを追加
};

このコードでは、useState を使用してフォームを管理しています。forms は現在表示されているフォームのリストで、handleAddForm 関数を呼ぶと、新しい空のフォームが追加されます。[...forms, {}] により、既存のフォームに新しい空のオブジェクトを追加する形です。

2. バリデーション機能

const InputField = styled.input`
  font-size: 16px;
  color: #000;
  border: ${({ isError }) => (isError ? "2px solid red" : "1px solid #ccc")};

  &::placeholder {
    font-size: 14px;
    font-weight: 400;
    color: #999;
  }
`;

const handleValidate = (e) => {
  e.preventDefault();
  let hasError = false;
  const newErrors = forms.map((_, index) => {
    const formErrors = {};
    const inputs = refs.current[index]; // フォームの各入力項目を参照
    if (inputs.id.value === "") {
      formErrors.id = true;
      hasError = true;
    }
    // 他の入力項目についても同様にチェック
    return formErrors;
  });
  setErrors(newErrors);
  if (!hasError) {
    alert("生徒が追加されました");
  }
};

handleValidate はバリデーションを行う関数です。ここでは、refs を使って各フォームの入力項目にアクセスし、空欄がある場合には hasError フラグを立て、エラーステートを更新します。エラーがなければ、アラートで「生徒が追加されました」と表示されます。
styled-componentsの記述ではエラーがあった時に項目のborderを赤くするように記述しています。

3. フォームの JSX 構造

<FormContainer key={index}>
  <FormInner>
    <FormHeader number={index + 1} />
    <InputField
      type="text"
      placeholder="生徒ID"
      isError={errors[index]?.id}
      ref={(el) => (refs.current[index] = { ...refs.current[index], id: el })}
    />
    {/* 他の入力フィールドも同様に配置 */}
  </FormInner>
</FormContainer>

各フォームは map 関数を使って、動的に生成されます。index を使ってフォームごとに一意のキーを設定し、エラーがある場合には InputField コンポーネントに isError プロップを渡して、エラー表示を制御しています。

4. ボタンの処理

<AddCompleteButton
  handleAddForm={handleAddForm}
  handleValidate={handleValidate}
/>

AddCompleteButton コンポーネントを通じて、追加確定 のボタンがレンダリングされます。handleAddForm で新しいフォームを追加し、handleValidate でバリデーションを実行します。


FormHeader.jsx

const FormHeaderWrapper = styled.div`
  width: 30px;
  height: 30px;
  background-color: #2c65c7;
  display: flex;
  justify-content: center;
  align-items: center;
`;

const FormNumberSpan = styled.span`
  color: #fff;
  font-size: 16px;
`;

export const FormHeader = ({ number }) => {
  return (
    <FormHeaderWrapper>
      <FormNumberSpan>{number}</FormNumberSpan>
    </FormHeaderWrapper>
  );
};

この FormHeader コンポーネントは、各フォームの番号を表示します。例えば、1つ目のフォームには "1" が、2つ目のフォームには "2" が表示されます。


MonthWeek.jsx

export const MonthWeek = () => {
  return (
    <div className="student-info">
      <label htmlFor="slot">コマ</label>
      <select id="day">
        <option></option>
        <option></option>
        {/* 他の曜日 */}
      </select>
      <span>曜日</span>
      <select id="period">
        <option>1</option>
        <option>2</option>
        {/* 他のコマ */}
      </select>
      <span></span>
    </div>
  );
};

MonthWeek コンポーネントは、授業が行われる曜日と週を選択するための部分です。曜日とコマの選択肢が提供され、生徒ごとの時間割を設定できます。


AddCompleteButton.jsx

const DoubleButton = styled.div`
  .add {
    background-color: #2c65c7;
    margin-right: 36px;
  }
`;

export const AddCompleteButton = ({ handleAddForm, handleValidate }) => {
  return (
    <DoubleButton>
      <button className="add" onClick={handleAddForm}>
        追加
      </button>
      <button className="complete" onClick={handleValidate}>
        確定
      </button>
    </DoubleButton>
  );
};

この AddCompleteButton では、2つのボタンを表示しています。1つ目の「追加」ボタンは handleAddForm を呼び出して新しいフォームを追加し、2つ目の「確定」ボタンは handleValidate を呼び出してバリデーションを実行します。


まとめ

今回の記事では、React を使った生徒追加フォームの実装方法について、コードを参照しながら解説しました。動的にフォームを追加したり、バリデーションを行ったりするロジックは、他のアプリケーションにも応用できるポイントです。

ぜひこのコードを参考に、自分のプロジェクトでも実践してみてください!

最終的なコード

AddStudentPage.jsx
    import React, { useState, useRef } from "react";
import styled from "styled-components";
import { FormHeader } from "./FormHeader";
import { MonthWeek } from "./MonthWeek";
import { AddCompleteButton } from "./AddCompleteButton";

const FormContainer = styled.div`
  width: 700px;
  display: flex;
  flex-direction: column;
  align-items: center;
  margin: 0 auto 25px auto;
`;

const FormInner = styled.form`
  width: 520px;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  margin-inline: auto;
`;

// スタイル付きのinputフィールド
const InputField = styled.input`
  font-size: 16px;
  color: #000;
  border: ${({ isError }) => (isError ? "2px solid red" : "1px solid #ccc")};

  &::placeholder {
    font-size: 14px;
    font-weight: 400;
    color: #999;
  }
`;

export const AddStudentPage = () => {
  const [forms, setForms] = useState([{}]); // FormContainerのリスト
  const [errors, setErrors] = useState([{}]); // 各フォームごとのエラーステート
  const refs = useRef([]); // 各フォームのinputフィールドを参照するためのリファレンス

  // フォームを追加する
  const handleAddForm = () => {
    setForms([...forms, {}]); // 新しい空のフォームを追加
    setErrors([...errors, {}]); // 新しいフォームに対応するエラーステートも追加
  };

  // バリデーションを実行
  const handleValidate = (e) => {
    e.preventDefault();
    let hasError = false;

    const newErrors = forms.map((_, index) => {
      const formErrors = {};
      const inputs = refs.current[index];

      if (inputs.id.value === "") {
        formErrors.id = true;
        hasError = true;
      }

      if (inputs.name.value === "") {
        formErrors.name = true;
        hasError = true;
      }

      if (inputs.kana.value === "") {
        formErrors.kana = true;
        hasError = true;
      }

      if (inputs.school.value === "") {
        formErrors.school = true;
        hasError = true;
      }

      return formErrors;
    });

    setErrors(newErrors);

    if (hasError) {
      {
      }
    } else {
      alert("生徒が追加されました");
    }
  };

  return (
    <>
      {forms.map((form, index) => (
        <FormContainer key={index}>
          <FormInner>
            <FormHeader
              beforeWidth="233px"
              afterWidth="233px"
              beforeLeft="70px"
              afterRight="70px"
              number={index + 1}
            />
            <div className="student-info">
              <label htmlFor={`id-${index}`}>ID</label>
              <InputField
                type="text"
                id={`id-${index}`}
                placeholder="生徒ID"
                isError={errors[index]?.id}
                ref={(el) =>
                  (refs.current[index] = { ...refs.current[index], id: el })
                }
              />
            </div>
            <div className="student-info">
              <label htmlFor={`name-${index}`}>氏名</label>
              <InputField
                type="text"
                id={`name-${index}`}
                placeholder="生徒氏名"
                isError={errors[index]?.name}
                ref={(el) =>
                  (refs.current[index] = { ...refs.current[index], name: el })
                }
              />
            </div>
            <div className="student-info">
              <label htmlFor={`kana-${index}`}>フリガナ</label>
              <InputField
                type="text"
                id={`kana-${index}`}
                placeholder="トウカイリン イチタロウ"
                isError={errors[index]?.kana}
                ref={(el) =>
                  (refs.current[index] = { ...refs.current[index], kana: el })
                }
              />
            </div>
            <div className="student-info">
              <label htmlFor={`school-${index}`}>学校</label>
              <InputField
                type="text"
                id={`school-${index}`}
                placeholder="札幌小学校"
                isError={errors[index]?.school}
                ref={(el) =>
                  (refs.current[index] = { ...refs.current[index], school: el })
                }
              />
            </div>
            <MonthWeek />
            <div className="student-info">
              <label htmlFor={`duration-${index}`}>受講時間</label>
              <select id={`duration-${index}`}>
                <option>50分</option>
                <option>75分</option>
              </select>
            </div>
            <div className="student-info">
              <label htmlFor={`start-${index}`}>初回授業</label>
              <InputField type="date" id={`start-${index}`} />
            </div>
          </FormInner>
        </FormContainer>
      ))}
      <AddCompleteButton
        handleAddForm={handleAddForm}
        handleValidate={handleValidate}
      />
    </>
  );
};

FormHeader.jsx
import styled from "styled-components";

const FormHeaderWrapper = styled.div`
  width: 30px;
  height: 30px;
  background-color: #2c65c7;
  border-radius: 2px;
  display: flex;
  justify-content: center;
  align-items: center;
  position: relative;
  margin-bottom: 36px;
  margin-inline: auto;

  &::before,
  &::after {
    position: absolute;
    content: "";
    height: 1px;
    background-color: #4a4a4a;
  }

  &::before {
    width: ${(props) => props.beforeWidth || "233px"};
    left: ${(props) => props.beforeLeft || "70px"};
  }

  &::after {
    width: ${(props) => props.afterWidth || "233px"};
    right: ${(props) => props.afterRight || "70px"};
  }
`;

const FormNumberSpan = styled.span`
  display: block;
  color: #fff;
  font-size: 16px;
  inset: 0;
  margin: auto;
`;

export const FormHeader = ({
  width,
  beforeWidth,
  afterWidth,
  beforeLeft,
  afterRight,
  number,
}) => {
  return (
    <>
      <FormHeaderWrapper
        width={width}
        beforeWidth={beforeWidth}
        afterWidth={afterWidth}
        beforeLeft={beforeLeft}
        afterRight={afterRight}
      >
        <FormNumberSpan>{number}</FormNumberSpan>
      </FormHeaderWrapper>
    </>
  );
};
MonthWeek.jsx
export const MonthWeek = () => {
  return (
    <>
      <div className="student-info">
        <label htmlFor="slot">コマ</label>
        <select id="day">
          <option></option>
          <option></option>
          <option></option>
          <option></option>
          <option></option>
        </select>
        <span>曜日</span>
        <span></span>
        <select id="period">
          <option>1</option>
          <option>2</option>
          <option>3</option>
          <option>4</option>
          <option>5</option>
          <option>6</option>
          <option>7</option>
        </select>
        <span></span>
      </div>
    </>
  );
};
AddCompleteButton.jsx
import styled from "styled-components";

const DoubleButton = styled.div`
  width: 580px;
  text-align: right;
  margin-inline: auto;

  .add {
    background-color: #2c65c7;
    margin-right: 36px;
  }
`;

export const AddCompleteButton = ({ handleAddForm, handleValidate }) => {
  return (
    <DoubleButton>
      <button className="add" onClick={handleAddForm}>
        追加
      </button>
      <button className="complete" onClick={handleValidate}>
        確定
      </button>
    </DoubleButton>
  );
};
1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?