入力内容をInputタグからフォーカスを外した時に保存する

# はじめに

Inputフィールドに入力した後、「保存ボタン」や「送信ボタン」を押すことなく、内容保存できる機能の作成となると複数方法があると思います。

1. Qiitaのように、記事の内容を1文字追加・削除する度にPOST Requestをサーバーサイドに投げる
2. 入力が始まって、何秒か経過したらPOST Requestをサーバーサイドに投げる
3. Inputフィールド以外の何かをクリックしたらPOST Requestをサーバーサイドに投げる
4. 入力内容をInputタグからフォーカスを外した時に保存する

まだあるかもしれませんが、今回は4番目の内容についてまとめます。
テストで使用するメソッドはこちらです。

```ts
await fireEvent.blur(getByLabelText('username'))
```

[Testing Library 公式Doc](https://testing-library.com/docs/vue-testing-library/api/#touchelem)

# 完成図
![GIF Maker video to GIF.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3878075/8dda266d-eced-9f38-1a87-012f02069bd1.gif)


1. 「Blur Sample」のタイトルが
2. inputフィールド
3. 「Memo Record」のサブタイトル
4. DBから引っ張ってきた文字の表示

# 全体構成

Backend側の実装も行なっています。
本筋と異なるので記載はしませんが、下記画像のような構成になっています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3878075/7837aa48-9fa7-3b47-b40b-fd108b996a4f.png)

# テスト & 実装
TDDの勉強も兼ねて、テストと実装を書いたので内容は省略しますが、細かくテストを書いていきました。

Screen層の役割は、Repository層のpostMemoメソッドに正しい引数を渡すことです。
Test DoubleのSpyを使用してテスト用のRepositoryを作成します。

```ts:SpyRepository.ts
import { UserRepository } from "../../repository/UserRepository.ts";
import { UserMemo } from "../../type/TypeMemoRepository.ts";

export class SpyUserRepository implements UserRepository {
    postMemo_argumentValue:UserMemo | null = null
    
    postMemo(inputObject: UserMemo): void {
        this.postMemo_argumentValue = inputObject
    }
    async getAllMemo(): Promise<UserMemo[]> {
        // ...省略
    }

}
```

次にScreen層（App.tsx）のテストです。

```tsx:App.test.tsx
import {describe, expect, test} from "vitest"
import { screen, render, fireEvent } from "@testing-library/react"
import App from "../App.tsx";
import { MemoScreen } from "../screens/MemoScreen.tsx";
import { SpyUserRepository } from "./repository/SpyUserRepository.ts";
import { userEvent } from "@testing-library/user-event";

describe("Appのテスト", () => {
        test('Appをレンダリングした時に「Blur Sample」の文字が見える', () => {
            // ...省略
        })

        test('Appをレンダリングした時に、inputフィールドがある', () => {
            // ...省略
        })

        test('Appをレンダリングした時に、MemoScreenコンポーネントをレンダリングする', () => {
            // ...省略
        })

        test('メモを入力し、フォーカスを外した時にuserRepositoryのpostMemoメソッドを正しい引数で呼んでいる', async() => {
               const spyUserRepository = new SpyUserRepository()
                render(<App userRepository={spyUserRepository}/>)
                const inputField = screen.getByRole('textbox')
                await userEvent.type(inputField, 'Test Memo')

                await fireEvent.blur(inputField)

                expect(spyUserRepository.postMemo_argumentValue?.memo).toEqual('Test Memo')
        })
})
```

そして、実装です。

```tsx:App.tsx
import './App.css'
import { MemoScreen } from './screens/MemoScreen'
import { DefaultUserRepository, UserRepository } from "./repository/UserRepository.ts";
import { useEffect, useRef, useState } from "react";
import { UserMemo } from "./type/TypeMemoRepository.ts";

type Props = {
    userRepository?: UserRepository
}

function App({ userRepository = new DefaultUserRepository() }: Props) {
    const inputRef = useRef<HTMLInputElement>(null)
    const [memo, setMemo] = useState<UserMemo[]>([])
    const postMemo = async () => {
        if (inputRef.current) {
            const inputObject = {
                memo : inputRef.current.value
            }
            await userRepository.postMemo(inputObject)
            inputRef.current.value = ''
        }
    }

    useEffect(() => {
        const getAllMemo = async () => {
            const res = await userRepository.getAllMemo()
            setMemo(res)
        }
        getAllMemo()
    }, [memo])

    return (
        <>
            <h1>Blur Sample</h1>
            <input
                ref={inputRef}
                onBlur={postMemo}
            />
            <MemoScreen memo={memo}/>
        </>
    )
}

export default App
```

テスト側：`await fireEvent.blur(<対象のelement>)`
実装側：`<input onBlur={<保存処理>} />`

完成図のように、inputフィールドからフォーカスを外した時に、保存ができるようになりました。

# 最後に
すごく簡単はフルスタックアプリでしたが、学びが多かったです！
もっと作っていくことで、基本的なことを頭に叩き込んでいこうと思います！！