LoginSignup
11
3

Brightコードハック①Elixirプロダクション攻略:【Web CRUD】LiveView Scaffold+Tailwindで爆速開発

Last updated at Posted at 2023-12-06

この記事は、Elixir Advent Calendar 2023 シリーズ12 の7日目です


piacere です、ご覧いただいてありがとございます :bow:

過去/現在/未来のスキルから、あなたのBright(輝き)とRight(正しさ)を引き出すプロダクト「Bright」を今年後半にαリリースしました

まもなくβリリースするBrightの実装から、Elixirプロダクション開発のヒントをお届けします

なお、Brightが生まれた背景は、昨年Elixir Advent Calendarで下記にまとめています(400いいねを超える人気コラム)

あと、このコラムが、面白かったり、役に立ったら、image.png をお願いします :bow:

LiveView Scaffold+Tailwindで爆速開発

今回は、Web CRUD開発の基本となる「LiveView Scaffold」と、最新のPhoenix 1.7で標準搭載化された「Tailwind」の組み合わせで、いかに簡単にプロダクト開発できるかを解説します

実際にPJを作成し、コーディングしていくので、ぜひお手元でもお試しください
image.png

事前準備:Phoenix PJの作成

前提として、ElixirとPostgreSQLのインストールが済んでいることとします

未インストールの方は、エリクサーチの下記コラムを参考に構築してください

https://elixir-lang.info/topics/entry_column
image.png

https://elixir-lang.info/topics/web_spa
※下図「③ローカル環境上でLiveView環境を構築(その後は上記②がローカルで可能となる)」のPostgreSQL部分のみ実施してください
image.png

次に、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初期画面が表示できます
image.png

LiveView Scaffoldの基本

LiveView Scaffoldを使うと、DB CRUD画面の自動生成ができ、テーブル作成のためのマイグレーションファイルも生成できます

ここでは、Brightの「スキルパネル」機能の下記赤枠部分と似たようなものを作ってみます
https://app.bright-fun.org/panels/01HA663FMC0SM4AX1D56750QAN?class=1
image.png

上記画面に存在する項目相当として、下記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に下記を追加します(なお、上記コマンド実施時にターミナルにも出力されます)

/lib/bright_web/router.ex
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 も同様)
image.png

「New Skill」ボタンからデータ追加できます(作成後、「Edit」リンクや「Delete」リンクでデータ操作もできます)
image.png

skills にサンプルデータを下記のように追加します
image.png

skills に紐づく skill_scores のサンプルデータも下記のように追加します
image.png

テーブル同士をリレーションさせる

skillsskill_scores の間に、1:n のリレーションを設定します

/lib/bright/skill_units/skill.ex
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 時に結合して取得します

/lib/bright/skill_units.ex
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 でユーザー指定できるよう修正します

/lib/bright_web/live/skill_live/index.ex
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 行を削除)

/lib/bright_web/live/skill_live/index.html.heex
<: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 を表示すると、以下のようになります
image.png

http://localhost:4000/skills?id=2 だと下記です
image.png

ユーザー毎の保有スキルである skill_scoresskills に紐付いて表示(保有スキル無時は空リスト)できるようになりました

PhoenixにおけるTailwindの基本

上記データテーブルをPhoenix 1.7から標準装備のTailwindでビジュアルを良くしていきましょう

データテーブルのHTMLは、これまたPhoenix 1.7で機能追加された「core_components」の table 関数にあります

/lib/bright_web/components/core_components.ex
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"]}>

外枠はBrightの「スキルパネル」そっくりになりました
image.png

保有スキルのアイコン化

保有スキルを「●」「▲」「-」で表示分けします

/lib/bright_web/live/skill_live/index.html.heex
<!-- 下記を追加 -->
- <: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の「スキルパネル」そっくりになりました
image.png

ここまでで習得したスキルは?

さて、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
  • 「Webアプリ開発 Elixir」
    • クラス1:零細Web開発をサポート無しでこなせる
      • Tailwind
    • クラス2:小規模Web開発/マイクロサービスできる
      • ピン演算子

image.png

ぜひ下記からBrightを無料ユーザー登録して、スキルパネル「Elixir入門」「Webアプリ開発 Elixir」に、ここまで習得したスキルに「●」をスキル入力してみてください

イマイチ内容とスキルが結びついていない方も、Brightを使いながら、該当スキルを調べたり、復習をしてみてください

次回は、プロダクションの要である認証周りとして、phx_gen_auth+Überauthで独自ID+SNSログインを攻略します

p.s.このコラムが、面白かったり、役に立ったら…

image.png にて、どうぞ応援よろしくお願いします :bow:

11
3
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
11
3