Building a Component the Redwood Way
このブログに足りないものは何でしょう?コメントですね。簡単なコメントエンジンを追加して、人々が私たちのブログ記事に完全に合理的で十分なコメントを残せるようにしましょう。インターネットですからね。何か問題ありますか?
私たちが構築しなければならない機能は、大きく2つあります:
- コメントフォームと作成
- コメントの取得と表示
どの順番で構築するかは、私たち次第です。簡単にするために、まずコメントの取得と表示から始めて、それからより複雑な新しいコメントを作成するためのフォームとサービスを追加する作業に移りましょう。もちろんRedwoodですから、フォームやサービスだって それほど 複雑ではありません!
Storybook
それでは1つのコメントを表示するコンポーネントを作りましょう。まずはジェネレータです:
yarn rw g component Comment
Storybook が更新され、 "生成された" コメントのストーリーが準備されるはずです:
ユーザに何を要求するか、コメントで表示させたい内容を考えてみましょう。名前とコメントそのものの内容だけではどうでしょうか?あと、そのコメントが作成された日付/時刻を投げ込みます。 Comment コンポーネントを更新して、 comment
オブジェクトが3つのプロパティを受け付けるようにしましょう。
- JavaScript
- TypeScript
const Comment = ({ comment }) => {
return (
<div>
<h2>{comment.name}</h2>
<time dateTime={comment.createdAt}>{comment.createdAt}</time>
<p>{comment.body}</p>
</div>
)
}
export default Comment
// Just a temporary type. We'll replace this later
interface Props {
comment: {
name: string
createdAt: string
body: string
}
}
const Comment = ({ comment }: Props) => {
return (
<div>
<h2>{comment.name}</h2>
<time dateTime={comment.createdAt}>{comment.createdAt}</time>
<p>{comment.body}</p>
</div>
)
}
export default Comment
そのファイルを保存してStorybookをリロードすると、ぶっ壊れます:
このコメントオブジェクトを取り込んで props として渡すよう、ストーリーを更新しなければなりません:
- JavaScript
- TypeScript
import Comment from './Comment'
export const generated = () => {
return (
<Comment
comment={{
name: 'Rob Cameron',
body: 'This is the first comment!',
createdAt: '2020-01-01T12:34:56Z'
}}
/>
)
}
export default {
title: 'Components/Comment',
component: Comment,
}
import Comment from './Comment'
export const generated = () => {
return (
<Comment
comment={{
name: 'Rob Cameron',
body: 'This is the first comment!',
createdAt: '2020-01-01T12:34:56Z'
}}
/>
)
}
export default {
title: 'Components/Comment',
component: Comment,
}
GraphQLから送られてくる日付は ISO8601形式 なので、ここでも同じフォーマットにしなければなりません。
Storybook は再読み込みしてよりhappyになるでしょう:
この Comment コンポーネントに、少しスタイリングと日付変換を足して、素敵な完成されたデザイン要素にしましょう:
- JavaScript
- TypeScript
const formattedDate = (datetime) => {
const parsedDate = new Date(datetime)
const month = parsedDate.toLocaleString('default', { month: 'long' })
return `${parsedDate.getDate()} ${month} ${parsedDate.getFullYear()}`
}
const Comment = ({ comment }) => {
return (
<div className="bg-gray-200 p-8 rounded-lg">
<header className="flex justify-between">
<h2 className="font-semibold text-gray-700">{comment.name}</h2>
<time className="text-xs text-gray-500" dateTime={comment.createdAt}>
{formattedDate(comment.createdAt)}
</time>
</header>
<p className="text-sm mt-2">{comment.body}</p>
</div>
)
}
export default Comment
const formattedDate = (datetime: ConstructorParameters<typeof Date>[0]) => {
const parsedDate = new Date(datetime)
const month = parsedDate.toLocaleString('default', { month: 'long' })
return `${parsedDate.getDate()} ${month} ${parsedDate.getFullYear()}`
}
// Just a temporary type. We'll replace this later
interface Props {
comment: {
name: string
createdAt: string
body: string
}
}
const Comment = ({ comment }: Props) => {
return (
<div className="bg-gray-200 p-8 rounded-lg">
<header className="flex justify-between">
<h2 className="font-semibold text-gray-700">{comment.name}</h2>
<time className="text-xs text-gray-500" dateTime={comment.createdAt}>
{formattedDate(comment.createdAt)}
</time>
</header>
<p className="text-sm mt-2">{comment.body}</p>
</div>
)
}
export default Comment
いい感じ!では、このコンポーネントが私たちの望むとおりに動くかどうか、テストしてみましょう。
Testing
サンタさんが来ないと困るので、きちんと Comment コンポーネントをテストしましょう。作者の名前とコメントの本文が表示されるか、投稿された日付が表示されるかをテストできます。
生成されたコンポーネントに付属するデフォルトのテストは、エラーが投げられないことを確認するだけで、これは私たちがコンポーネントに求める最低限のことです!
テストにサンプルコメントを追加して、各部が描画されることを確認してみましょう:
- JavaScript
- TypeScript
import { render, screen } from '@redwoodjs/testing'
import Comment from './Comment'
describe('Comment', () => {
it('renders successfully', () => {
const comment = {
name: 'John Doe',
body: 'This is my comment',
createdAt: '2020-01-02T12:34:56Z',
}
render(<Comment comment={comment} />)
expect(screen.getByText(comment.name)).toBeInTheDocument()
expect(screen.getByText(comment.body)).toBeInTheDocument()
const dateExpect = screen.getByText('2 January 2020')
expect(dateExpect).toBeInTheDocument()
expect(dateExpect.nodeName).toEqual('TIME')
expect(dateExpect).toHaveAttribute('datetime', comment.createdAt)
})
})
import { render, screen } from '@redwoodjs/testing'
import Comment from './Comment'
describe('Comment', () => {
it('renders successfully', () => {
const comment = {
name: 'John Doe',
body: 'This is my comment',
createdAt: '2020-01-02T12:34:56Z',
}
render(<Comment comment={comment} />)
expect(screen.getByText(comment.name)).toBeInTheDocument()
expect(screen.getByText(comment.body)).toBeInTheDocument()
const dateExpect = screen.getByText('2 January 2020')
expect(dateExpect).toBeInTheDocument()
expect(dateExpect.nodeName).toEqual('TIME')
expect(dateExpect).toHaveAttribute('datetime', comment.createdAt)
})
})
ここでは、出力される createdAt
タイムスタンプの両方の要素についてテストしています:出力される実際のテキスト(記事の本文を切り詰めるテストと同様)だけでなく、そのテキストをラップしている要素が <time>
タグであり、 comment.createdAt
という生の値を持つ datetime
属性を含んでいるかどうかです。これはやりすぎのように思えるかもしれませんが、 datetime
属性のポイントは、ブラウザが(理論的には)フックして何かできるような、機械的に読み取れるタイムスタンプを提供することなのです。このテストでその能力を維持していることを確認します。
もしテストがまだ別のターミナルウィンドウで実行されていないなら、今すぐ開始できます:
yarn rw test
そうです。ちょうど、切り詰める長さを変更したら切り詰めテキストを変更しなければならないのと同じです。フォーマットされた出力をテストするための別のアプローチとして、日付フォーマットの形式を Comment
コンポーネントからエクスポートできる関数に移動させることができます。
そして、それをテストにインポートして、フォーマットされた出力をチェックするために使うことができます。
これで形式を変更しても、 Comment
と関数を共有しているため、テストはパス(成功)し続けることができます。