作成日: 2023-10-10T08:21:14.008Z
前回まではログインの機能を作っていましたね。
今回からはメインのtodo機能を作っていきます。
APIを叩くイメージ等がここでついていくのではないかと思いますので
頑張ってやっていきましょう。
作っていく機能はtodoの投稿部分と取得部分です。
このように登録したものが見れて
このように内容を入れると
ちゃんと朝食というものが入っていますね。
他の工夫点としては、
このtodoリストが出る前に「Loding...」というものが出たり
textFieldが空の時はボタンを押せなくしたりといった工夫をしています。
todo:{
title:"TODO APP",
description:"自分だけのTODOを作成しよう!",
greeting:" Hello!!!!!",
notLogin:"ログインしてから来いやー",
buttonTitle:"登録",
attribute:{
todo:{
labelName:"todo",
},
},
logingMsg:"Loding..."
}
postTodo:"http://localhost:8080/todo/postTodo/",
getTodo:"http://localhost:8080/todo/getList/"
//todoの型
export type inputTodo={
todo:string;
}
//todopostの型
export type todoType={
id:string;
todo:string;
checked:number;
}
ここまでは前準備です。
今回は「api」というフォルダにtodo.tsというファイルを作ってここにtodo系のAPIを叩くコードを書いていきたいと思います。
まずは投稿の部分から実装しておきます。
動作確認のために新規登録、もしくはログインをしてtodoの画面に遷移しておいてください。
import linkName from "@/shared/linkName";
import { todoType } from "@/shared/type";
//todo投稿
export const postTodo=async(data:todoType,id:string)=>{
const res=await fetch(`${linkName.postTodo}${id}`,{
method:"POST",
headers:{
'Content-Type':'application/json'
},
body:JSON.stringify(data)
});
const resData=await res.json();
return resData.data as todoType[];
}
この関数は入力したデータとユーザidを引数としてそのうちのidを使って元のURLと組み合わせています。
バックエンドではidをパラメータにしてDBのテーブルを作成していたのでユーザのテーブル名は
idとなります。それらを利用してdataをbodyとしてPOSTしています。レスポンスとして全てのtodoリストが返ってくるのでそれをreturnしておきましょう。
次にリスト取得のAPIです。
//todo取得
export const getTodo=async(id:string)=>{
const res=await fetch(`${linkName.getTodo}${id}`);
const resData=await res.json();
return resData.data as todoType[];
}
こちらは非常にシンプル。
APIを叩いてレスポンスを返すだけです。
今回はログインしてから取りに行くのでCSR(クライアントサイドレンダリング)でレンダリングするのでこのファイルに書く必要はないのですが
APIを叩く関数はAPIを叩く関数のファイルとまとめておきたいのでそうしています。
全体コードをここに載せておきます。
import linkName from "@/shared/linkName";
import { todoType } from "@/shared/type";
//todo投稿
export const postTodo=async(data:todoType,id:string)=>{
const res=await fetch(`${linkName.postTodo}${id}`,{
method:"POST",
headers:{
'Content-Type':'application/json'
},
body:JSON.stringify(data)
});
const resData=await res.json();
return resData.data as todoType[];
}
//todo取得
export const getTodo=async(id:string)=>{
const res=await fetch(`${linkName.getTodo}${id}`);
const resData=await res.json();
return resData.data as todoType[];
}
新規登録やログインの画面でも作りましたが、今回はvalidation機能を入れないので別で作ります。
「todo」フォルダの中に「inputForm」フォルダを作成してその下のファイルでコードを書いていきます。
import { TextField } from "@mui/material";
import { Dispatch, SetStateAction } from "react";
interface props{
labelName:string;
value:string;
setValue:Dispatch<SetStateAction<string>>;
}
const InputForm = ({labelName,value,setValue}:props) => {
return (
<div>
<TextField
required
value={value}
onChange={(e)=>setValue(e.target.value)}
/>
</div>
);
}
export default InputForm;
新たにonChangeが入っています。textFieldの中身が変わるごとに発火されるものであり今回はその部分の値をvalueに保存しています。
Dispatch<SetStateAction<string>>;はuseStateの代入する方ですね。
const [state,setState]=useState("state");
の部分のsetStateにあたるものです。Reactが出て仮想DOMというものが出てきました。通常のHTML、CSS、JSの場合は一部分が変更されたら全ての画面の要素を描画し直していました。では仮想DOMになって何が変わったかというと以前の状態と現在の状態の差分を見て変更部分のみを描画するようになりました。
ボタンに関しては今回はinputTextが空になったらボタンを押せなくする(disabledする)という形にしようと思います。
「todo」フォルダの下に「postButton」というフォルダを作りましょう。そのフォルダでコードを書いていきます。
import { inputTodo } from "@/shared/type";
import { Button } from "@mui/material";
interface props{
clickEvent:()=>Promise<void>;
buttonTitle:string;
isDisabled:boolean;
}
const PostButton = ({clickEvent,buttonTitle,isDisabled}:props) => {
return (
<Button
variant="contained"
onClick={clickEvent}
disabled={isDisabled}
>
{buttonTitle}
</Button>
);
}
export default PostButton;
では作ったcomponentを呼び出しさらに処理を加えていきたいと思います。
「todo」フォルダの下に「todoInputForm」というフォルダを作ってそこでコードを書いていきましょう。
'use client'
import ja from "@/shared/ja";
import { todoType } from "@/shared/type";
import styles from "./style.css";
import { Dispatch, SetStateAction, useState } from "react";
import InputForm from "../inputForm";
import PostButton from "../postButton";
import { v4 } from "uuid";
import { postTodo } from "@/api/todo";
interface props{
id:string;
setTodos:Dispatch<SetStateAction<todoType[]>>;
}
const TodoInputForm = ({id,setTodos}:props) => {
const [todo,setTodo]=useState<string>("");
const onClick=async()=>{
const uuid:string=v4();
const setData:todoType={
id:uuid,
todo:todo,
checked:0,
}
const data:todoType[]=await postTodo(setData,id);
setTodos(data);
setTodo("");
};
return (
<div>
<div>
<InputForm
labelName={ja.todo.attribute.todo.labelName}
value={todo}
setValue={setTodo}
/>
</div>
<div>
<PostButton
clickEvent={onClick}
buttonTitle={ja.todo.buttonTitle}
isDisabled={todo?false:true}
/>
</div>
</div>
);
}
export default TodoInputForm;
ここではuuidをimportしていますが、uuidと@types/uuidをinstallしておいてください。
定数setTodoを紛らわしくしてしまいましたが、こちらはtextField用のものでsetTodosはデータが入ったものです。
onClick関数ではidを作りAPIを叩くコードを呼び出しています。
呼び出して返ってきたデータをsetTodosに入れています。さらにtextFieldの部分を空文字にしています。
todoのリストを表示するcomponentを作成します。
「todo」フォルダに「todoList」というフォルダを作ります。
'use client'
import { getTodo } from "@/api/todo";
import ja from "@/shared/ja";
import { todoType } from "@/shared/type";
import { Dispatch, SetStateAction, useEffect, useState } from "react";
interface props{
id:string|null;
todos:todoType[];
setTodo:Dispatch<SetStateAction<todoType[]>>;
}
const TodoList = ({id,todos,setTodo}:props) => {
const [loadingMsg,setLodingMsg]=useState<string>(ja.todo.logingMsg);
const firstFunction=async(userId:string)=>{
const data:todoType[]=await getTodo(userId);
setTodo(data);
}
useEffect(()=>{
if(id){
firstFunction(id);
setLodingMsg("");
}else{
setLodingMsg(ja.todo.logingMsg)
}
},[id])
return (
<>
{loadingMsg?(
<div>
<p>{loadingMsg}</p>
</div>
):(
<ul>
{todos.map((item:todoType)=>(
<li key={item.id}>
{item.todo}
</li>
))}
</ul>
)}
</>
);
export default TodoList;
propsとしてユーザidとtodoのデータ等を受け取っています。
idが受け取れてない場合があるのでその時のために「Loding...」を描画させておきます。
なので、useStateを宣言しておきます。さらに、今回はuseEffectを使用しています。
ユーザidが変わった時に 第一引数の関数を発火させるようにしてあります。
第二引数を宣言しないと無限に第一引数の関数が呼ばれるので注意です。
初回時の時のみ発火させたい場合は第二引数を[]と空の配列を宣言しておきましょう。
そして描画部分では「?」を使って場合分けをしています。ここでは「lodingMsg」に文字が入っているか否かの
判定をしています。普通if文を思い浮かべるかもしれませんがjsx形式ではifを使うことはできません。forも同様です。
ifやfor以外の書き方でうまいこと書いていかなくてはならないのです。条件分岐の場合は基本的に「三項演算子」と呼ばれる「?」を用います。
これで分岐を描くことができます。「()」の中身は基本的に親タグは1つと決められています。複数のタグを書く事ができないためその場合は、
「<></>」や「<div></div>」で囲いましょう。
ここでは条件分岐以外にmap関数を使って要素を順番に描画しています。
「<li>」を使っていますが「key」という属性があると思います。map関数で描画するときは固有のキーを宣言してあげる必要があります。
今回はidを指定します。idなど固有のキーがない場合はmap関数の第二引数がindexなのでそれを指定してあげると良いでしょう。型はnumber型になります。
「Hello!!!〇〇」と描画させるcomponentを作成します。
「todo」フォルダの下に「greeting」フォルダを作成します。
import ja from "@/shared/ja";
interface props{
name:string;
}
const Greeting = ({name}:props) => {
return (
<p>
{ja.todo.greeting}{name}
</p>
);
}
export default Greeting;
idとnameがない場合はログインされていない状況です。
なので警告するcomponentを作っておきます。
「todo」フォルダの下に「notLogin」フォルダを作ります。
import ja from "@/shared/ja";
const NotLogin = () => {
return (
<div>
{ja.todo.notLogin}
</div>
);
}
export default NotLogin;
page.tsxにて今まで作ってきたcomponentを呼び出していきます。
'use client'
import { ReadonlyURLSearchParams, useSearchParams } from "next/navigation";
import Title from "../components/title";
import Greeting from "./greeting";
import styles from "./style.css";
import NotLogin from "./notLogin";
import TodoInputForm from "./todoInputForm";
import TodoList from "./todoList";
import { useState } from "react";
import { todoType } from "@/shared/type";
const Todo = () => {
const searchParams:ReadonlyURLSearchParams=useSearchParams();
const name:string|null=searchParams.get('name');
const id:string|null=searchParams.get('id');
const [todo,setTodo]=useState<todoType[]>([]);
return (
<main className={styles.containar}>
{id&&name?(
<>
<Title/>
<Greeting
name={name}
/>
<TodoInputForm
id={id}
setTodos={setTodo}
/>
<TodoList
id={id}
todos={todo}
setTodo={setTodo}
/>
</>
):(
<NotLogin/>
)}
</main>
);
}
export default Todo;
最後にstylesheetを書きますが、各ページフォルダ直下のstyle.css.tsと変わらないのでファイルをコピーして貼り付けても問題ないです。
一応記述はしておきます。
import { style } from "@vanilla-extract/css";
const styles={
containar:style({
width:"75%",//長さはブラウザの横の75%
margin:"150px auto",// 上に50pxで以外は中央よせに,
textAlign:"center",//文字を中央に寄せる(デフォルトは中央添え)
}),
}
export default styles;
少し長かったですが、お疲れ様でした。
todoの画面を作り終える