この記事は、Elixir Advent Calendar 2023 シリーズ12 の7日目です
piacere です、ご覧いただいてありがとございます
過去/現在/未来のスキルから、あなたのBright(輝き)とRight(正しさ)を引き出すプロダクト「Bright」を今年後半にαリリースしました
まもなくβリリースするBrightの実装から、Elixirプロダクション開発のヒントをお届けします
なお、Brightが生まれた背景は、昨年Elixir Advent Calendarで下記にまとめています(400いいねを超える人気コラム)
あと、このコラムが、面白かったり、役に立ったら、 をお願いします
LiveView Scaffold+Tailwindで爆速開発
今回は、Web CRUD開発の基本となる「LiveView Scaffold」と、最新のPhoenix 1.7で標準搭載化された「Tailwind」の組み合わせで、いかに簡単にプロダクト開発できるかを解説します
実際にPJを作成し、コーディングしていくので、ぜひお手元でもお試しください
事前準備:Phoenix PJの作成
前提として、ElixirとPostgreSQLのインストールが済んでいることとします
未インストールの方は、エリクサーチの下記コラムを参考に構築してください
https://elixir-lang.info/topics/entry_column
https://elixir-lang.info/topics/web_spa
※下図「③ローカル環境上でLiveView環境を構築(その後は上記②がローカルで可能となる)」のPostgreSQL部分のみ実施してください
次に、Phoenixをインストールし、Phoenix PJを作成した後、起動します
なお、BrightのPhoenixは1.7.3ですが、2023年12月5日時点の最新版は1.7.10でした
mix archive.install hex phx_new
mix phx.new bright
cd bright
mix ecto.create
iex -S mix phx.server
ブラウザで http://localhost:4000
にアクセスすると、PJ初期画面が表示できます
LiveView Scaffoldの基本
LiveView Scaffoldを使うと、DB CRUD画面の自動生成ができ、テーブル作成のためのマイグレーションファイルも生成できます
ここでは、Brightの「スキルパネル」機能の下記赤枠部分と似たようなものを作ってみます
https://app.bright-fun.org/panels/01HA663FMC0SM4AX1D56750QAN?class=1
上記画面に存在する項目相当として、下記4つのテーブルとDB CRUD画面を自動生成してみます
- skill_units
- skill_categories
- skills
- skill_scores
なお、実際のBrightにおける各テーブルIDは、UUIDを利用していますが、ここでは簡便化のためにintegerで定義します
またuser_idは、phx.gen.auth
での生成をBrightでは利用していますが、ここではタダのintegerで定義し、GETパラメータでスイッチする想定とします
mix phx.gen.live SkillUnits SkillUnit skill_units name:string loced_date:datetime trace_id:integer
mix phx.gen.live SkillUnits SkillCategory skill_categories skill_unit_id:references:skill_units name:string position:integer trace_id:integer
mix phx.gen.live SkillUnits Skill skills skill_category_id:references:skill_categories name:string position:integer trace_id:integer
mix phx.gen.live SkillScores SkillScore skill_scores user_id:integer skill_id:integer score:string exam_progress:string reference_read:boolean evidence_filled:boolean
routerに下記を追加します(なお、上記コマンド実施時にターミナルにも出力されます)
defmodule BrightWeb.Router do
use BrightWeb, :router
…
scope "/", BrightWeb do
pipe_through :browser
## ここから追加
live "/skill_units", SkillUnitLive.Index, :index
live "/skill_units/new", SkillUnitLive.Index, :new
live "/skill_units/:id/edit", SkillUnitLive.Index, :edit
live "/skill_units/:id", SkillUnitLive.Show, :show
live "/skill_units/:id/show/edit", SkillUnitLive.Show, :edit
live "/skill_categories", SkillCategoryLive.Index, :index
live "/skill_categories/new", SkillCategoryLive.Index, :new
live "/skill_categories/:id/edit", SkillCategoryLive.Index, :edit
live "/skill_categories/:id", SkillCategoryLive.Show, :show
live "/skill_categories/:id/show/edit", SkillCategoryLive.Show, :edit
live "/skills", SkillLive.Index, :index
live "/skills/new", SkillLive.Index, :new
live "/skills/:id/edit", SkillLive.Index, :edit
live "/skills/:id", SkillLive.Show, :show
live "/skills/:id/show/edit", SkillLive.Show, :edit
live "/skill_scores", SkillScoreLive.Index, :index
live "/skill_scores/new", SkillScoreLive.Index, :new
live "/skill_scores/:id/edit", SkillScoreLive.Index, :edit
live "/skill_scores/:id", SkillScoreLive.Show, :show
live "/skill_scores/:id/show/edit", SkillScoreLive.Show, :edit
## ここまで追加
get "/", PageController, :home
end
…
下記コマンドでマイグレートします
mix ecto.migrate
ブラウザで http://localhost:4000/skills
にアクセスすると、下記画面でDB CRUD操作できるようになります(/skill_units
や /skill_categories
、/skill_scores
も同様)
「New Skill」ボタンからデータ追加できます(作成後、「Edit」リンクや「Delete」リンクでデータ操作もできます)
skills
に紐づく skill_scores
のサンプルデータも下記のように追加します
テーブル同士をリレーションさせる
skills
と skill_scores
の間に、1:n のリレーションを設定します
defmodule Bright.SkillUnits.Skill do
use Ecto.Schema
import Ecto.Changeset
schema "skills" do
field :name, :string
field :position, :integer
field :trace_id, :integer
field :skill_category_id, :id
# 下記を追加
has_many :skill_scores, Bright.SkillScores.SkillScore
timestamps(type: :utc_datetime)
end
…
skills
の一覧に skill_scores
をLEFT JOINで user_id
一致時、かつ skill_id
一致時もしくは nil
時に結合して取得します
defmodule Bright.SkillUnits do
…
def list_skills do
Repo.all(Skill)
end
# 下記を追加
alias Bright.SkillScores.SkillScore
def list_skills_with_scores(nil), do: []
def list_skills_with_scores(user_id) do
Repo.all(from skill in Skill,
left_join: score in SkillScore,
on: ^user_id == score.user_id and (skill.id == score.skill_id or is_nil(score.skill_id)),
order_by: skill.id,
preload: [skill_scores: score])
end
…
LiveView mount時に上記関数を使い、GETパラメータ id
でユーザー指定できるよう修正します
defmodule BrightWeb.SkillLive.Index do
…
@impl true
def mount(params, _session, socket) do
# {:ok, stream(socket, :skills, SkillUnits.list_skills())}
# 下記に変更
{:ok, stream(socket, :skills, SkillUnits.list_skills_with_scores(params["id"]))}
end
…
skill.skill_scores
をinspectします(ついでに不要な skill.position
行/skill.trace_id
行を削除)
…
<:col :let={{_id, skill}} label="Name"><%= skill.name %></:col>
<!-- skill.position行を削除 -->
<!-- 下記を追加 -->
<:col :let={{_id, skill}} label="Score"><%= skill.skill_scores |> inspect %></:col>
<!-- skill.trace_id行を削除 -->
<:action :let={{_id, skill}}>
…
http://localhost:4000/skills?id=1
を表示すると、以下のようになります
http://localhost:4000/skills?id=2
だと下記です
ユーザー毎の保有スキルである skill_scores
を skills
に紐付いて表示(保有スキル無時は空リスト)できるようになりました
PhoenixにおけるTailwindの基本
上記データテーブルをPhoenix 1.7から標準装備のTailwindでビジュアルを良くしていきましょう
データテーブルのHTMLは、これまたPhoenix 1.7で機能追加された「core_components」の table
関数にあります
defmodule BrightWeb.CoreComponents do
…
def table(assigns) do
…
~H"""
<div class="overflow-y-auto px-4 sm:overflow-visible sm:px-0">
<table class="w-[40rem] mt-11 sm:w-full">
<thead class="text-sm text-left leading-6 text-zinc-500">
<tr>
- <th :for={col <- @col} class="p-0 pb-4 pr-6 font-normal"><%= col[:label] %></th>
+ <th :for={col <- @col} class="py-3 text-center border border-gray-400 text-sm font-bold bg-black text-white"><%= col[:label] %></th>
<th :if={@action != []} class="relative p-0 pb-4">
<span class="sr-only"><%= gettext("Actions") %></span>
</th>
</tr>
</thead>
<tbody
id={@id}
phx-update={match?(%Phoenix.LiveView.LiveStream{}, @rows) && "stream"}
class="relative divide-y divide-zinc-100 border-t border-zinc-200 text-sm leading-6 text-zinc-700"
>
<tr :for={row <- @rows} id={@row_id && @row_id.(row)} class="group hover:bg-zinc-50">
<td
:for={{col, i} <- Enum.with_index(@col)}
phx-click={@row_click && @row_click.(row)}
- class={["relative p-0", @row_click && "hover:cursor-pointer"]}
+ class={["relative p-0 border border-gray-400", @row_click && "hover:cursor-pointer"]}
>
- <div class="block py-4 pr-6">
+ <div class="block">
<span class="absolute -inset-y-px right-0 -left-4 group-hover:bg-zinc-50 sm:rounded-l-xl" />
- <span class={["relative", i == 0 && "font-semibold text-zinc-900"]}>
+ <span class={["relative", i == 0 && "p-4 text-base"]}>
…
保有スキルのアイコン化
保有スキルを「●」「▲」「-」で表示分けします
…
<!-- 下記を追加 -->
- <:col :let={{_id, skill}} label="Score"><%= skill.skill_scores |> inspect %></:col>
+ <:col :let={{_id, skill}} label="Score">
+<%
+score = case skill.skill_scores |> List.first do
+ nil -> %{class: "", text: "-"}
+ n -> if n.score == "high" do
+ %{class: "text-teal-600", text: "●"}
+ else
+ %{class: "text-teal-400", text: "▲"}
+ end
+end
+%>
+<p class="text-center text-lg"><span class={score.class}><%= score.text %></span></p>
+ </:col>
<!-- skill.trace_id行を削除 -->
…
ここまでで習得したスキルは?
さて、Bright「スキルパネル」そっくりな画面を作るのに、下記のスキルを習得したことになります
- 「Elixir入門」
- クラス1:個人で簡単なWeb+DBアプリが作れる
- if~else/case/cond
- リスト/マップ
- Web inspectデバッグ
- mix archive.install hex phx_new
- mix phx.new
- mix phx.server/iex -S mix phx.server
- Router(router.ex)追加/変更
- mix phx.gen.live(scaffold)/ブラウザ上CRUD操作
- scaffold生成.html.heexの「<%= ~ %>」追加/改修
- scaffold生成.exのmount改修
- Scaffold後のmix ecto.create
- Scaffold後のmix ecto.migrate
- HTML
- クラス2:Web+DBアプリ開発チームに参画できる
- 関数パターンマッチ
- scaffold生成のContext/Schema改修
- Ecto.Query記述(from/DSL両方)
- クラス3:サポート有ならWeb+DBチーム開発できる
- params
- has_one/has_many/many_to_many/belongs_to
- preload
- クラス1:個人で簡単なWeb+DBアプリが作れる
- 「Webアプリ開発 Elixir」
- クラス1:零細Web開発をサポート無しでこなせる
- Tailwind
- クラス2:小規模Web開発/マイクロサービスできる
- ピン演算子
- クラス1:零細Web開発をサポート無しでこなせる
ぜひ下記からBrightを無料ユーザー登録して、スキルパネル「Elixir入門」「Webアプリ開発 Elixir」に、ここまで習得したスキルに「●」をスキル入力してみてください
イマイチ内容とスキルが結びついていない方も、Brightを使いながら、該当スキルを調べたり、復習をしてみてください
次回は、プロダクションの要である認証周りとして、phx_gen_auth+Überauthで独自ID+SNSログインを攻略します