こんにちは!
プログラミング未経験文系出身、Elixirの国に迷い込んだ?!見習いアルケミストのaliceと申します。
今回はElixirDesktop + Phoenix1.7のスマホアプリにCRUDの機能を実装したいと思います。
なお、本記事は2023/7/13開催ElixirMobile#3:DBも内蔵したElixirDesktopスマホアプリ開発ハンズオンのイベントレポート(後半)を兼ねています。
------ハンズオンの様子(後半)はこちらから------
↓27:03~以前が前回の記事、以降が本記事のハンズオン動画です
↓「微調整」の部分以降の動画です。
■「ElixirDesktopでスマホアプリを作る」シリーズの目次
①Windows11+WSL2に環境構築
|> ②Phoenix1.7のアプリを起動
|> ③LiveViewを使用してCRUDの機能を実装する
|> ④Android実機に転送する(Google Drive経由)
目的
こちらの記事をWindows11 + WSL2の環境で動かします。
実行環境
Windows 11 + WSL2 + Ubuntu 22.04
Elixir v1.14.3
Erlang v25.0.4
Phoenix v1.7.3
前提条件
本記事は、前回の記事までの実施が完了している前提で進めます。
CRUD作成
LiveViewのスキャフォールドを使用して、CRUDの機能を実装します。
mix phx.gen.live Tasks Task tasks name:string status:boolean
上記コマンド実行後、コンソールの指示に従って以下を追加します。
scope "/", TodoAppWeb do
pipe_through :browser
get "/", PageController, :home
+ live "/tasks", TaskLive.Index, :index
+ live "/tasks/new", TaskLive.Index, :new
+ live "/tasks/:id/edit", TaskLive.Index, :edit
+ live "/tasks/:id", TaskLive.Show, :show
+ live "/tasks/:id/show/edit", TaskLive.Show, :edit
end
Migration周り
ElixirDesktopにはマイグレーションしてくれる機能がありません。
よって起動時にマイグレーションを実行する機能の実装が必要です。
migrationsディレクトリのコピーとリネーム
todo_app/priv/repo/migrations
フォルダ全体をtodo_app/lib/todo_app/
にコピーします。
xxxx_create_tasks.exs
をxxxx_create_tasks.ex
にリネームします。1
※青枠の部分が実施前、赤枠の部分が実施後のmigrations周りのディレクトリです。
名前衝突を防ぐためにモジュール名をリネームします。
- defmodule TodoApp.Repo.Migrations.CreateTasks do
+ defmodule TodoApp.Migrations.CreateTasks do
...
end
起動時にマイグレーションする設定
前回の記事でrepo.ex
内に作成したinitialize関数に、マイグレーションを実行する内容を書いていきます。
Ecto.Migrator.upはマイグレーションを実行する関数で
第1引数はRepoモジュール
第2引数はマイグレーション番号、今回はファイル名のタイムスタンプ
第3引数は先ほどコピーしたマイグレーションファイルのモジュール名
をそれぞれ指定します。
defmodule TodoApp.Repo do
...
def initialize() do
+ Ecto.Migrator.up(TodoApp.Repo, 20_230_712_123_236, TodoApp.Migrations.CreateTasks)
end
end
起動時に開くページを指定
前回の記事完了時点では、起動時にPhoenix1.7のトップページ2が開かれています。
起動時の設定を変更し、エンドポイント/tasks
が起動時に開かれるようにします。
defmodule TodoApp do
...
@app Mix.Project.config()[:app]
def start(:normal, []) do
...
{:ok, _} =
Supervisor.start_child(sup, {
Desktop.Window,
[
app: @app,
id: TodoAppWindow,
title: "TodoApp",
size: {400, 800},
- url: "http://localhost:#{port}"
+ url: "http://localhost:#{port}/tasks"
]
})
end
end
以上がPhonenixプロジェクトの設定です。
androidでの起動
再ビルド
ブランクスクリプトではない方を走らせて、リビルドします。
cd ../android-example-app/app
./run_mix_build
Android StudioでRunする
前回の記事の手順に従って、Android StudioでRunします。
無事CRUD画面が表示されました(^▽^)/
(補足)"table tasks already exits"という内容のエラーが出て変更内容がandroidに反映されない場合
"table tasks already exits"というエラーが返ってくる場合、DBのリネームが必要です。
# DBの場所を指定
Application.put_env(:todo_app, TodoApp.Repo,
- database: Path.join(config_dir(), "/database.sq3")
+ database: Path.join(config_dir(), "/database2.sq3")
)
微調整
元記事様と同様に、微調整を行います。
core_component カスタム
core_components.ex内にあるtableで表のサイズが指定されていますので、これを微調整します。
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を追加
元記事様と同様に、削除リンクをモーダルを開くナビゲーションにしてindex.html.heexの末尾にconfirm modalを表示するようにします。
削除イベントのトリガー(発火)部分
editを参考にイベントの発火部分と、deleteモーダルのレンダリング部分を追加します。
...
<.table
id="tasks"
rows={@streams.tasks}
row_click={fn {_id, task} -> JS.navigate(~p"/tasks/#{task}") end}
>
<:col :let={{_id, task}} label="Name"><%= task.name %></:col>
<:col :let={{_id, task}} label="Status"><%= task.status %></:col>
<:action :let={{_id, task}}>
<div class="sr-only">
<.link navigate={~p"/tasks/#{task}"}>Show</.link>
</div>
<.link patch={~p"/tasks/#{task}/edit"}>Edit</.link>
</:action>
- <:action :let={{id, task}}>
+ <:action :let={{_id, task}}> #使用しない変数idを_で潰しておく。
<.link
- phx-click={JS.push("delete", value: %{id: task.id}) |> hide("##{id}")}
- data-confirm="Are you sure?"
+ patch={~p"/tasks/#{task}/delete"} #"delete"のリンクを押下した際に部分的にページを更新する。
>
Delete
</.link>
</:action>
</.table>
...
+ <.modal
+ :if={@live_action in [:delete]} #変数live_actionがdeleteのときに表示されるモーダル。
+ id="task-delete-modal" #idは一意の値。
+ show
+ on_cancel={JS.navigate(~p"/tasks")} #モーダルのキャンセルボタンが押下されたときは~p"/tasks"に遷移する。
+ >
+ <p class="m-8">Are you sure?</p>
+ <.button phx-click={JS.push("delete", value: %{id: @task.id})}>Delete</.button> #ボタンのレンダリング、およびそのボタンが押下された時に、valueとして渡されたtask.idの内容を削除するイベントをトリガーする。
+ </.modal>
-
patch={~p"/tasks/#{task}/delete"}
の部分が増えたので、新しいルーティングが必要になります。(「削除イベントを実行させるルーティング」参照) -
:if={@live_action in [:delete]}
の部分が増えたため、index.ex内handle_params関数のパターンマッチにヒットするようなapply_action関数が必要になります(「削除イベントのハンドラ部分」参照) -
phx-click={JS.push("delete", value: %{id: @task.id}
の部分が増えたので、トリガーしたイベントの処理内容を書く関数が必要になります。(「削除イベントのハンドラ部分」参照)
削除イベントのハンドラ部分
こちらもeditを参考にapply_action部分と、トリガーしたイベントの処理内容を追加します。
defmodule TodoAppWeb.TaskLive.Index do
use TodoAppWeb, :live_view
alias TodoApp.Tasks
alias TodoApp.Tasks.Task
...
def handle_params(params, _url, socket) do
{:noreply, apply_action(socket, socket.assigns.live_action, params)}
end
...
defp apply_action(socket, :index, _params) do
socket
|> assign(:page_title, "Listing Tasks")
|> assign(:task, nil)
end
+ defp apply_action(socket, :delete, %{"id" => id}) do
+ socket
+ |> assign(:page_title, "Delete Task")
+ |> assign(:task, Tasks.get_task!(id))
+ end
@impl true
def handle_info({TodoAppWeb.TaskLive.FormComponent, {:saved, task}}, socket) do
{:noreply, stream_insert(socket, :tasks, task)}
end
@impl true
def handle_event("delete", %{"id" => id}, socket) do
task = Tasks.get_task!(id)
{:ok, _} = Tasks.delete_task(task)
+ {
+ :noreply,
+ socket
+ |> put_flash(:info, "Task deleted successfully") # flashを追加
+ |> push_navigate(to: ~p"/tasks") # モーダルを閉じる
+ |> stream_delete(:tasks, task) # 要素が削除されたことをstream上に反映させる
+ }
end
end
削除イベントを実行させるルーティング
routeにdelete actionを追加します
scope "/", TodoAppWeb do
pipe_through :browser
get "/", PageController, :home
live "/tasks", TaskLive.Index, :index
live "/tasks/new", TaskLive.Index, :new
live "/tasks/:id/edit", TaskLive.Index, :edit
+ live "/tasks/:id/delete", TaskLive.Index, :delete # 追加
live "/tasks/:id", TaskLive.Show, :show
live "/tasks/:id/show/edit", TaskLive.Show, :edit
end
androidでの起動(微調整後)
再ビルド
ブランクスクリプトではない方を走らせて、リビルドします
cd ../android-example-app/app
./run_mix_build
Android StudioでRunする
前回の記事の手順に従って、Android StudioでRunします。
DELETEの機能がついて、CRUDの機能が一通り実装されました(^▽^)/
~Elixirの国のご案内~
↓Elixirって何ぞや?と思ったらこちらもどぞ。Elixirは先端のアレコレをだいたい全部できちゃいます
↓ゼロからElixirを始めるなら「エリクサーチ」がおすすめ!私もエンジニア未経験から学習中です。
↓We Are The Alchemists, my friends!3
Elixirコミュニティは本当に優しくて温かい人たちばかり!
私が挫折せずにいられるのもこの恵まれた環境のおかげです。
まずは気軽にコミュニティを訪れてみてください。4