ElixirDesktop CoreComponentの解説とConfirm Modal 実装

# はじめに
この記事は Elixirアドベントカレンダーのシリーズ４の8日目の記事です

PhoenixにビルドインのコンポーネントのCoreComponentの紹介と
ネイティブでは使用できないブラウザのConfirmダイアログを独自に実装します


# CoreComponentとは
Phoenixにビルドインされた TailwindとLiveView Phoniex Componentで実装されたデフォルトコンポーネント群で以下のようなコンポーネントがあります

- modal -> CRUD作成時にも使用されるモーダル
- flash -> 保存、エラー時に右上に表示されるフラッシュ
- flash_group -> 上記をまとめたもの
- simple_form -> input部分と保存、キャンセル等のボタンのレイアウトを調整したフォーム
- button -> 各丸ボタン
- input -> 各種各丸input、正規化やエラーメッセージ表示も対応
- label -> デザイン調整されたラベル
- error -> input等で使うエラーメッセージ
- header -> タイトル、サブタイトル、アクション含むデザインヘッダー
- table -> LiveStream対応したデザイン調整されたテーブル
- list -> 構造体やMapの情報を一覧する
- back -> 戻るボタン
- icon -> Heroiconを表示するコンポーネント

# attr と slot
CoreComponentの元になっているPhoenix.Componentは attrとslotという値を定義することができます

attrはパラメーターとして渡せる値を定義できます
slotはcomponentのタグで挟んだ内容をどの場所で展開するかを定義できます

実際の実装を見てみましょう

```elixir
@doc """
  Renders a back navigation link.

  ## Examples

      <.back navigate={~p"/posts"}>Back to posts</.back>
  """
  attr :navigate, :any, required: true
  slot :inner_block, required: true

  def back(assigns) do
    ~H"""
    <div class="mt-16">
      <.link
        navigate={@navigate}
        class="text-sm font-semibold leading-6 text-zinc-900 hover:text-zinc-700"
      >
        <.icon name="hero-arrow-left-solid" class="h-3 w-3" />
        <%= render_slot(@inner_block) %>
      </.link>
    </div>
    """
  end
```

Examplesに使い方が載っています 
`attr`に`navigate`、`slot`に `Back to Posts`が指定されています
attrのnavigateはlinkタグのnavigateにパスとして展開されています
slotの中身を展開するときは `render_slot`を使用します



# core_component カスタム
CoreComponentですが細かいところがスマホアプリのWebViewと合わないので微調整を行います

変更する場合は`core_components.ex`に全てあるので、対象のコンポーネントを検索してclassを値を変更することで調整ができます

`.table`コンポーネントが横に広くてスクロールが必要になるのが嫌なので、固定値から`w-full`にしましょう

```diff_elixir:lib/bookshelf_web/components/core_components.ex:L470
  def table(assigns) do
    assigns =
      with %{rows: %Phoenix.LiveView.LiveStream{}} <- assigns do
        assign(assigns, row_id: assigns.row_id || fn {id, _item} -> id end)
      end

    ~H"""
    <div class="overflow-y-auto px-4 sm:overflow-visible sm:px-0">
-      <table class="mt-11 w-[40rem] sm:w-full">
+      <table class="mt-11 w-full"> 
    # 以下省略
    """
  end
```

# confirm modalを追加
WebViewだと削除時に出てくる confirmやalertのダイアログは出ないので独自に実装が必要です
削除リンクをモーダルを開くナビゲーションにして
index.html.heexの末尾にconfirm modalを表示するようにします

クリックイベントからモーダル表示用のリンクに差し替え、 live_actionがdeleteの時に開くモーダルを追加します

```diff_elixir:lib/bookshelf_web/live/shelf_live/index.html.heex
...
<.table
  id="shelves"
  rows={@streams.shelves}
  row_click={fn {_id, shelf} -> JS.navigate(~p"/shelves/#{shelf}") end}
>
  <:col :let={{_id, shelf}} label="Name"><%= shelf.name %></:col>

  
  <:action :let={{_id, shelf}}>
    <div class="sr-only">
      <.link navigate={~p"/shelves/#{shelf}"}>Show</.link>
    </div>
    <.link patch={~p"/shelves/#{shelf}/edit"}>Edit</.link>
- </:action>
- <:action :let={{id, shelf}}>
-   <.link
-     phx-click={JS.push("delete", value: %{id: shelf.id}) |> hide("##{id}")}
-       data-confirm="Are you sure?"
-    >
-      Delete
-   </.link>
+   <.link patch={~p"/shelves/#{shelf}/delete"}>Delete</.link>
  </:action>
</.table>

<.modal :if={@live_action in [:new, :edit]} id="shelf-modal" show on_cancel={JS.patch(~p"/shelves")}>
  <.live_component
    module={BookshelfWeb.ShelfLive.FormComponent}
    id={@shelf.id || :new}
    title={@page_title}
    action={@live_action}
    shelf={@shelf}
    patch={~p"/shelves"}
  />
</.modal>

+ <.modal
+  :if={@live_action in [:delete]}
+   id="shelf-delete-modal"
+   show
+  on_cancel={JS.navigate(~p"/shelves")}
+ >
+  <p class="mb-4 text-lg">Are you sure?</p>
+  <.button phx-click={JS.push("delete", value: %{id: @shelf.id})}>Delete</.button>
+ </.modal>
```

次にlive_actionのハンドリングを行います
actionがdeleteの場合はパラメーターのidからshelfを取得してアサインします

```diff_elixir:lib/bookshelf_web/live/shelf_live/index.ex
defmodule BookshelfWeb.ShelfLive.Index do
  use BookshelfWeb, :live_view

  ...
  defp apply_action(socket, :index, _params) do
    socket
    |> assign(:page_title, "Listing Shelves")
    |> assign(:shelf, nil)
  end

+ defp apply_action(socket, :delete, %{"id" => id}) do
+   socket
+   |> assign(:page_title, "Delete Shelf")
+   |> assign(:shelf, Shelves.get_shelf!(id))
+ end
  ...
end
```

deleteボタンを押した時のイベントを修正します

```diff_elixir:lib/bookshelf_web/live/shelf_live/index.ex
  @impl true
  def handle_event("delete", %{"id" => id}, socket) do
    shelf = Shelves.get_shelf!(id)
    {:ok, _} = Shelves.delete_shelf(shelf)

-   {:noreply, stream_delete(socket, :shelves, shelf)}
+    {
+      :noreply,
+      socket
+      # flashを表示
+      |> put_flash(:info, "Shelf deleted successfully")
+      # モーダルを閉じるように変更
+      |> push_navigate(to: ~p"/shelves")
+      |> stream_delete(:shelves, shelf)
+    }
  end
```


routeにdelete actionを追加します

```diff_elixir:lib/bookshelf_web/router.ex
  scope "/", BookshelfWeb do
    pipe_through :browser

    get "/", PageController, :home

    live "/shelves", ShelfLive.Index, :index
    live "/shelves/new", ShelfLive.Index, :new
    live "/shelves/:id/edit", ShelfLive.Index, :edit
+   live "/shelves/:id/delete", ShelfLive.Index, :delete
    
    live "/shelves/:id", ShelfLive.Show, :show
    live "/shelves/:id/show/edit", ShelfLive.Show, :edit
  end
```

これでconfirmダイアログ風なモーダルを通して削除をができるようになりました

![fed844214c27f6c8df03977eebc869cb.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/98c74abd-2423-61db-63bf-7154da07ce21.gif)


# 最後に
本記事ではCoreComponentの使い方、カスタムについての解説と
実際に使って削除確認ダイアログを実装しました

次はUIライブラリの追加とナビゲーションコンポーネントを作成してきます

本記事は以上になります、ありがとうございました