9
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ElixirDesktopで作るブログアプリ モバイル&API 削除モーダル、ユーザー削除

Posted at

はじめに

この記事はElixirアドベントカレンダー2025シリーズ2の23日目の記事です。

今回はポストの削除、ユーザー削除を実装します

Post削除モーダル

現在開発はwxwidgetですが、iOS,Androidだとcondirmダイアログはでないのでモーダルでやるようにします

コンポーネントはこちら

対象idにハイフンが入っているとモーダルが開かなかったので注意が必要です
id -> post-[id]
post.id -> [id]
なのでpost.idの方を使えば大丈夫です

modalを開く場合は onclick={"modal_#{[ID]}.showModal()"}
閉じる場合は onclick={"modal_#{[ID]}.close()"}
を使います

      <.table
        id="posts"
        rows={@streams.posts}
        row_click={fn {_id, post} -> JS.navigate(~p"/posts/#{post}") end}
      >
        <:col :let={{_id, post}} label="Text">{post.text}</:col>
        <:action :let={{_id, post}}>
          <div class="sr-only">
            <.link navigate={~p"/posts/#{post}"}>Show</.link>
          </div>
          <.link navigate={~p"/posts/#{post}/edit"}>Edit</.link>
        </:action>
        <:action :let={{id, post}}>
-         <.link
-           phx-click={JS.push("delete", value: %{id: post.id}) |> hide("##{id}")}
-           data-confirm="Are you sure?"
-         >
-           Delete
-         </.link>
+         <button onclick={"modal_#{post.id}.showModal()"}>Delete</button>
+         <dialog id={"modal_#{post.id}"} class="modal">
+           <div class="modal-box">
+             <h3 class="text-lg font-bold">Are you sure?</h3>
+             <div class="modal-action">
+               <form method="dialog">
+                 <.button
+                   onclick={"modal_#{post.id}.close()"}
+                   phx-click={JS.push("delete", value: %{id: post.id})}
+                 >
+                   Delete
+                 </.button>
+                 <button class="btn">Cancel</button>
+               </form>
+             </div>
+           </div>
+           <form method="dialog" class="modal-backdrop">
+             <button>close</button>
+           </form>
+         </dialog>
        </:action>
      </.table>

457ba524f5c4f2d5095f0ba931dbb910.gif

ユーザー削除

メールアドレス登録してないので、一度ログアウトしたらログインはできないのでユーザー削除をおこないます

ポケットに入れてると誤タップ連射して、勝手に消えてしまう悲劇を防ぐためにテキストボックスに利用停止と入力したら削除が有効化するようにします

API

has_manyで on_delete: :delete_allをつけてユーザー削除されたら関連データを消すようにします

lib/blog/accounts/user.ex
  schema "users" do
    field :email, :string
    field :password, :string, virtual: true, redact: true
    field :hashed_password, :string, redact: true
    field :confirmed_at, :utc_datetime
    field :authenticated_at, :utc_datetime, virtual: true

+   has_many :posts, Blog.Posts.Post, on_delete: :delete_all
+   has_many :tokens, Blog.Accounts.UserToken, on_delete: :delete_all

    timestamps(type: :utc_datetime)
  end

削除する関数を追加します

lib/blog/accounts.ex
  def terminate_user(user) do
    Repo.delete(user)
  end

APIを追加します

lib/blog_web/controllers/api/v1/user_controller.ex
  def terminate(conn, _) do
    user = conn.assigns.current_scope.user

    case Accounts.terminate_user(user) do
      {:ok, _user} ->
        send_resp(conn, 200, "OK")

      _ ->
        send_resp(conn, 400, "Bad Request")
    end
  end
lib/blog_web/router.ex
  scope "/api/v1", BlogWeb.Api.V1 do
    pipe_through [:api, :require_verify_header]

    get "/users/status", UserController, :status
    post "/refresh_token", UserController, :refresh_token
    post "/logout", UserController, :logout
+   post "/terminate", UserController, :terminate

    resources "/posts", PostController, except: [:new, :edit]
  end

モバイル

APIリクエスト部分を作成します、成功したら端末にあるトークンも削除します

  def terminate() do
    {:ok, resp} = Req.post(Api.client(), url: "/terminate")

    case resp.status do
      200 ->
        File.rm(BlogApp.config_dir() <> "/token")
        :ok

      _ ->
        nil
    end
  end

liveview側を作ります
dialogのモーダルはliveviewの更新イベントで閉じられてしまうので、liveview側のopenフラグで制御します
また利用停止は、テキストボックスに利用停止と入力しないと押せないようになってます

lib/blog_app_web/live/user_live/settings.ex
  def render(assigns) do
    ~H"""
    <Layouts.app
      name={:setting}
      page_title="Settings"
      flash={@flash}
      current_scope={@current_scope}
    >
      <.form for={@email_form} id="email_form" phx-submit="update_email" phx-change="validate_email">
      </.form>

      <div class="divider" />
+     <.link
+       phx-click="open"
+       class="btn btn-error text-white normal-case text-xl"
+     >
+       利用停止
+     </.link>
+
+     <dialog id="terminate" class="modal" open={@open}>
+       <div class="modal-box">
+         <h3 class="text-lg font-bold">BlogAppの利用停止</h3>
+         <p class="py-4">この操作は取り消せません</p>
+         <p>利用停止を行うと、すべてのデータはアクセスできなくなり、元に戻すことはできません。</p>
+         <br />
+
+         <form phx-change="change">
+           <label class="text-error" for="terminate">利用停止と入力してください</label>
+           <.input type="text" name="terminate" value={@terminate} />
+         </form>
+         <div class="modal-action">
+           <button
+             class="btn btn-error text-white"
+             phx-click="terminate"
+             disabled={!(@terminate == "利用停止")}
+           >
+             利用停止
+           </button>
+           <form method="dialog">
+             <button phx-click="close" class="btn">キャンセル</button>
+           </form>
+        </div>
+       </div>
+       <form method="dialog" class="modal-backdrop">
+         <button phx-click="close">close</button>
+       </form>
+     </dialog>
    </Layouts.app>
    """
  end

  @impl true
  def mount(_params, _session, socket) do
    user = socket.assigns.current_scope.user
    email_changeset = Accounts.change_user_email(user, %{}, validate_unique: false)

    socket =
      socket
      |> assign(:current_email, user.email)
      |> assign(:email_form, to_form(email_changeset))
      |> assign(:trigger_submit, false)
+     |> assign(:open, false)
+     |> assign(:terminate, "")

    {:ok, socket}
  end

  @impl true
  def handle_event("validate_email", params, socket) do
  end

  def handle_event("update_email", params, socket) do
  end

+ def handle_event("open", _, socket), do: {:noreply, assign(socket, :open, true)}

+ def handle_event("close", _, socket) do
+   socket
+   |> assign(:open, false)
+   |> assign(:terminate, "")
+   |> then(&{:noreply, &1})
+ end

+ def handle_event("change", params, socket) do
+   socket
+   |> assign(:terminate, params["terminate"])
+   |> then(&{:noreply, &1})
+ end

+ def handle_event("terminate", _params, socket) do
+   case Accounts.terminate() do
+     :ok ->
+       socket
+       |> put_flash(:info, "BlogAppを利用停止しました")
+       |> redirect(to: ~p"/")
+       |> then(&{:noreply, &1})
+
+     _ ->
+       socket
+       |> put_flash(:error, "利用停止に失敗しました")
+       |> assign(:open, false)
+       |> assign(:terminate, "")
+       |> then(&{:noreply, &1})
+   end
  end

動作確認

利用停止と入力されるまで、押せない状態
押すと消したユーザーのpostsとtokensが消されている
削除後トップページに戻される

5af4d230c6528507de6ac248b0a08e09.gif

最後に

本記事では、confirmモーダルをdaisyuiへの変更
キーワードを入力しないとユーザーが消せないようにしました

次はファイルアップロードを実装します

9
0
0

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
9
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?