メインコンテンツまでスキップ

ブロックの実装と使用

フロントエンドフレームワークのテンプレートでのカスタマイズの一般的な使用例の1つは、既存のテンプレート内でページやコンポーネントを作成または置き換えることです。このガイドでは、コンポーネントやページを作成するための一般的なワークフローについて説明します。

備考

これは、問題にアプローチするための一般的なプロセスとして提供されています。このプロセスは、フロントエンドフレームワークを内部的に使用する方法ですが、必要に応じて自由にアプローチすることができます。

例: プロフィールページの作成

まず、簡単なユースケースから始めましょう。プロフィールページの作成です。 このページには、以下の情報が表示されます。

  • ログイン中のユーザーの名前
  • ログイン中のユーザーのメールアドレス
  • ログイン中のユーザーの電話番号

また、ページがロード中の場合や、ユーザーがログインしていない場合の挙動も処理する必要があります。

一般的に、問題をMVCパターンに分割してアプローチすることをお勧めします。

  • モデル: バックエンドからロードされる表示するデータ
  • ビュー: データを表示するコンポーネント
  • コントローラー: データのロードやエラーハンドリングを行うロジック

モデルはこの問題の中で最も簡単な部分です。@basaldev/blocks-frontend-sdkUserApi クライアントを使用して、ユーザーのデータをロードします。このクライアントには、ログイン中のユーザーのデータを返す getUser メソッドが提供されています。

ビューでは、ユーザーのデータを表示する新しいコンポーネントを作成します。

コントローラーでは、データのロードを処理し、この情報をビューに渡す新しいブロックを作成します。

ビューコンポーネントの作成

まず、ユーザーのデータを表示する新しいコンポーネントを作成します。このコンポーネントにはビジネスロジックを含めず、ページのさまざまな状態を表示することだけに専念すべきです。

注記

基本的なルールとして、ビュー内で状態を使用することは避けるべきです。代わりに、親コンポーネントからデータを渡すコールバックを使用し、クエリパラメータ、ページナビゲーション、ローカルストレージなどのより具体的な方法で状態を処理するようにしてください。

import {
LoadingPage,
ShowPage,
Spacing,
Typography,
} from '@basaldev/blocks-frontend-framework';

import classes from './ProfileView.module.css';

export interface ProfileViewProps {
isLoading?: boolean;
labels: {
email: string;
fallback: string;
name: string;
pageTitle: string;
phoneNumber: string;
};
values: {
email: string;
name: string;
phoneNumber: string;
};
}

const DataRow = ({
fallback,
label,
value,
}: {
fallback: string;
label: string;
value: string;
}) => {
let valueElement = (
<Typography size="S" color="low-emphasis">
{fallback}
</Typography>
);
if (value) {
valueElement = <Typography size="S">{value}</Typography>;
}

return (
<Spacing
direction="row"
alignItems="center"
className={classes.showPageItem}
>
<Typography size="S" weight="bold">
{label}
</Typography>
{valueElement}
</Spacing>
);
};

export const ProfileView: React.FC<MyProfileProps> = ({
labels,
values,
isLoading,
}) => {
if (isLoading) {
return <LoadingPage screenMode="desktop" />;
}

return (
<ShowPage pageTitle={labels.pageTitle}>
<Spacing direction="column" gapSize="none">
<DataRow
label={labels.name}
value={values.name}
fallback={labels.fallback}
/>
<DataRow
label={labels.email}
value={values.email}
fallback={labels.fallback}
/>
<DataRow
label={labels.phoneNumber}
value={values.phoneNumber}
fallback={labels.fallback}
/>
</Spacing>
</ShowPage>
);
};

DataRow コンポーネントを定義し、ラベルと値を表示しています。この例では、独自のコンポーネントを作成する方法を示すために、デザインシステムをあまり使用していません。

CSSスタイルには、他のスタイルとの競合を避けるためにCSSモジュールの使用をお勧めします。

@import '@basaldev/blocks-frontend-framework/dist/style.css';

.showPageItem {
box-sizing: border-box;
width: 100%;
padding: 16px;
border-bottom: 1px solid var(--color-border-low-emphasis)
}

.showPageItem:first-child {
border-top: 1px solid var(--color-border-low-emphasis);
}

@import ステートメントはデザインシステムのCSSをインポートし、アプリケーションがテーマを正しく処理できるようにします。

最後に、コンポーネントを単独でテストするためにStorybookストーリーを作成します。

import type { Meta, StoryObj } from '@storybook/react';

import { ProfileView } from './ProfileView';

const meta: Meta<typeof ProfileView> = {
component: ProfileView,
parameters: {
layout: 'fullscreen',
},
title: 'Views/ProfileView',
};

export default meta;
type Story = StoryObj<typeof ProfileView>;

export const Main: Story = {
args: {
labels: {
email: 'メールアドレス',
fallback: '未設定',
name: '氏名',
pageTitle: 'プロフィール',
phoneNumber: '電話番号',
},
values: {
email: 'test@basal.dev',
name: '山田 太郎',
phoneNumber: '090-1234-5678',
},
},
};

export const BlankValues: Story = {
args: {
labels: {
email: 'メールアドレス',
fallback: '未設定',
name: '氏名',
pageTitle: 'プロフィール',
phoneNumber: '電話番号',
},
values: {
email: '',
name: '',
phoneNumber: '',
},
},
};

export const Loading: Story = {
args: {
isLoading: true,
},
};

Storybookを開いて、コンポーネントが正しくレンダリングされているか確認してみてください。yarn storybook を実行してStorybookサーバーを起動します。すべてが期待通りに動作している場合、次のようなコンポーネントが表示されるはずです。

メニューを使用して異なるストーリーを切り替え、コンポーネントがロード中、データがある場合、データが不完全な場合に正しくレンダリングされているか確認してください。

ブロックの作成

次に、データのロードを処理し、この情報をビューに渡すブロックを作成します。このブロックは、UserApi クライアントを使用してユーザーのデータをロードし、ビューに渡します。

import {
BlockComponentProps,
PaymentPage,
useApiGet,
useTranslation,
} from '@basaldev/blocks-frontend-framework';

import { NBError, ErrorCode, api } from '@basaldev/blocks-frontend-sdk';

import { ProfileView } from './ProfileView';

export interface MyProfileOptions {
/** UserAPI client */
userApi: Pick<api.UserApi, 'getUser'>;
}

/**
* Creates a page that displays the logged in user's profile information
*/
export const createMyProfile = ({ userApi }: MyProfileOptions) => {
const MyProfile: React.FC<BlockComponentProps> = (blockProps) => {
const { t } = useTranslation();
const { sessionState } = blockProps;
if (!sessionState.userId) {
throw new NBError({
code: ErrorCode.internalServerError,
message: 'User Id ID missing',
});
}

const [{ data, loading }] = useApiGet(
userApi.getUser.bind(userApi),
sessionState.userId
);

return (
<ProfileView
labels={{
email: t('MyProfile:email'),
fallback: t('MyProfile:fallback'),
name: t('MyProfile:name'),
pageTitle: t('MyProfile:pageTitle'),
phoneNumber: t('MyProfile:phoneNumber'),
}}
values={{
email: data?.email || '',
name: data?.name || '',
phoneNumber: data?.phoneNumber || '',
}}
isLoading={!data && loading}
/>
);
};

return MyProfile;
};

このブロックにはいくつか注目すべき点があります。

  • ブロック自体はReactコンポーネントを返す関数です。これは、フロントエンドフレームワークで一般的なパターンであり、起動時に設定や依存関係をブロックに「注入」することができます。この場合、UserApi クライアントを注入しています。
  • ブロックは useApiGet フックを使用してユーザーのデータをロードします。このフックは UserApi クライアントの getUser メソッドに非同期リクエストを送信し、データとロード状態を返します。
  • ブロックは useTranslation フックを使用してページの翻訳をロードします。これは、i18nextreact-i18next パッケージで提供されており、現在の言語に基づいてページの翻訳をロードするために使用されます。
  • 現在の userIdblockProps.sessionState.userId からロードされます。これは、フロントエンドフレームワークのすべてのブロックに渡される標準プロパティの1つであり、現在のユーザーのIDを取得するために使用されます。
備考

ブロックに渡されるプロパティの完全なリストについては、フロントエンドフレームワークパッケージの BlockComponentProps インターフェースを参照してください。

さらに、ページの翻訳を追加する必要があります。これは、translationOverrides.yaml ファイルに追加することで実行できます。

ja:
MyProfile:
email: メールアドレス
fallback: 未設定
name: 氏名
pageTitle: プロフィール
phoneNumber: 電話番号

これをテンプレート設定でオーバーライドとしてインポートします。詳細については、テキストのカスタマイズガイドを参照してください。

必要に応じて、このブロックのStorybookストーリーも作成して、単独でテストすることができます。特に、モックデータを使用してブロックの動作をテストしたり、ロード状態をテストするためにモック遅延を実装する場合に便利です。

テンプレートブロックページの設定

このブロックを使用するには、フロントエンドフレームワークテンプレートに追加する必要があります。これは、テンプレート設定に新しいブロックページエントリを追加することで行います。

import { isLoggedIn } from '@basaldev/blocks-frontend-framework';
import { createMyProfile } from './MyProfileBlock';

{
...
blockPages: [
...,
{
component: createMyProfile({ userApi: dependencies.userApi }),
name: 'my_profile',
pageTitle: (t) => t('MyProfile:pageTitle'),
path: '/my-profile',
validators: {
isLoggedIn: isLoggedIn({
notLoggedInRedirect: 'auth.login',
}),
},
},
]
}

この設定では、my_profile という名前の新しいブロックページと、/my-profile というパスを追加しています。このブロックページは、createMyProfile ブロックを使用してページをレンダリングし、ログインしていない場合は auth.login ページにリダイレクトします。

リダイレクトを処理するために、フロントエンドフレームワークの isLoggedIn バリデーターを使用しています。バリデーターは、ブロックページがレンダリングされる前に実行される関数で、ユーザーがログインしているかどうか、正しい権限を持っているかどうかなど、一般的な使用ケースを処理するために使用されます。

この設定により、ユーザーは /my-profile ページにアクセスし、ログインしている場合はプロフィール情報を見ることができます。アプリケーションでこのページに移動して、期待通りに動作しているか確認してください。

ブロックページの設定方法の詳細については、テンプレートの設定ガイドの blockPages プロパティを参照してください。

サイドバーとメニュー

ブロックページは、フロントエンドフレームワークでサポートされている唯一のブロックタイプではありません。デフォルトのナビゲーションコンポーネントを使用する場合、サイドナビゲーションや右上のメニューにもブロックを追加することができます。

一般的なユースケースとして、サイドバーにプロフィールページへのリンクを追加したい場合があります。私たちは、ほとんどの一般的な静的ユースケースを処理するデフォルトのブロックを提供しています。

{
navigationComponent: createNavigation({
...
sideNavigationBlocks: [
createSideNavigationItemDefault({
icon: 'profile',
requiresLogin: true,
text: (t) => t('MyProfile:pageTitle'),
toRoute: 'my_profile',
}),
]
}),
}

この設定では、サイドバーにプロフィールページへのリンクを追加します。requiresLogin プロパティは、ユーザーがログインしている場合にのみリンクが表示されるようにします。

単なる静的な文字列を表示するだけでなく、より複雑なサイドバーブロックを実装したい場合もあります。この場合、カスタムサイドバーブロックを作成できます。例えば、ユーザーの名前を表示するブロックの実装は次のとおりです。

export interface SideNavigationItemUserInfoOptions {
/** Route to link to when clicked */
toRoute: string;
/** User api */
userApi: Pick<api.UserApi, 'getUser'>;
}

/**
* A side navigation item that loads and displays info about the currently logged in user's name
* as a header
*/
export const createSideNavigationItemUserInfo = (
options: SideNavigationItemUserInfoOptions
) => {
const { toRoute, userApi } = options;
const SideNavigationItemUserInfo: React.FC<BlockComponentProps> = ({
onNavigate,
sessionState,
urlForRoute,
}) => {
const href = urlForRoute(toRoute);
const [{ data: userData }] = useApiGet(
userApi.getUser.bind(userApi),
sessionState.userId ?? '',
{ skip: !sessionState.isLoggedIn }
);

if (!userData) {
return null;
}

return (
<SideNavigationItem
key="user-info"
href={href}
icon="person"
onNavigate={onNavigate}
text={userData.name}
isHeader
/>
);
};

return SideNavigationItemUserInfo;
};

使用方法:

{
navigationComponent: createNavigation({
...
sideNavigationBlocks: [
createSideNavigationItemUserInfo({
toRoute: 'my_profile',
userApi: dependencies.userApi,
}),
]
}),
}

これは、ユーザーのデータをロードして名前を表示する、より複雑なブロックです。データが読み込まれるまで何も表示されません。SideNavigationItem デザインシステムコンポーネントを表示として使用している点に注目してください。ビューはフルページに限定されるものではなく、他の場所でコンポーネントとして使用することもできます。

注記

SideNavigationItemUserInfo は、実際にはフロントエンドフレームワークが提供するブロックの1つであり、ゼロから作成する代わりにインポートすることができます。

メニューはサイドナビゲーションに似ていますが、画面の右上に表示されます。

ウィザードおよびその他のユーティリティブロック

標準のブロックタイプに加えて、フロントエンドフレームワークは、カスタムコンポーネントを記述せずに、より複雑なインタラクションを作成できるユーティリティブロックも提供しています。

ウィザード

次の例を見てみましょう:

interface ExampleFormData {
name: string;
email: string;
phoneNumber: string;
}

...

{
...
blockPages: [
{
component: createWizard<ExampleFormData>({
pages: [
{
component: createExamplePageOne({ ... }),
key: 'my-page-1',
},
{
component: createExamplePageTwo({ ... }),
key: 'my-page-2',
},
{
component: createExampleWizardSubmit({
...
}),
key: 'submit',
},
],
wizardCompleteRoute: 'after.wizard',
}),
name: 'example_wizard',
pageTitle: (t) => // ...
path: '/example-wizard',
}
]
},

この例では、2ページと送信ページを持つウィザードを作成しています。ユーザーがウィザードを完了すると、ウィザードは after.wizard ルートに移動します。

ウィザードの各サブページは、他のブロックと同じパターンを使用して作成できますが、ウィザードから追加のプロパティも受け取ります。

  • blockProps: ブロックに渡される標準のプロパティ
  • currentPageKey: ウィザード内で開かれている現在のページのキー
  • currentSubPageKey: ウィザード内で開かれている現在のサブページのキー(下記のサブページを参照)
  • onSubPagesUpdated: 現在のページに対するサブページを設定するためのコールバック
  • onBack: 前のページに戻るためのコールバック
  • onSkipOver: 現在のページをスキップして次のページに進むためのコールバック
  • onSubmit: 現在のページを送信して次のページに進むためのコールバック
  • progress: ウィザード内の現在の進捗に関する情報
  • wizardFormState: ウィザードの現在のフォーム状態

フォーム状態

ウィザードはページ間でフォーム状態を保持し、最終ページで onSubmit が呼び出されるまでフォーム状態を各ページに渡します。これにより、前のページでのユーザー入力に基づいてページを動的に更新できます。

ウィザードのフォーム状態は最終送信までローカルストレージに保存されるため、ユーザーがウィザードから離れたり、ページをリフレッシュしたりしても、同じ状態に戻ることができます。これにより、よりシームレスなユーザー体験が可能になります。 状態は nodeblocksWizard:<name> キーの下に保存されます。

onSkipOver

onSkipOver コールバックは、現在のページをスキップして次のページに進むために使用されます。ただし、これによりページがウィザードの進行状況で「無視」されたと見なされ、onBack を呼び出してもスキップされたページに戻ることはありません。 このプロパティを使用して、前のページの入力に基づいて無視されるべきでないページをスキップすることができます。

サブページ

サブページは、ウィザードの1つのページ内でより複雑なインタラクションを作成する方法です。例えば、ユーザーが複数のカテゴリを選択でき、その後、選択された各カテゴリに対する一連のサブページが表示されるページがある場合、onSubPagesUpdated を呼び出して現在のページのサブページを設定することでこれを実装できます。ウィザードの進行状況は、ページ数の増加を反映して更新されます。

サブページのリストもローカルストレージに保存されるため、設定後はウィザードが表示されるべきページを覚えています。

最終ページなしで送信する

最終確認ページなしでウィザードを送信したい場合、自動的にウィザードを送信するページを実装できます。例えば、開かれるとウィザードを自動的に送信する内容のないページを実装できます。

リダイレクト

リダイレクトブロックは、レンダリングされるとユーザーを別のページにリダイレクトする単純なブロックです。テンプレート内で簡単なリダイレクトを作成するのに役立ちます。

{
...
blockPages: [
{
component: createRedirect({
to: 'my_profile',
}),
name: 'redirect_to_profile',
path: '/',
}
]
},

セクション

セクションブロックは、他のブロックの縦に結合されたリストをレンダリングする単純なブロックです。複数のブロックで構成されたページを作成するのに役立ちます。

{
...
blockPages: [
{
component: createSections({
components: [
createMyProfile({ ... }),
createExamplePageOne({ ... }),
],
}),
name: 'sections',
path: '/sections',
}
]
}

ダミー

ダミーブロックは、いくつかのサンプルテキストをレンダリングする単純なブロックです。開発中にテンプレート内でプレースホルダーブロックを作成するのに役立ちます。

{
...
blockPages: [
{
component: createDummy({
text: 'This is a dummy block',
}),
name: 'dummy',
path: '/dummy',
}
]
}