【Amplify Studio 中級編】Collections Component をマスターしよう

記事タイトルとURLをコピーする

こんにちは。アプリケーションサービス部の河野です。

今回は、UI Library の機能の一つである、Collections Component について紹介します。

Figma to Code (React) - Working with collections - AWS Amplify Docs

以下のような一覧画面を実装しながら、Collections Component の使い方をマスターしていきます。

Amplify Studio 初めての方は初級編の記事も合わせてご参照ください。

blog.serverworks.co.jp

blog.serverworks.co.jp

前提

Collections の使い方にフォーカスしたいため、以下を実施済みとします。

  1. Amplify Studio がセットアップ済みである
  2. UI Component は Amplify UI Kit と同期している
  3. データモデルを事前に設定し、デプロイ済みである(スキーマは以下参照)
  4. Data Manager から テストデータを登録している

今回使用するスキーマ

テストデータ

実践

1. データバインディングの設定

Collections は、任意の 1 コンポーネントを一覧表示できる機能です。

まずは、Collections で表示するコンポーネントのデータの紐付けを行います。

今回は、Amplify UI Kit の ItemCard をそのまま使用します。

Component Properties から データの紐付けを設定します。

  • 商品名を Product.name に紐づける

  • 商品説明を Product.description に紐づける

  • 商品価格を Product.price に紐づける

2. Collections の作成

ItemCard のコンポーネント設定の画面右上にある「Create collection」をクリックします。

コレクション名を設定して、「Create」をクリックします。

ItemCard を一覧表示するコンポーネントが表示されました。

各種設定

Collection コンポーネントは、レイアウト、検索バー、ページネーションの設定が可能です。

今回は以下の通り設定しました。

検索、ページネーションの細かいレイアウト調整(検索バーを右寄せに配置したい等)は Studio からは設定できません。

Studio の画面上で、検索や、ページネーションの動作確認することも可能です。

  • 検索

  • ページネーション

画面右のメニューでは、一覧表示するデータを調整することができます。

今回は、登録日の降順でソートするように設定しました。

3. コンポーネント取り込み

「Get component code」をクリックすると、取り込み方法および、実装方法が表示されます。

初期セットアップの方法も確認することができます。

初期セットアップ後、App.js に以下のコードを実装しました。

コレクションコンポーネントを配置しただけです。

import {
    ItemCardCollection
} from './ui-components';

function App() {
    return (
        <div className="App">
            <ItemCardCollection />
       </div>
    );
}

export default App;

アプリケーションを起動して、動作を確認します。

yarn start

検索とページネーションも問題なく動作しました!

カードコンポーネントが白背景と同化しているので、少し UI を調整します。

4. Figma で Shadow を追加

Figma で、Effects を追加します。Drop shadow を追加しました。

Amplify Studio に取り込みます。

amplify pull して、もう一度起動します。 カードっぽくなりました。

少し見やすいように、画面中央に配置するように App.css を調整しました。

.App {
    text-align: center;
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100vh;
    /* Viewport height */
    margin: 0;
}

ほぼ、コードを書かずにデータフェッチして、一覧表示、ページネーション、検索機能まで実装することができました。

コードによる拡張

component props

今回は、Amplify Stduio 側で、ページネーションや検索機能を追加しましたが、Component props からも設定が可能です。

Collections は、Amplify UI で提供されているコレクションコンポーネント Props を持ちます。

ui.docs.amplify.aws

以下のようにProps を渡すことで、Studio 側で設定した内容と同様に実装できます。

 <ItemCardCollection
    type="grid"
    autoFlow="row"
    isSearchable
    isPaginated
    searchPlaceholder="Search..."
    itemsPerPage={6}
/>

実際に、Studio が出力したコンポーネントコードも、Studio で設定した内容で Props に渡されていることがわかります。

  • Studio が出力した ItemCardCollection.tsx (一部抜粋)
  return (
    <Collection
      type="grid"
      isSearchable={true}
      isPaginated={true}
      searchPlaceholder="Search..."
      itemsPerPage={6}
      templateColumns="1fr 1fr 1fr"
      autoFlow="row"
      alignItems="stretch"
      justifyContent="stretch"
      items={items || []}
      {...getOverrideProps(overrides, "ItemCardCollection")}
      {...rest}
    >
      {(item, index) => (
        <ItemCard
          product={item}
          height="auto"
          width="auto"
          margin="10px 10px 10px 10px"
          key={item.id}
          {...(overrideItems && overrideItems({ item, index }))}
        ></ItemCard>
      )}
    </Collection>

overrideItems

overrideItems を使用すれば、コレクションで表示している各アイテム毎に Props を設定することができます。

Figma to Code (React) - Extend via code - AWS Amplify Docs

overrideItems は、イテレートの各要素とindex を引数に持ち、その要素の props を返すことができます。

Studio は各コンポーネントの宣言ファイルにて型情報も一緒に出力してくれているため、詳細についてこちらを確認すると良いです。

ItemCardCollection.d.ts (一部抜粋)

export declare type ItemCardCollectionProps = React.PropsWithChildren<Partial<CollectionProps<any>> & {
    items?: any[];
    overrideItems?: (collectionItem: {
        item: any;
        index: number;
    }) => ItemCardProps;
} & {
    overrides?: ItemCardCollectionOverridesProps | undefined | null;
}>;
export default function ItemCardCollection(props: ItemCardCollectionProps): React.ReactElement;

いくつかを例に、設定してみます。

1. マウスオーバー時にポインターにする

各アイテムにマウスオーバーされた際に、カーソルをポインターに変更するスタイルを追加します。

 <ItemCardCollection
    overrideItems={({ item, index }) => ({
        style: { cursor: "pointer" }
    })}
/>

2. ボタンクリックで詳細モーダルを表示する

詳細で表示するモーダルは、Amplify UI kit の ProductCard をそのまま使用します。

画像を表示させたいたいため、データモデルに image カラムを追加し、テストデータにサンプル画像を追加しておきます。

ProductCard のデータの紐付けを設定します。 商品名、商品説明、商品画像のみ設定しました。

画像を紐づける場合、Prop を src に設定する必要があります。

amplify pull を実行して、Studio 側の変更を取り込みます。

クリック時のモーダル表示するロジックを追加します。

function App() {
    const [showProductCard, setShowProductCard] = useState()

    return (
        <div className="App">
            <ItemCardCollection
                overrideItems={({ item }) => ({
                    style: { cursor: "pointer" },
                    onClick: () => setShowProductCard(item)
                })}
            />
        {showProductCard &&
            <div className='modal'>
                //表示するデータを product の props で渡している
                <ProductCard product={showProductCard} />
            </div>}
        </div>
    );
}

参考:Figma to Code (React) - UI event handler - AWS Amplify Docs

モーダル表示は CSS を追加しました。

.modal {
    overflow: scroll;
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background-color: rgba(0, 0, 0, 0.5);
    display: flex;
    align-items: center;
    justify-content: center;
}

モーダルを表示することができました。 画像が透けていたり、カードコンポーネントをそのまま使用しているので、若干違和感もありますが、ご愛嬌ということで。

※1 モーダルを閉じる処理は省略しています。
※2 表示する画像によりますが、画像背景が透けないようにするには、Figima で Fill を追加してみてください。

今回はモーダル表示にチャレンジしましたが、単純に onClick イベントでページ遷移させた方が、コード量は少なく実現できそうです。

Tips

リレーションを持つコンポーネントコレクション

belong to

コレクション対象のコンポーネントが、belogs to のリレーションを持っていた場合、コード側で一手間必要です。

例えば、Product モデルに、Category モデルのリレーションを追加した場合を考えます。 モデルは以下の通りになります。

Product : Category(1 : N) の関係です。

ItemCard に Category モデルを紐づけます。 今回は、Badge のアイコンに Category.name を紐付けました。

コレクションの設定画面を開きます。

右メニューに 「1.Link collection to data model」と、「2. Map data to properties」の表示が追加されています。

  • Link collection to data model
    • イテレーション対象のモデルを指定します。
  • Map data to properties
    • collection 内のコンポーネントに紐づけるプロパティを指定できるはずなのですが、1 で指定したモデルしか選択できないため、undefiend のままにしています。

belogs to の関係は Studio 側の設定ではサポートされていないので、コレクションの設定はそのままで、amplify pull で取り込みます。

コード側で、以下のように Category をオーバーライドします。

<ItemCardCollection
    overrideItems={({ item }) => ({
        category: item.Category, // ← 追加
        style: { cursor: "pointer" },
        onClick: () => setShowProductCard(item)
    })}
/>

カテゴリーが表示されました。

has many

コレクション対象のコンポーネントが、has many のリレーションを持っていた場合を考えます。

Review(レビュー)モデルを追加して、Product ごとに複数のレビューコメントを表示する場合を考えます。 モデルは以下のようになります。

Product : Review(1 : N) の関係です。

カードにコメントを表示するようにデザインを変更します。

AutoLayout を設定した reviews フレームにテキストラベルを入れて、ItemCard に配置しました。

コンポーネントスロットを設定すると、設定した props 名でコンポーネント(UI element)を差し込むことができます。

Figma to Code (React) - Component slots - AWS Amplify Docs

コレクションで表示している商品ごとに、複数のレビューコメントを表示させたい場合等、コレクション内のデータが has many のリレーションを持っている時に使用すると便利です。

コレクションの設定は変更せずに、取り込みます。

コードは以下の通り変更します。

<ItemCardCollection
    overrideItems={({ item }) => ({
        category: item.Category,
        style: { cursor: "pointer" },
        onClick: () => setShowProductCard(item),
        reviews:
            <div>
                {item.Reviews.map(review => <div>{review.comment}</div>)}
            </div>
    })}
/>

reviews prop に、Product 内のコメントを map して返す処理を追加します。

何もスタイルをあてていないため、若干違和感がありますが、商品ごとに、レビューコメントを表示することができました。

many to many

コレクション対象のコンポーネントが、many to many のリレーションを持っていた場合、Collections はサポートされていません。

Limitation: Nested data fetching for many-to-many relationships Automatic nested data fetching for many-to-many relationships is currently not supported. Currently, component slots only automatically fetches data for has One and has Many relationships, like one Post to many Comments. https://docs.amplify.aws/console/uibuilder/slots/#adding-component-slots-to-collections

この場合は、直接 DataCollection を使用する必要があります。

ui.docs.amplify.aws

今回は、商品ごとに複数のカテゴリーを付与する場合を考えます。 商品とカテゴリーは N:N のリレーションになるため、モデルを以下のように変更します。

カテゴリーが複数配置できるように、デザインを変更します。

Figma で編集した内容を取り込んで、categories にも 先ほど同様に コンポーネントスロットを設定します。

has many 同様にコードを修正してみます。

<ItemCardCollection
    overrideItems={({ item }) => ({
        categories:
            <div>
                {item.categorys.map(category => <div>{category.name}</div>)}
            </div>,
        style: { cursor: "pointer" },
        onClick: () => setShowProductCard(item),
        reviews:
            <div>
                {item.Reviews.map(review => <div>{review.comment}</div>)}
            </div>
    })}

item 内に categorys というデータを取得できずにエラーになりました。

Collections が N : N に対応していないことがわかります。

実際に、ItemCardCollection.jsx のソースコードを確認すると、以下の処理があります。

React.useEffect(() => {
    if (itemsProp !== undefined) {
      setItems(itemsProp);
      return;
    }
    async function setItemsFromDataStore() {
      var loaded = await Promise.all(
        itemsDataStore.map(async (item) => ({
          ...item,
          Reviews: await item.Reviews.toArray(),
        }))
      );
      setItems(loaded);
    }
    setItemsFromDataStore();
  }, [itemsProp, itemsDataStore]);

Reviews データに関しては、await item.Reviews.toArray() の処理で、item にデータを挿入していることがわかります。

先ほど追加した、category についての処理はないため、item から取得できないことがわかります。

DataCollection が対応していない場合は、Amplify UI の Collection コンポーネントを直接使用する必要があります。

その場合、データのフェッチ処理も合わせて実装する必要があるため、少し大変です。

詳しくは説明しませんが、簡単に実装すると以下のようになります。

import { ItemCard } from './ui-components'
import { useEffect, useState } from 'react'
import { DataStore } from '@aws-amplify/datastore';
import { Product, Category } from './models';
import { Badge, Collection } from "@aws-amplify/ui-react";

import "./App.css";

function App() {
    const [products, setProducts] = useState()

    useEffect(() => {
        const fetchProduct = async () => {
            const response = await DataStore.query(Product);

            const loaded = await Promise.all(
                response.map(async (item) => {
                    const categories = await DataStore.query(Category, category => category.Products.product.id.eq(item.id)) // Product に紐づく Categody を取得するクエリ

                    return {
                        ...item,
                        reviews: await item.Reviews.toArray(),
                        categories: categories // ← category を item に挿入
                    }
                })
            )
            setProducts(loaded)
        }

        fetchProduct()

    }, [])

    return (
        <div className="App">
            <Collection
                items={products || []}
                type="grid"
                templateColumns="1fr 1fr 1fr"
                autoFlow="row"
                isSearchable={true}
                isPaginated={true}
                searchPlaceholder="Search..."
                itemsPerPage={6}
            >
                {(item) => (
                    <ItemCard
                        key={item.id}
                        product={item}
                        height="auto"
                        width="auto"
                        margin="10px 10px 10px 10px"
                        reviews={
                            <div>
                                {item.reviews.map((review) => (
                                    <div key={review.id}>
                                        {review.comment}
                                    </div>
                                ))}
                            </div>
                        }
                        categories={
                            <div>
                                {item.categories.map((category) => (
                                    <Badge key={category.id} variation="success">{category.name}</Badge>
                                ))}
                            </div>
                        }
                    />
                )}
            </Collection>
        </div>
    );
}

export default App;

Categoryも複数表示することができました。

※ Collections の場合は、データ更新時に再フェッチされますが、その処理まで実装していません。

おわりに

最後の N : N の実装をしながら、Collections の便利さが身に染みました...

特に、データフェッチ処理まで含めたコンポーネントになっているのは、すごく良いですね。

また、overrideItems で、コンポーネント毎に、ロジックやスタイルを追加できるため、拡張性もあることが分かりました。

N : N のリレーションに対応していない点については注意が必要ですが、積極的に使っていきたいです。

swx-go-kawano (執筆記事の一覧)