Skip to main content
Version: 5.1

Saving Data

Add a Contact Model​

新しいデータベーステーブルを追加してみましょう。 api/db/schema.prisma を開いて、Postモデルの後にContactモデルを追加します:

api/db/schema.prisma
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}

generator client {
provider = "prisma-client-js"
binaryTargets = "native"
}

model Post {
id Int @id @default(autoincrement())
title String
body String
createdAt DateTime @default(now())
}

model Contact {
id Int @id @default(autoincrement())
name String
email String
message String
createdAt DateTime @default(now())
}
tip

フィールドをオプショナルにする(つまり、値として NULL を許可する) には、例えば name String? のように、データ型の後ろにクエスチョンマークを付加します。この場合、 name の値は String または NULL のいずれかになります。

次に、マイグレーションを作成し、適用します:

yarn rw prisma migrate dev

これには "create contact" のような名前を付けることができます。

Create an SDL & Service​

では、このテーブルにアクセスするためのGraphQLインターフェイスを作成します。この generate コマンドはまだ使っていません( scaffold コマンドは裏で使っていましたが):

yarn rw g sdl Contact

scaffold コマンドと同じように、api ディレクトリの下にいくつかの新しいファイルを作成します:

  1. api/src/graphql/contacts.sdl.ts : GraphQLのスキーマ定義言語でGraphQLスキーマを定義する
  2. api/src/services/contacts/contacts.ts : アプリのビジネスロジックを含む(関連するテストファイルも作成される)

how Redwood works with data(Redwoodはデータをどのように取り扱うのか) での議論を思い出してもらえれば、SDL ファイル内のクエリやミューテーションが、サービスで定義されたリゾルバに自動的にマップされることがわかります。したがって SDL ファイルを生成したときにサービスファイルも作成されるのは、依存関係があるからです。

api/src/graphql/contacts.sdl.ts を開くと、Post scaffold で作成したのと同じ Query 型と Mutation 型が Contact 用に定義されているのがわかります。 Contact 、 CreateContactInput 、 UpdateContactInput 、そして contacts と contact を含む Query 型、さらに createContact 、 updateContact 、 deleteContact を含む Mutation 型が定義されています。

api/src/graphql/contacts.sdl.js
export const schema = gql`
type Contact {
id: Int!
name: String!
email: String!
message: String!
createdAt: DateTime!
}

type Query {
contacts: [Contact!]! @requireAuth
contact(id: Int!): Contact @requireAuth
}

input CreateContactInput {
name: String!
email: String!
message: String!
}

input UpdateContactInput {
name: String
email: String
message: String
}

type Mutation {
createContact(input: CreateContactInput!): Contact! @requireAuth
updateContact(id: Int!, input: UpdateContactInput!): Contact! @requireAuth
deleteContact(id: Int!): Contact! @requireAuth
}
`

Query 型と Mutation 型の後にある @requireAuth の文字列は schema directive で、この GraphQL クエリにアクセスするにはユーザ認証が必要であることを示しています。認証を追加するまでは @requireAuth (の背後で動作する関数)は常に true を返すので、ログインしていようがいまいが、誰でもクエリを実行することができます。

CreateContactInput と UpdateContactInput とは何でしょうか?Redwood はミューテーションにおいて設定可能なフィールドを一つ一つ列挙するのではなく、GraphQL が推奨する Input Types を使います。 schema.prisma で必要なフィールドは CreateContactInput でも必要ですが (それがないと有効なレコードを作成できません)、 UpdateContactInput では必須のフィールドはありません。これは、1つのフィールドのみ、または2つのフィールド、あるいはすべてのフィールドを更新したい場合があるからです。別の方法は、更新したいフィールドの組み合わせごとに、別々の Input 型を作成することです。私たちは、更新用の Input 型を1つだけ持つことが、開発者のエクスペリエンスを最適化するための良い妥協点だと考えています。

info

Redwood はコードが id や createdAt という名前のフィールドに値を設定しようとしないことを想定しているので、これらを Input 型から外しています。しかし、もしデータベースがこれらの値を手動で設定できるようになっていれば、 CreateContactInput や UpdateContactInput を更新してこれらを追加できます。

DB のカラムはすべて schema.prisma ファイルで必須とされていたため、GraphQL Types ではデータ型に ! サフィックスをつけて必須としてマークされています(例: name: String! )。

tip

GraphQL の SDL 構文では、フィールドが必須で ある 場合、末尾の ! が必要です。覚えておいてください: schema.prisma の構文では、フィールドが必須で ない 場合、末尾の ? 文字が必要です。

Side Quest: How Redwood Deals with Data で説明したように、SDL ファイルには明示的なリゾルバが定義されていません。Redwood はシンプルな命名規則に従っています: sdl ファイル( api/src/graphql/contacts.sdl.ts )の Query 型と Mutation 型にリストされている各フィールドは、services ファイル( api/src/services/contacts/contacts.ts )の同じ名前を持つ関数と対応づけられています。

tip

Psssstttt ちょっとした秘密を教えましょう:単純な読み取り専用の SDL が必要なだけなら、次のように SDL ジェネレータにフラグを渡すことで create/update/delete ミューテーションの作成を省略することができます:

yarn rw g sdl Contact --no-crud

すべてを返す contacts 型しか生成されません。

お問い合わせページでは createContact だけが必要です。 createContact は変数 input をひとつだけ受け取ります。 input は CreateContactInput に期待されるもの、すなわち { name, email, message } に適合するオブジェクトです。このミューテーションは誰でもアクセスできるようにする必要がありますので、 @requireAuth を @skipAuth に変更する必要があります。これは認証が必要で なく 、誰でも匿名でメッセージを送ることができるようにする、というものです。 それぞれの Query と Mutation には少なくとも1つのスキーマディレクティブが必要で、さもないとエラーになることに注意してください: Redwood は "secure by default" という考え方を取り入れています。つまり、アクセスを防ぐために特別なことをしなくても、あなたのアプリケーションを安全に保とうとするのです。この場合、誤ってユーザのデータをインターネットに公開してしまうよりも、エラーを投げる方がずっと安全です!

info

偶然にも、デフォルトのスキーマディレクティブである @requireAuth は、まさにすべてのお問い合わせを返す contacts クエリに必要なもので -- 私たちブログのオーナーだけがそれらをすべて読むことができるアクセス権を持っているはずです。

メッセージの更新や削除を誰にもさせないようにするため、 これらのフィールドを完全に削除することができます。変更後の SDL ファイルは以下のようになります:

api/src/graphql/contacts.sdl.js
export const schema = gql`
type Contact {
id: Int!
name: String!
email: String!
message: String!
createdAt: DateTime!
}

type Query {
contacts: [Contact!]! @requireAuth
contact(id: Int!): Contact @requireAuth
}

input CreateContactInput {
name: String!
email: String!
message: String!
}

type Mutation {
createContact(input: CreateContactInput!): Contact! @skipAuth
}
`

SDL ファイルについては以上です。次はサービスを見てみましょう:

api/src/services/contacts/contacts.js
import { db } from 'src/lib/db'

export const contacts = () => {
return db.contact.findMany()
}

export const contact = ({ id }) => {
return db.contact.findUnique({
where: { id },
})
}

export const createContact = ({ input }) => {
return db.contact.create({
data: input,
})
}

export const updateContact = ({ id, input }) => {
return db.contact.update({
data: input,
where: { id },
})
}

export const deleteContact = ({ id }) => {
return db.contact.delete({
where: { id },
})
}

とてもシンプルですね。ここで createContact() 関数が input 引数を想定し、 create() 呼び出しの中でそれをPrismaに渡すだけであることがわかるでしょう。

ここで updateContact と deleteContact を削除することもできますが、アクセス可能な GraphQL フィールドがなくなったため、いずれにしてもクライアントからは使用できなくなりました。

これをUIに繋ぎこむ前に、yarn redwood dev を実行するだけで得られる気の利いたGUIを見てみましょう。

GraphQL Playground​

実装の道を進みすぎてから何かが欠けていることに気づく前に、 "生の" API呼び出しで実験することは良いことです。APIレイヤかWebレイヤのどちらかに typo (タイプミス)がありますか?APIレイヤにだけアクセスして調べてみましょう。

yarn redwood dev (または yarn rw dev )で開発を開始したとき、実は同時に2つ目のプロセスを起動していました。ブラウザで新しいタブを開いて、http://localhost:8911/graphql にアクセスしてください。これは GraphQL Yoga の GraphiQL で、GraphQL API 用の Web ベースの GUI です:

image

まだあまりエキサイティングではありませんが、左上の "Docs" タブを選択し、query: Query をクリックします

image

これは、SDL ファイルで定義された完全なスキーマです!プレイグラウンドはこれらの定義を取り込み、左側にオートコンプリートのヒントを表示するので、イチからクエリを作成するのに役立ちます。データベース内のすべてのブログ記事の ID を取得してみましょう;左側にクエリを入力し、 "Play" ボタンをクリックすると実行されます:

image

GraphQL Playgroundは、APIの実験や、期待通りの動作をしないクエリやミューテーションに遭遇した時のトラブルシューティングに最適な方法です。

Creating a Contact​

GraphQL ミューテーションはバックエンドで実行する準備ができているので、あとはフロントエンドから呼び出すだけです。フォームに関連するものはすべて ContactPage にあるので、そこでミューテーションを呼び出します。まず、ミューテーションを定数として定義し、後で呼び出すことにします(これはコンポーネントの外側で、 import ステートメントの直後に定義することができます):

web/src/pages/ContactPage/ContactPage.js
import { MetaTags } from '@redwoodjs/web'
import {
FieldError,
Form,
Label,
TextField,
TextAreaField,
Submit,
} from '@redwoodjs/forms'

const CREATE_CONTACT = gql`
mutation CreateContactMutation($input: CreateContactInput!) {
createContact(input: $input) {
id
}
}
`

const ContactPage = () => {
const onSubmit = (data) => {
console.log(data)
}

return (
<>
<MetaTags title="Contact" description="Contact page" />

<Form onSubmit={onSubmit} config={{ mode: 'onBlur' }}>
<Label name="name" errorClassName="error">
Name
</Label>
<TextField
name="name"
validation={{ required: true }}
errorClassName="error"
/>
<FieldError name="name" className="error" />

<Label name="email" errorClassName="error">
Email
</Label>
<TextField
name="email"
validation={{
required: true,
pattern: {
value: /^[^@]+@[^.]+\..+$/,
message: 'Please enter a valid email address',
},
}}
errorClassName="error"
/>
<FieldError name="email" className="error" />

<Label name="message" errorClassName="error">
Message
</Label>
<TextAreaField
name="message"
validation={{ required: true }}
errorClassName="error"
/>
<FieldError name="message" className="error" />

<Submit>Save</Submit>
</Form>
</>
)
}

export default ContactPage

Contacts SDL で定義した createContact ミューテーションを参照し、 input オブジェクトを渡します。このオブジェクトには、実際の名前、メールアドレス、メッセージの値が入ります。

次に、Redwoodが提供する useMutation フックを呼び出し、準備ができたときにミューテーションを実行できるようにします( import を忘れずに):

web/src/pages/ContactPage/ContactPage.js
import { MetaTags, useMutation } from '@redwoodjs/web'
import {
FieldError,
Form,
Label,
TextField,
TextAreaField,
Submit,
} from '@redwoodjs/forms'

const CREATE_CONTACT = gql`
mutation CreateContactMutation($input: CreateContactInput!) {
createContact(input: $input) {
id
}
}
`

const ContactPage = () => {
const [create] = useMutation(CREATE_CONTACT)

const onSubmit = (data) => {
console.log(data)
}

return (
<>
<MetaTags title="Contact" description="Contact page" />

<Form onSubmit={onSubmit} config={{ mode: 'onBlur' }}>
<Label name="name" errorClassName="error">
Name
</Label>
<TextField
name="name"
validation={{ required: true }}
errorClassName="error"
/>
<FieldError name="name" className="error" />

<Label name="email" errorClassName="error">
Email
</Label>
<TextField
name="email"
validation={{
required: true,
pattern: {
value: /^[^@]+@[^.]+\..+$/,
message: 'Please enter a valid email address',
},
}}
errorClassName="error"
/>
<FieldError name="email" className="error" />

<Label name="message" errorClassName="error">
Message
</Label>
<TextAreaField
name="message"
validation={{ required: true }}
errorClassName="error"
/>
<FieldError name="message" className="error" />

<Submit>Save</Submit>
</Form>
</>
)
}

export default ContactPage

create はミューテーションを呼び出す関数で、 variables キーを持つオブジェクトを受け取り、 input キーを持つ別のオブジェクトを格納します。例えば、次のように呼び出すことができます:

create({
variables: {
input: {
name: 'Rob',
email: 'rob@redwoodjs.com',
message: 'I love Redwood!',
},
},
})

思い出してほしいのですが、 <Form> はすべてのフィールドを、フィールド名をキーとするオブジェクトで提供してくれます。つまり、 onSubmit で受け取る data オブジェクトは、 input に必要な適切なフォーマットになっています!

つまり、 onSubmit 関数を更新して、受け取ったデータでミューテーションを呼び出すことができるのです:

web/src/pages/ContactPage/ContactPage.js
import { MetaTags, useMutation } from '@redwoodjs/web'
import {
FieldError,
Form,
Label,
TextField,
TextAreaField,
Submit,
} from '@redwoodjs/forms'

const CREATE_CONTACT = gql`
mutation CreateContactMutation($input: CreateContactInput!) {
createContact(input: $input) {
id
}
}
`

const ContactPage = () => {
const [create] = useMutation(CREATE_CONTACT)

const onSubmit = (data) => {
create({ variables: { input: data } })
}

return (
<>
<MetaTags title="Contact" description="Contact page" />

<Form onSubmit={onSubmit} config={{ mode: 'onBlur' }}>
<Label name="name" errorClassName="error">
Name
</Label>
<TextField
name="name"
validation={{ required: true }}
errorClassName="error"
/>
<FieldError name="name" className="error" />

<Label name="email" errorClassName="error">
Email
</Label>
<TextField
name="email"
validation={{
required: true,
pattern: {
value: /^[^@]+@[^.]+\..+$/,
message: 'Please enter a valid email address',
},
}}
errorClassName="error"
/>
<FieldError name="email" className="error" />

<Label name="message" errorClassName="error">
Message
</Label>
<TextAreaField
name="message"
validation={{ required: true }}
errorClassName="error"
/>
<FieldError name="message" className="error" />

<Submit>Save</Submit>
</Form>
</>
)
}

export default ContactPage

フォームに入力して送信してみてください。データベースに新しいContactが追加されているはずです! Prisma Studio や GraphQL Playground で確認することができます:

image
Wait, I thought you said this was secure by default and someone couldn't view all contacts without being logged in?

Remember: 私たちはまだ認証を追加していないので、誰かがログインしているという概念は今は無意味です。新しいアプリケーションでイライラするようなエラーを防ぐために、 @requireAuth ディレクティブは認証システムをセットアップするまでは単に true を返します。その時点で、ディレクティブはユーザがログインしているかどうかを判断するための実際のロジックを使用し、それに応じて動作するようになります。

Improving the Contact Form​

お問い合わせフォームは正常に動作しますが、現在いくつかの課題があります:

  • 送信ボタンを複数回クリックすると、複数回送信される
  • 投稿が成功したかどうか、ユーザにはわからない
  • サーバでエラーが発生した場合、ユーザに通知する方法がない

これらの課題を解決しましょう。

Disable Save on Loading​

フックの useMutation は、それを呼び出す関数と一緒にさらにいくつかの要素を返します。これらは、返される配列の2番目の要素としてデストラクトすることができます。気になるのは loading と error の2つです:

web/src/pages/ContactPage/ContactPage.js
// ...

const ContactPage = () => {
const [create, { loading, error }] = useMutation(CREATE_CONTACT)

const onSubmit = (data) => {
create({ variables: { input: data } })
}

return (...)
}

// ...

これで loading を見ることで、データベースの呼び出しがまだ進行中かどうかを知ることができます。多重投稿の問題を簡単に解決するには、応答がまだ進行中の場合は送信ボタンを無効にすることです。 "Save" ボタンの disabled 属性に loading の値を設定すればよいのです:

web/src/pages/ContactPage/ContactPage.js
return (
// ...
<Submit disabled={loading}>Save</Submit>
// ...
)

送信が速いので開発中は違いがわかりにくいかもしれませんが、ChromeのWebインスペクタのNetworkタブでネットワークのスロットリングを有効にすれば、遅い接続をシミュレートすることができます:

応答を待つ間、 "Save" ボタンが1〜2秒無効になるのがわかります。

Notification on Save​

次に、送信が成功したことをユーザに知らせる通知を表示しましょう。Redwoodには react-hot-toast があり、ページ上にポップアップ通知を素早く表示することができます。

useMutation は、第二引数としてオプションオブジェクトを受け取ります。オプションの1つはコールバック関数の onCompleted で、これはミューテーションが正常に完了したときに呼び出されます。このコールバックを使って toast() 関数を呼び出すと、 <Toaster> コンポーネントに表示するメッセージが追加されます。

useMutation に onCompleted コールバックを追加し、 <Toaster> コンポーネントを、 return の <Form> の直前に追加してください:

web/src/pages/ContactPage/ContactPage.js
import { MetaTags, useMutation } from '@redwoodjs/web'
import { toast, Toaster } from '@redwoodjs/web/toast'
import {
FieldError,
Form,
Label,
TextField,
TextAreaField,
Submit,
} from '@redwoodjs/forms'

const CREATE_CONTACT = gql`
mutation CreateContactMutation($input: CreateContactInput!) {
createContact(input: $input) {
id
}
}
`

const ContactPage = () => {
const [create, { loading, error }] = useMutation(CREATE_CONTACT, {
onCompleted: () => {
toast.success('Thank you for your submission!')
},
})

const onSubmit = (data) => {
create({ variables: { input: data } })
}

return (
<>
<MetaTags title="Contact" description="Contact page" />

<Toaster />
<Form onSubmit={onSubmit} config={{ mode: 'onBlur' }}>
<Label name="name" errorClassName="error">
Name
</Label>
<TextField
name="name"
validation={{ required: true }}
errorClassName="error"
/>
<FieldError name="name" className="error" />

<Label name="email" errorClassName="error">
Email
</Label>
<TextField
name="email"
validation={{
required: true,
pattern: {
value: /^[^@]+@[^.]+\..+$/,
message: 'Please enter a valid email address',
},
}}
errorClassName="error"
/>
<FieldError name="email" className="error" />

<Label name="message" errorClassName="error">
Message
</Label>
<TextAreaField
name="message"
validation={{ required: true }}
errorClassName="error"
/>
<FieldError name="message" className="error" />

<Submit disabled={loading}>Save</Submit>
</Form>
</>
)
}

export default ContactPage

Toast notification on successful submission

Toast の完全なドキュメントは ここ で読むことができます。

Displaying Server Errors​

次に、サーバのエラーをユーザに通知します。今までは client エラーしか表示できませんでした:フィールドがないか、フォーマットが正しくないといったものです。しかし、サーバサイドの制約がある場合 <Form> はそれらについて知ることができませんが、それでも何か問題があったことをユーザに知らせる必要があります。

クライアント側にメールアドレスの検証を実装しましたが、シリコンバレーに見合う開発者なら誰でも クライアントを信用してはいけない ことを知っています。

たとえクライアント側の検証を回避する人がいたとしても(エリートハッカーはいつもこれをやっています)、データベースによくないデータが入らないようにするために、APIサイドにもメールアドレスの検証を追加しましょう。

No server-side validation for some fields?

なぜ、名前、メールアドレス、メッセージの存在をサーバサイドで検証する必要がないのでしょうか?なぜなら、GraphQL がすでにそれを行っているからです!SDL ファイルの Contact 型にある String! 宣言を覚えているかもしれません:これは、これらのフィールドが APIサイドに到達した時点で null であってはならないという制約を追加するものです。もしそうであれば、GraphQL はリクエストを拒否して、クライアントにエラーを返します。

しかし、あるサービスを別のサービスの中から使い始めると、検証は行われないでしょう!GraphQLは "外部" からのリクエスト(ブラウザなど)があった場合にのみ関与します。フィールドが存在するか、正しくフォーマットされているかを本当に確認したいのであれば、サービス自体の中にバリデーションを追加する必要があります。そうすれば、誰がそのサービス関数を呼び出しても(GraphQLや他のサービス)、あなたのデータは確実にチェックされることになります。

わたしたちは自由にバリデーションを実装できる追加レイヤを設けています:名前、メールアドレス、メッセージは schema.prisma ファイルで必須として設定されているので、データベースは null が記録されないようにします。通常、入力の検証をデータベースのみに依存しないことをお勧めします:データのフォーマットはビジネスロジックの範疇であり、Redwoodのアプリではビジネスロジックはサービスにあります!

ビジネスロジックはサービスファイルに属するという話をしましたが、これはその好例です。入力の検証はよくあることなので、Redwood は Service Validations で再び私たちの生活を楽にしてくれています。

このcontacts サービスの validate 関数は、 email フィールドが実際にメールアドレスのようにフォーマットされているかどうかを検証するためのものです:

api/src/services/contacts/contacts.js
import { validate } from '@redwoodjs/api'

// ...

export const createContact = ({ input }) => {
validate(input.email, 'email', { email: true })
return db.contact.create({ data: input })
}

email への言及が多いので、分解してみましょう:

  1. 第一引数はチェックしたい値。この場合、 input にはすべてのお問い合わせデータが含まれており、 email の値がチェックしたい値
  2. 第二引数は <TextField> の name プロパティで、ページ上のどの入力フィールドでエラーが発生したかを知ることができる
  3. 第三引数は呼び出したい バリデーションディレクティブ を含むオブジェクト。この場合1つだけで、email: true は組み込みのEメールアドレスバリデータを使うことを意味する

したがって、createContact が呼ばれると、まず入力を検証し、エラーがスローされない場合にのみ、実際のデータベースへのレコード作成に進みます。

今すぐには、サーバ上でバリデーションをテストすることすらできないでしょう。なぜなら <TextField> の validation propsで、入力がメールアドレスのようにフォーマットされていることをすでにチェックしているからです。一時的にこれを削除して、不正なデータがサーバに送信されるようにしましょう:

web/src/pages/ContactPage/ContactPage.js
 <TextField
name="email"
validation={{
required: true,
- pattern: {
- value: /^[^@]+@[^.]+\..+$/,
- message: 'Please enter a valid email address',
- },
}}
errorClassName="error"
/>

以前 <Form> にはもう一つトリックがあると言ったのを覚えていますか?それがこれです!

<FormError> コンポーネントを追加し、 useMutation から取得した error 定数を渡し、 wrapperStyle に少しスタイルを指定します( import をお忘れなく)。また、<Form> に error を渡して、コンテキストをセットアップできるようにします:

web/src/pages/ContactPage/ContactPage.js
import { MetaTags, useMutation } from '@redwoodjs/web'
import { toast, Toaster } from '@redwoodjs/web/toast'
import {
FieldError,
Form,
FormError,
Label,
TextField,
TextAreaField,
Submit,
} from '@redwoodjs/forms'

const CREATE_CONTACT = gql`
mutation CreateContactMutation($input: CreateContactInput!) {
createContact(input: $input) {
id
}
}
`

const ContactPage = () => {
const [create, { loading, error }] = useMutation(CREATE_CONTACT, {
onCompleted: () => {
toast.success('Thank you for your submission!')
},
})

const onSubmit = (data) => {
create({ variables: { input: data } })
}

return (
<>
<MetaTags title="Contact" description="Contact page" />

<Toaster />
<Form onSubmit={onSubmit} config={{ mode: 'onBlur' }} error={error}>
<FormError error={error} wrapperClassName="form-error" />

<Label name="name" errorClassName="error">
Name
</Label>
<TextField
name="name"
validation={{ required: true }}
errorClassName="error"
/>
<FieldError name="name" className="error" />

<Label name="email" errorClassName="error">
Email
</Label>
<TextField
name="email"
validation={{
required: true,
}}
errorClassName="error"
/>
<FieldError name="email" className="error" />

<Label name="message" errorClassName="error">
Message
</Label>
<TextAreaField
name="message"
validation={{ required: true }}
errorClassName="error"
/>
<FieldError name="message" className="error" />

<Submit disabled={loading}>Save</Submit>
</Form>
</>
)
}

export default ContactPage

それでは無効なメールアドレスでメッセージを送信してください:

Email error from the server side

何か問題があったという素の英語のエラーメッセージが上部に表示され、 かつ 実際の入力フィールドがインラインバリデーションのようにハイライトされます!上部のメッセージは短いフォームでは過剰かもしれませんが、フォームが複数画面に及ぶ場合は重要です;何が問題だったのかがまとめて一度にわかるので、長いフォームの中で赤枠を探す手間が省けます。しかし、上部にあるメッセージボックスを使用する必要は ありません 。 <FormError> を削除するだけでよく、ハイライトはそのままになります。

info

<FormError> にはいくつかのスタイルオプションがあり、メッセージの異なる部分に付加されます:

  • wrapperStyle / wrapperClassName : メッセージ全体のコンテナ
  • titleStyle / titleClassName : "Errors prevents this form..." というタイトル
  • listStyle / listClassName : エラーのリストを格納する <ul>
  • listItemStyle / listItemClassName : 各エラーを囲むそれぞれの <li>

これは、Service Validationsでできることのほんの一部です。1度の呼び出しで複数のディレクティブを組み合わせるなど、より複雑なバリデーションを実行することができます。もし "車" を表すモデルがあり、ユーザが私たちの独占的な車のショッピングサイトにそれらを送信することができるとしたらどうでしょう?どのようにすれば、よりすぐりの電気自動車だけを入手できるでしょうか?サービスバリデーションを使えば、カスタムチェックをせずに、組み込みの validate() 呼び出しだけで、ユーザが送信できる値について非常に細かく指定することができるようになります:

export const createCar = ({ input }) => {
validate(input.make, 'make', {
inclusion: ['Audi', 'BMW', 'Ferrari', 'Lexus', 'Tesla'],
})
validate(input.color, 'color', {
exclusion: { in: ['Beige', 'Mauve'], message: "No one wants that color" }
})
validate(input.hasDamage, 'hasDamage', {
absence: true
})
validate(input.vin, 'vin', {
format: /[A-Z0-9]+/,
length: { equal: 17 }
})
validate(input.odometer, 'odometer', {
numericality: { positive: true, lessThanOrEqual: 10000 }
})

return db.car.create({ data: input })
}

独自のバリデーションロジックを組み込んで、組み込みのバリデーションと同じようにエラーを処理させることもできます:

validateWith(() => {
const oneWeekAgo = new Date()
oneWeekAgo.setDate(oneWeekAgo.getDate() - 7)

if (input.lastCarWashDate < oneWeekAgo) {
throw new Error("We don't accept dirty cars")
}
})

これで、ポンコツは排除できます!

One more thing...​

フォームが送信された後にリダイレクトを行わないので、少なくともフォームフィールドをクリアする必要があります。そのためには React Hook Form の一部である reset() 関数にアクセスする必要がありますが、(現在使用しているような) <Form> の基本的な使い方ではアクセスすることができません。

Redwood には useForm() ( React Hook Form 由来)というフックがあり、通常は <Form> 内で呼び出されます。フォームをリセットするには、このフックを自分自身で呼び出す必要があります。しかし useForm() が提供する機能は Form の中で使用する必要があります。ここでは、その方法を説明します:

まず、 useForm をインポートします:

web/src/pages/ContactPage/ContactPage.js
import {
FieldError,
Form,
FormError,
Label,
Submit,
TextAreaField,
TextField,
useForm,
} from '@redwoodjs/forms'

そして、コンポーネントの中で呼び出します:

web/src/pages/ContactPage/ContactPage.js
const ContactPage = () => {
const formMethods = useForm()
//...

最後に、 <Form> に対して useForm() から取得した formMethods を使うよう指示します:

web/src/pages/ContactPage/ContactPage.js
return (
<>
<Toaster />
<Form
onSubmit={onSubmit}
config={{ mode: 'onBlur' }}
error={error}
formMethods={formMethods}
>
// ...

これで、 toast() を呼び出した後に formMethods で reset() を呼べるようになりました:

web/src/pages/ContactPage/ContactPage.js
// ...

const [create, { loading, error }] = useMutation(CREATE_CONTACT, {
onCompleted: () => {
toast.success('Thank you for your submission!')
formMethods.reset()
},
})

// ...
caution

これで <TextField> にメールアドレス検証を戻すことができますが、念のためサーババリデーションも残しておきましょう。

ページ全体はこちらです:

web/src/pages/ContactPage/ContactPage.js
import { MetaTags, useMutation } from '@redwoodjs/web'
import { toast, Toaster } from '@redwoodjs/web/toast'
import {
FieldError,
Form,
FormError,
Label,
Submit,
TextAreaField,
TextField,
useForm,
} from '@redwoodjs/forms'

const CREATE_CONTACT = gql`
mutation CreateContactMutation($input: CreateContactInput!) {
createContact(input: $input) {
id
}
}
`

const ContactPage = () => {
const formMethods = useForm()

const [create, { loading, error }] = useMutation(CREATE_CONTACT, {
onCompleted: () => {
toast.success('Thank you for your submission!')
formMethods.reset()
},
})

const onSubmit = (data) => {
create({ variables: { input: data } })
}

return (
<>
<MetaTags title="Contact" description="Contact page" />

<Toaster />
<Form
onSubmit={onSubmit}
config={{ mode: 'onBlur' }}
error={error}
formMethods={formMethods}
>
<FormError error={error} wrapperClassName="form-error" />

<Label name="name" errorClassName="error">
Name
</Label>
<TextField
name="name"
validation={{ required: true }}
errorClassName="error"
/>
<FieldError name="name" className="error" />

<Label name="email" errorClassName="error">
Email
</Label>
<TextField
name="email"
validation={{
required: true,
pattern: {
value: /^[^@]+@[^.]+\..+$/,
message: 'Please enter a valid email address',
},
}}
errorClassName="error"
/>
<FieldError name="email" className="error" />

<Label name="message" errorClassName="error">
Message
</Label>
<TextAreaField
name="message"
validation={{ required: true }}
errorClassName="error"
/>
<FieldError name="message" className="error" />

<Submit disabled={loading}>Save</Submit>
</Form>
</>
)
}

export default ContactPage

以上です! React Hook Form は <Form> が公開しないたくさんの 機能 を提供します。それらの機能を利用したい場合は、自分で useForm() を呼び出しますが、返されたオブジェクト (ここでは formMethods と呼びます) を <Form> に props として渡して、バリデーションやその他の機能が動作し続けるようにしなければなりません。

info

onBlur フォームの設定は、自分で useForm() を呼び出すと動作しなくなることにお気づきかもしれません。これは、Redwood が裏で useForm() を呼び出し、あなたが <Form> に与えた config propsを自動的に渡しているためです。Redwood はもはやあなたのために useForm() を呼び出さないので、もし何らかのオプションを渡す必要がある場合は手動で行う必要があります:

web/src/pages/ContactPage/ContactPage.js
const ContactPage = () => {
const formMethods = useForm({ mode: 'onBlur' })
//...

公開サイトはかなりいい感じになってきましたね。ブログ記事を作成したり編集したりする管理機能はどうでしょうか?管理セクションのようなものに移動して、ログインの後ろに置くべきでしょう。そうすれば、URLを詮索するランダムなユーザが勝手に割引医薬品の広告を作成することができなくなります。