LoginSignup
12
2

ElixirDesktop CoreComponentの解説とConfirm Modal 実装

Last updated at Posted at 2023-12-08

はじめに

この記事は Elixirアドベントカレンダーのシリーズ4の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のタグで挟んだ内容をどの場所で展開するかを定義できます

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

@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に使い方が載っています
attrnavigateslotBack to Postsが指定されています
attrのnavigateはlinkタグのnavigateにパスとして展開されています
slotの中身を展開するときは render_slotを使用します

core_component カスタム

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

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

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

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の時に開くモーダルを追加します

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を取得してアサインします

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ボタンを押した時のイベントを修正します

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を追加します

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

最後に

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

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

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

12
2
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
2