今回やること
前回は、コンポーネントや初期データを 役割ごとのファイルへ整理しました。
今回は、CreateScreenに
名前・カテゴリ・ステータス・価格・メモを入力するフォームを作ります。
入力途中の値はStateで管理し、
入力内容が変わったときにStateも更新できるようにします。
最後に、入力内容を
newItemという新しいデータにまとめ、
Consoleで確認します。
公開日:更新日:
前回は、コンポーネントや初期データを 役割ごとのファイルへ整理しました。
今回は、CreateScreenに
名前・カテゴリ・ステータス・価格・メモを入力するフォームを作ります。
入力途中の値はStateで管理し、
入力内容が変わったときにStateも更新できるようにします。
最後に、入力内容を
newItemという新しいデータにまとめ、
Consoleで確認します。
今回はCreateScreenの中でStateを使うため、
まずuseStateを読み込みます。
CreateScreen.jsxの先頭に追加します。
import { useState } from "react";
次に、フォームに最初から入れておく値を用意します。
CreateScreen.jsxのimport文の下、
CreateScreenコンポーネントの外側に
initialInputValuesを追加してください。
const initialInputValues = {
name: "",
category: "本",
status: "気になる",
price: "",
memo: "",
};
名前・価格・メモは空の状態から始め、 カテゴリとステータスには最初から表示する値を入れています。
続いて、CreateScreenコンポーネントの中に
Stateを追加します。
const [inputValues, setInputValues] = useState(initialInputValues);
inputValuesが現在の入力値を保持するstate変数、
setInputValuesがStateを更新するための更新関数です。
Stateの値には、名前・カテゴリ・ステータス・価格・メモを持つ
1つのオブジェクトを使っています。
いきなり5つすべての入力欄を作るのではなく、 まずは名前だけを作ってみましょう。
CreateScreenのh2の下に
formを追加し、
その中に名前の入力欄を作ります。
<form>
<label>
名前
<input value={inputValues.name} />
</label>
</form>
value={inputValues.name}によって、
inputに表示する値をstate変数
inputValuesのnameと結び付けています。
ファイルを保存してブラウザを開き、
名前の入力欄へ文字を入力してみましょう。
この状態では、文字を入力しようとしても入力内容は変わりません。
inputに表示される内容が
inputValues.nameによって決まっているためです。
文字を入力したときに、
State側の値も更新する必要があります。
文字を入力したときにStateも更新できるように、
名前の入力欄へonChangeを追加します。
先ほど追加したformの部分を、次のように書き換えてください。
<form>
<label>
名前
<input
value={inputValues.name}
onChange={(event) => {
setInputValues({
...inputValues,
name: event.target.value,
});
}}
/>
</label>
</form>
ファイルを保存してブラウザを確認し、
名前の入力欄へ文字を入力してみましょう。
今度は入力した文字が表示されればOKです。
onChangeを追加すると入力できるようになりました。
ここで、先ほど追加した処理を順番に確認しましょう。
onChange={...}
onChangeは、
入力内容が変わったときに処理を実行するために使います。
event.target.value
入力内容が変わると、
そのときの情報がeventとして渡されます。
event.targetは今回変更されたinputを表し、
そのvalueから現在入力されている文字を取得できます。
setInputValues({
...inputValues,
name: event.target.value,
});
setInputValuesを使って、
入力された文字でStateを更新します。
...inputValuesで現在の入力値をコピーし、
nameだけを新しい入力値に変更しています。
文字を入力する
↓
onChangeが実行される
↓
event.target.valueで入力値を取得する
↓
現在のinputValuesをコピーする
↓
nameを新しい値に変更する
↓
setInputValuesでStateを更新する
↓
新しい値がinputに表示される
名前の入力欄は動くようになりました。
ただし、このままカテゴリ・ステータス・価格・メモにも
同じようなonChangeを書くと、
似た処理を何度も書くことになります。
そこで、どの入力欄が変更された場合でも使えるように、
入力値を更新する処理をhandleChangeへまとめます。
CreateScreenコンポーネントの中で、
Stateの下に次の関数を追加してください。
const handleChange = (event) => {
const fieldName = event.target.name;
const value = event.target.value;
const newInputValues = { ...inputValues };
newInputValues[fieldName] = value;
setInputValues(newInputValues);
};
handleChangeでは、
入力欄のname属性を使って
どの項目を更新するかを切り替えます。
まず、名前の入力欄へ
name="name"を追加し、
onChangeにはhandleChangeを指定します。
<label>
名前
<input
name="name"
value={inputValues.name}
onChange={handleChange}
/>
</label>
name属性によって、
この入力欄がinputValuesの
nameを変更する入力欄だと区別できるようになります。
const fieldName = event.target.name;
event.target.nameから、
変更された入力欄のname属性を取得します。
名前の入力欄ではname="name"なので、
fieldNameには"name"が入ります。
fieldName → "name"
入力された値は、先ほどと同じように
event.target.valueから取得します。
const value = event.target.value;
const newInputValues = { ...inputValues };
newInputValues[fieldName] = value;
newInputValuesを作る考え方は、
先ほどのState更新と同じです。
今回は、fieldNameをプロパティ名として使うことで、
変更する項目を切り替えています。
名前の入力欄ならfieldNameは
"name"なので、実際には次の処理になります。
newInputValues["name"] = value;
カテゴリの入力欄にname="category"を付ければ、
fieldNameには"category"が入り、
同じhandleChangeでcategoryを更新できます。
名前を変更
↓
fieldName → "name"
↓
newInputValues["name"]を更新
カテゴリを変更
↓
fieldName → "category"
↓
newInputValues["category"]を更新
最後に、変更したnewInputValuesで
Stateを更新します。
setInputValues(newInputValues);
これで、入力欄ごとに別々の処理を書くのではなく、
1つのhandleChangeで入力値を更新できるようになりました。
名前の入力欄を作れたので、 残りの入力欄も追加していきましょう。
カテゴリとステータスでは、
前回options.jsに用意した選択肢を使います。
CreateScreen.jsxの先頭に、
次のimport文を追加してください。
import { categories, statuses } from "../options";
CreateScreen.jsxは
componentsフォルダの中にあり、
options.jsはその1つ上のsrcにあります。
そのため、1つ上のフォルダを表す
../を使って読み込んでいます。
名前の入力欄の下に、
カテゴリとステータスのselectを追加します。
<label>
カテゴリ
<select
name="category"
value={inputValues.category}
onChange={handleChange}
>
{categories.map((value) => (
<option key={value} value={value}>
{value}
</option>
))}
</select>
</label>
<label>
ステータス
<select
name="status"
value={inputValues.status}
onChange={handleChange}
>
{statuses.map((value) => (
<option key={value} value={value}>
{value}
</option>
))}
</select>
</label>
categoriesとstatusesを
それぞれmap()で処理し、
1つずつoptionとして表示しています。
map()とkeyの考え方は、
一覧表示のときと同じです。
また、name="category"と
name="status"を付けているため、
どちらも同じhandleChangeで更新できます。
続いて、価格とメモを ステータスの入力欄の下に追加します。
<label>
価格
<input
name="price"
type="number"
min="0"
value={inputValues.price}
onChange={handleChange}
/>
</label>
<label>
メモ
<textarea
name="memo"
value={inputValues.memo}
onChange={handleChange}
/>
</label>
価格にはtype="number"、
メモには複数行入力できるtextareaを使います。
ファイルを保存して、 名前・カテゴリ・ステータス・価格・メモを それぞれ入力・変更できることを確認しましょう。
入力欄がそろったので、 フォームの最後に「登録する」ボタンを追加し、 送信時の処理も作りましょう。
formの最後に、次のボタンを追加します。
<button type="submit">
登録する
</button>
type="submit"を指定すると、
このボタンはフォームを送信するためのボタンになります。
フォームが送信されたときの処理は、
onSubmitで受け取ります。
これまで使ってきたformに、
onSubmitを追加します。
<form onSubmit={handleSubmit}>
次に、CreateScreenコンポーネントの中へ
handleSubmitを追加します。
const handleSubmit = (event) => {
event.preventDefault();
console.log(inputValues);
};
HTMLのformには、本来のフォーム送信処理があります。
フォームを通常どおり送信すると、
ブラウザは指定された送信先へデータを送り、
ページの移動や再読み込みを行います。
今回は、その標準の送信処理ではなく、
React側のJavaScriptで入力内容を処理したいので、
event.preventDefault()を使います。
preventDefault()は、
イベントが本来行う標準の動作を止めるためのメソッドです。
「登録する」を押す
↓
formのonSubmitが実行される
↓
handleSubmitが呼ばれる
↓
preventDefault()で通常のフォーム送信を止める
↓
JavaScriptで入力内容を処理する
名前や価格、メモなどを入力して「登録する」を押し、
ブラウザの開発者ツールでConsoleタブを確認しましょう。
入力した内容を持つオブジェクトが表示されれば、
フォームの値をStateで正しく管理し、送信時に取得できています。
このconsole.log(inputValues)は、
次のセクションでnewItemを確認するコードへ変更します。
入力内容を取得できたので、 登録する1件分のデータを作りましょう。
今回は、名前を必須にし、名前の前後にある空白を取り除きます。
また、inputから取得した価格は文字列として扱われるため、
数値へ変換してから登録します。
名前のinputにrequiredを追加してください。
<label>
名前
<input
name="name"
value={inputValues.name}
onChange={handleChange}
required
/>
</label>
requiredを付けると、
名前が未入力のままフォームを送信しようとしたときに、
ブラウザが入力を求めてくれます。
登録するデータは、
newItemという新しいオブジェクトにまとめます。
const newItem = {
...inputValues,
name: inputValues.name.trim(),
price: Number(inputValues.price) || 0,
};
...inputValues:現在の入力内容をコピーするtrim():名前の前後にある空白を取り除くNumber():価格を数値へ変換する
価格が未入力の場合は、今回は0として扱います。
handleSubmitを次のように変更します。
const handleSubmit = (event) => {
event.preventDefault();
const newItem = {
...inputValues,
name: inputValues.name.trim(),
price: Number(inputValues.price) || 0,
};
console.log(newItem);
};
フォームに値を入力して「登録する」を押し、
Consoleを確認してください。
入力した内容を持つnewItemが表示されればOKです。
これで、新規登録画面で入力した内容を 登録する1件分のデータとしてまとめられるようになりました。
今回登場した内容を、 ReactとJavaScriptに分けて整理します。
| 種類 | 今回登場したもの |
|---|---|
| React | State、state変数、更新関数、value、onChange、onSubmit |
| JavaScript | オブジェクト、スプレッド構文、trim()、Number()、preventDefault() |
フォームの入力値をStateと結び付けたり、
onChangeやonSubmitで処理したりする部分は、
Reactでよく使う書き方です。
一方、オブジェクトをコピーしたり、
文字列や数値を整えたりする部分はJavaScriptです。
inputValuesをstate変数、setInputValuesを更新関数として使ったvalueで入力欄とStateの値を結び付けたonChangeで入力内容の変更を受け取ったevent.target.valueで現在の入力値を取得したname属性で入力欄を区別したevent.target.nameで変更された項目を取得したhandleChangeで複数の入力欄の更新処理を共通化したcategoriesとstatusesから選択肢を表示したonSubmitでフォーム送信を受け取ったevent.preventDefault()でフォームの標準送信を止めたnewItemを作った
今回変更したCreateScreen.jsxの完成コードです。
import { useState } from "react";
import { categories, statuses } from "../options";
const initialInputValues = {
name: "",
category: "本",
status: "気になる",
price: "",
memo: "",
};
export default function CreateScreen({ onBack }) {
const [inputValues, setInputValues] = useState(initialInputValues);
const handleChange = (event) => {
const fieldName = event.target.name;
const value = event.target.value;
const newInputValues = { ...inputValues };
newInputValues[fieldName] = value;
setInputValues(newInputValues);
};
const handleSubmit = (event) => {
event.preventDefault();
const newItem = {
...inputValues,
name: inputValues.name.trim(),
price: Number(inputValues.price) || 0,
};
console.log(newItem);
};
return (
<main className="form-screen">
<header className="screen-header">
<h1>気になるものリスト</h1>
<div className="header-actions">
<button type="button" className="secondary" onClick={onBack}>
一覧へ
</button>
</div>
</header>
<h2 className="screen-title">新規登録画面</h2>
<form onSubmit={handleSubmit}>
<label>
名前
<input
name="name"
value={inputValues.name}
onChange={handleChange}
required
/>
</label>
<label>
カテゴリ
<select
name="category"
value={inputValues.category}
onChange={handleChange}
>
{categories.map((value) => (
<option key={value} value={value}>
{value}
</option>
))}
</select>
</label>
<label>
ステータス
<select
name="status"
value={inputValues.status}
onChange={handleChange}
>
{statuses.map((value) => (
<option key={value} value={value}>
{value}
</option>
))}
</select>
</label>
<label>
価格
<input
name="price"
type="number"
min="0"
value={inputValues.price}
onChange={handleChange}
/>
</label>
<label>
メモ
<textarea
name="memo"
value={inputValues.memo}
onChange={handleChange}
/>
</label>
<button type="submit">
登録する
</button>
</form>
</main>
);
}