はじめに
本記事は以下の記事で作成したプロジェクトに
1 Tailwind + DaisyUIでスマホUIの作成
2 phx.gen.authで認証機能
3 phx.gen.liveでCRUD作成
をそれぞれ実装していきます
tailwind追加
いつもどおり本家を参考に追加していきます
defmodule Trarecord.MixProject do
use Mix.Project
defp deps do
[
...
{:tailwind, "~> 0.1", runtime: Mix.env() == :dev} # 追加
]
end
end
import Config
...
# 以下追加
config :tailwind,
version: "3.1.8",
default: [
args: ~w(
--config=tailwind.config.js
--input=css/app.css
--output=../priv/static/assets/app.css
),
cd: Path.expand("../assets", __DIR__)
]
defmodule Trarecord.MixProject do
use Mix.Project
...
defp aliases do
[
...
# assets.deploy差し替え
"assets.deploy": ["tailwind default --minify", "esbuild default --minify", "phx.digest"]
]
end
end
desktopとwebにそれぞれ開発環境時にtailwindをホットリロードできるようにします
if config_env() == :dev do
config :trarecord, TrarecordWeb.EndpointWeb,
...
watchers: [
# Start the esbuild watcher by calling Esbuild.install_and_run(:default, args)
esbuild: {Esbuild, :install_and_run, [:default, ~w(--sourcemap=inline --watch)]},
tailwind: {Tailwind, :install_and_run, [:default, ~w(--watch)]} # 追加
]
...
end
if config_env() == :dev do
config :trarecord, TrarecordWeb.EndpointWeb,
...
watchers: [
# Start the esbuild watcher by calling Esbuild.install_and_run(:default, args)
esbuild: {Esbuild, :install_and_run, [:default, ~w(--sourcemap=inline --watch)]},
tailwind: {Tailwind, :install_and_run, [:default, ~w(--watch)]} # 追加
]
...
end
最後に関連ファイルをインストールします
mix tailwind.install
DaisyUI
こちらも公式通りにおこないます
npm関連はassets以下でおこなうのでcd assets
しておきましょう
cd assets
npm i daisyui
cd ..
let plugin = require('tailwindcss/plugin')
module.exports = {
content: [
'./js/**/*.js',
'../lib/*_web.ex',
'../lib/*_web/**/*.*ex'
],
theme: {
extend: {},
},
plugins: [
require('@tailwindcss/forms'),
require('daisyui'), // 追加
...
]
}
スマホアプリビルド時に npm iで関連ライブインストールするように前記事でコメントアウトした箇所をコメントインします
# 以下をコメントイン
if [ ! -d "assets/node_modules" ]; then
cd assets && npm i && cd ..
fi
# 以下をコメントイン
if [ ! -d "assets/node_modules" ]; then
cd assets && npm i && cd ..
fi
TailwindとDaisyUIのセットアップは以上になります
認証機能実装
次に認証機能を実装します
例のごとくコマンド一発で完了
mix phx.gen.auth Users User users
mix deps.get
mix ecto.migrate
Bcrypt -> Pbkdf2 に変更
Android/iOSでパスワードハッシュライブラリがBcryptだとエラーになるのでライブラリをPbkdf2に変更します
defmodule Trarecord.MixProject do
use Mix.Project
...
defp deps do
[
...
{:pbkdf2_elixir, "~> 2.0"}
]
end
...
end
defmodule Trarecord.Users.User do
use Ecto.Schema
import Ecto.Changeset
...
defp maybe_hash_password(changeset, opts) do
hash_password? = Keyword.get(opts, :hash_password, true)
password = get_change(changeset, :password)
if hash_password? && password && changeset.valid? do
changeset
# If using Bcrypt, then further validate it is at most 72 bytes long
|> validate_length(:password, max: 72, count: :bytes)
|> put_change(:hashed_password, Pbkdf2.hash_pwd_salt(password)) # Pbkdf2に変更
|> delete_change(:password)
else
changeset
end
end
@doc """
Verifies the password.
If there is no user or the user doesn't have a password, we call
`Pbkdf2.no_user_verify/0` to avoid timing attacks. # Pbkdf2に変更
"""
def valid_password?(%Trarecord.Users.User{hashed_password: hashed_password}, password)
when is_binary(hashed_password) and byte_size(password) > 0 do
Pbkdf2.verify_pass(password, hashed_password) # Pbkdf2に変更
end
def valid_password?(_, _) do
Pbkdf2.no_user_verify() # Pbkdf2に変更
false
end
end
ログイン前の画面
desktopの方にはリロードボタンがなくて面倒なので基本ブラウザで開発していきます
デベロッパーコンソール等でスマホサイズに画面をしておきましょう
MIX_TARGET=host iex -S mix phx.server
トップページ
header箇所を削除
headerタグを削除します
ついでにbg-colorもセットします
<!DOCTYPE html>
<html lang="en">
<head>
...
</head>
<body class="bg-base-200">
<%= @inner_content %>
</body>
</html>
トップページの作成
daisyuiのheroのサンプルを貼り付けます
priv/static/images に top.pngという名前でおしゃれな写真を入れておいてください
pタグ内にそれっぽい内容に書き換えます
<div class="hero min-h-screen" style={"background-image: url(#{Routes.static_path(@conn, "/images/top.png")});"}>
<div class="hero-overlay bg-opacity-60"></div>
<div class="hero-content text-center text-neutral-content">
<div class="max-w-md">
<h1 class="mb-5 text-5xl font-bold">Welcome to Trarecord</h1>
<p class="mb-5">Trarecord is travel note. we provides pick up spot, record visited spot information and more...</p>
<%= link "Get Started", to: Routes.user_registration_path(@conn, :new), class: "btn btn-primary" %>
</div>
</div>
</div>
各種 phx.gen.authのページをスタイリング
基本的にはTailblockのCTAの2つ目をベースにします
register
<div class="hero min-h-screen">
<div class="hero-content flex-col lg:flex-row-reverse">
<div class="text-center lg:text-left">
<h1 class="text-5xl font-bold">Register now!</h1>
<p class="py-6">Trarecord is Travel notes.</p>
<p class="py6">pick up want to spot. record visited spot information and more...</p>
</div>
<div class="card flex-shrink-0 w-full max-w-sm shadow-2xl bg-base-100">
<div class="card-body">
<.form let={f} for={@changeset} action={Routes.user_registration_path(@conn, :create)}>
<%= if @changeset.action do %>
<div class="alert alert-danger">
<p>Oops, something went wrong! Please check the errors below.</p>
</div>
<% end %>
<div class="form-control">
<%= label f, :email, class: "label" %>
<%= email_input f, :email, required: true, class: "input input-bordered" %>
<%= error_tag f, :email %>
</div>
<div class="form-control">
<%= label f, :password, class: "label" %>
<%= password_input f, :password, required: true, class: "input input-bordered" %>
<%= error_tag f, :password %>
</div>
<label class="label">
<%= link "Log in", to: Routes.user_session_path(@conn, :new), class: "label-text-alt link link-hover" %>
</label>
<label class="label">
<%= link "Forgot your password?", to: Routes.user_reset_password_path(@conn, :new), class: "label-text-alt link link-hover" %>
</label>
<div class="form-control mt-6">
<%= submit "Register", class: "btn btn-primary" %>
</div>
</.form>
</div>
</div>
</div>
</div>
Login
<div class="hero min-h-screen">
<div class="hero-content flex-col lg:flex-row-reverse">
<div class="text-center lg:text-left">
<h1 class="text-5xl font-bold">Login now!</h1>
<p class="py-6">Trarecord is Travel notes.</p>
<p class="py-6">pick up want to spot. record visited spot information and more...</p>
</div>
<div class="card flex-shrink-0 w-full max-w-sm shadow-2xl bg-base-100">
<div class="card-body">
<.form let={f} for={@conn} action={Routes.user_session_path(@conn, :create)} as={:user}>
<%= if @error_message do %>
<div class="alert alert-danger">
<p><%= @error_message %></p>
</div>
<% end %>
<div class="form-control">
<%= label f, :email, class: "label" %>
<%= email_input f, :email, required: true, class: "input input-bordered" %>
<%= error_tag f, :email %>
</div>
<div class="form-control">
<%= label f, :password, class: "label" %>
<%= password_input f, :password, required: true, class: "input input-bordered" %>
<%= error_tag f, :password %>
</div>
<label class="label">
<%= link "Register", to: Routes.user_registration_path(@conn, :new), class: "label-text-alt link link-hover" %>
</label>
<label class="label">
<%= link "Forgot your password?", to: Routes.user_reset_password_path(@conn, :new), class: "label-text-alt link link-hover" %>
</label>
<div class="form-control mt-6">
<%= submit "Login", class: "btn btn-primary" %>
</div>
</.form>
</div>
</div>
</div>
</div>
ほぼ同じなので割愛
forgot password?
こちらはTailBlockのContact1つめのフォーム部分のみ流用しています
<section class="text-gray-600 body-font relative">
<div class="container px-5 py-24 mx-auto">
<div class="flex flex-col text-center md:w-2/3 mx-auto mb-12">
<h1 class="normal-case text-4xl">Trarecord</h1>
</div>
<div class="lg:w-1/2 md:w-2/3 flex flex-col mb-12 mx-auto">
<h1 class="sm:text-3xl text-2xl font-medium title-font mb-4 text-gray-900">Forgot your password?</h1>
</div>
<div class="lg:w-1/2 md:w-2/3 mx-auto">
<.form let={f} for={:user} action={Routes.user_reset_password_path(@conn, :create)}>
<%= label f, :email, class: "label" %>
<%= email_input f, :email, required: true, class: "input input-bordered w-full" %>
<div>
<%= submit "Send instructions to reset password", class: "btn btn-primary mt-6" %>
</div>
</.form>
<p class="mt-6">
<%= link "Register", to: Routes.user_registration_path(@conn, :new), class: "label-text-alt link link-hover" %> |
<%= link "Log in", to: Routes.user_session_path(@conn, :new), class: "label-text-alt link link-hover" %>
</p>
</div>
</div>
</section>
<section class="text-gray-600 body-font relative">
<div class="container px-5 py-24 mx-auto">
<div class="flex flex-col text-center md:w-2/3 mx-auto mb-12">
<h1 class="normal-case text-4xl">Trarecord</h1>
</div>
<div class="lg:w-1/2 md:w-2/3 flex flex-col mb-12 mx-auto">
<h1 class="sm:text-3xl text-2xl font-medium title-font mb-4 text-gray-900">Reset password</h1>
</div>
<div class="lg:w-1/2 md:w-2/3 mx-auto">
<.form let={f} for={@changeset} action={Routes.user_reset_password_path(@conn, :update, @token)}>
<%= if @changeset.action do %>
<div class="alert alert-danger">
<p>Oops, something went wrong! Please check the errors below.</p>
</div>
<% end %>
<%= label f, :password, "New password", class: "label" %>
<%= password_input f, :password, required: true, class: "input input-bordered w-full" %>
<%= error_tag f, :password %>
<%= label f, :password_confirmation, "Confirm new password", class: "label" %>
<%= password_input f, :password_confirmation, required: true, class: "input input-bordered w-full" %>
<%= error_tag f, :password_confirmation %>
<div class="mt-6">
<%= submit "Reset password", class: "btn btn-primary" %>
</div>
</.form>
<p class="mt-6">
<%= link "Register", to: Routes.user_registration_path(@conn, :new), class: "label-text-alt link link-hover" %> |
<%= link "Log in", to: Routes.user_session_path(@conn, :new), class: "label-text-alt link link-hover" %>
</p>
</div>
</div>
</section>
ほとんど同じなので割愛
ログイン後の画面
live_viewと通常のcontoller両方で使えるように Phoenix.Componentで共通コンポーネントを作成します
headerは左側を戻るボタン、右側をアクションを指定できるようにします
Navigation
defmodule TrarecordWeb.Components.Navigation do
use Phoenix.Component
def header(assigns) do
~H"""
<div class="fixed z-20 navbar bg-primary text-primary-content w-full">
<div class="navbar-start">
<%= if @back do %>
<%= live_redirect "Back", to: @back, class: "btn btn-ghost normal-case text-xl" %>
<% end %>
</div>
<div class="navbar-center">
<a class="btn btn-ghost normal-case text-4xl"><%= @title %></a>
</div>
<div class="navbar-end">
<%= if @action do %>
<%= @action %>
<% end %>
</div>
</div>
"""
end
def bottom_tab(assigns) do
~H"""
<div class="btm-nav">
<button class={if @title == "Home", do: "active", else: ""}>
<a href="/home">
<span class="btm-nav-label">Home</span>
</a>
</button>
<button class={if @title == "Items", do: "active", else: ""}>
<a href="/home">
<span class="btm-nav-label">Items</span>
</a>
</button>
<button class={if @title == "Setting", do: "active", else: ""}>
<a href="users/settings">
<span class="btm-nav-label">Setting</span>
</a>
</button>
</div>
"""
end
end
現在はログイン後のHome, CRUD予定のItems, 3つ目にSettingになります
Home
共通コンポーネントのヘッダーと下タブができたのでログイン後のホーム画面に追加します
defmodule TrarecordWeb.HomeLive do
use TrarecordWeb, :live_view
alias TrarecordWeb.Components.Navigation
@impl true
def mount(_args, _session, socket) do
socket
|> assign(:title, "Home")
|> then(&{:ok, &1})
end
end
画面構成はDaisyUIのHeroにします
<Navigation.header title={@title} back={false} action={false} />
<div class="hero min-h-screen bg-base-200">
<div class="hero-content text-center">
<div class="max-w-md">
<h1 class="text-5xl font-bold">Hello there</h1>
<p class="py-6">Provident cupiditate voluptatem et in. Quaerat fugiat ut assumenda excepturi exercitationem quasi. In deleniti eaque aut repudiandae et a id nisi.</p>
<button class="btn btn-primary">Get Started</button>
</div>
</div>
</div>
<Navigation.bottom_tab title={@title} />
最後にrouterに追加して完了です
defmodule TrarecordWeb.Router do
use TrarecordWeb, :router
...
scope "/", TrarecordWeb do
pipe_through [:browser, :require_authenticated_user]
get "/users/settings", UserSettingsController, :edit
put "/users/settings", UserSettingsController, :update
get "/users/settings/confirm_email/:token", UserSettingsController, :confirm_email
live "/home", HomeLive # 追加
end
...
end
Settingページ
phx.gen.authのsettings_controllerを表示します
ベースはTailBlockのContact1つめのフォーム部分
共通コンポーネントのaliasはcontrollerではなくviewですので注意が必要です
defmodule TrarecordWeb.UserSettingsView do
use TrarecordWeb, :view
alias TrarecordWeb.Components.Navigation # 追加
end
<Navigation.header title="Setting" back={false} action={false} />
<section class="text-gray-600 body-font relative">
<div class="container px-5 py-24 mx-auto">
<div class="lg:w-1/2 md:w-2/3 flex flex-col mb-12 mx-auto">
<h1 class="sm:text-3xl text-2xl font-medium title-font mb-4 text-gray-900">Settings</h1>
</div>
<div class="lg:w-1/2 md:w-2/3 mx-auto">
<div class="mt-5">
<h3 class="text-xl text-gray-900">Change email</h3>
<.form let={f} for={@email_changeset} action={Routes.user_settings_path(@conn, :update)} id="update_email">
<%= if @email_changeset.action do %>
<div class="alert alert-danger">
<p>Oops, something went wrong! Please check the errors below.</p>
</div>
<% end %>
<%= hidden_input f, :action, name: "action", value: "update_email" %>
<%= label f, :email, class: "label" %>
<%= email_input f, :email, required: true, class: "input input-bordered w-full" %>
<%= error_tag f, :email %>
<%= label f, :current_password, for: "current_password_for_email", class: "label" %>
<%= password_input f, :current_password, required: true, name: "current_password", id: "current_password_for_email", class: "input input-bordered w-full" %>
<%= error_tag f, :current_password %>
<div class="mt-5">
<%= submit "Change email", class: "btn btn-block btn-primary" %>
</div>
</.form>
</div>
<div class="mt-6">
<h3 class="text-xl text-gray-900">Change password</h3>
<.form let={f} for={@password_changeset} action={Routes.user_settings_path(@conn, :update)} id="update_password">
<%= if @password_changeset.action do %>
<div class="alert alert-danger">
<p>Oops, something went wrong! Please check the errors below.</p>
</div>
<% end %>
<%= hidden_input f, :action, name: "action", value: "update_password" %>
<%= label f, :password, "New password", class: "label" %>
<%= password_input f, :password, required: true, class: "input input-bordered w-full" %>
<%= error_tag f, :password %>
<%= label f, :password_confirmation, "Confirm new password", class: "label" %>
<%= password_input f, :password_confirmation, required: true, class: "input input-bordered w-full" %>
<%= error_tag f, :password_confirmation %>
<%= label f, :current_password, for: "current_password_for_password", class: "label" %>
<%= password_input f, :current_password, required: true, name: "current_password", id: "current_password_for_password", class: "input input-bordered w-full" %>
<%= error_tag f, :current_password %>
<div class="mt-6">
<%= submit "Change password", class: "btn btn-block btn-primary" %>
</div>
</.form>
</div>
<div class="mt-6">
<%= link "Log out", to: Routes.user_session_path(@conn, :delete), method: :delete, class: "btn btn-block btn-error" %>
</div>
</div>
</div>
</section>
<Navigation.bottom_tab title="Setting" />
ページの最後にログアウトボタンも追加しておきます
トップページ切り替え
次にログイン時はHome、非ログイン時にはユーザー登録ページにリダイレクトするようにします
defmodule TrarecordWeb.PageController do
use TrarecordWeb, :controller
alias Trarecord.Users.User
def index(conn, _params) do
redirect_to(conn, conn.assigns.current_user)
end
def redirect_to(conn, %User{}) do
redirect(conn, to: Routes.live_path(TrarecordWeb.Endpoint, TrarecordWeb.HomeLive))
end
def redirect_to(conn, nil) do
redirect(conn, to: Routes.user_registration_path(TrarecordWeb.Endpoint, :new))
end
end
Flashをtoastに変更
今のままだとヘッダーの上の段に出てしまうので、スマホでよくある右下に出るやつに変更します
fadeoutアニメーションが無いのでこちらを参考に追加します
let plugin = require('tailwindcss/plugin')
module.exports = {
content: [...],
theme: {
extend: {
// ここから
keyframes: {
disappear: {
"0%": { opacity: 1 },
"100%": { opacity: 0 },
},
},
animation: {
disappear: "disappear 3s ease 2s 1 forwards",
},
// ここまで追加
},
},
plugins: [...]
}
コンポーネントはこちら
appは通常のcontrollerで表示するページで適応されます
下タブに重ならないようにz-40としています
<main class="container">
<div class="toast toast-end z-40 animate-disappear">
<div>
<p
class={"alert alert-info " <> if is_nil(get_flash(@conn, :info)), do: "hidden", else: ""}
role="alert"
><%= get_flash(@conn, :info) %></p>
</div>
<div>
<p
class={"alert alert-danger " <> if is_nil(get_flash(@conn, :error)), do: "hidden", else: ""}
role="alert"
><%= get_flash(@conn, :error) %></p>
</div>
</div>
<%= @inner_content %>
</main>
liveは名前の通りLiveViewで表示するページで適応されます
<main class="container">
<div class="toast toast-end z-40 animate-disappear">
<div>
<p class={"alert alert-info " <> if is_nil(live_flash(@flash, :info)), do: "hidden", else: ""}
role="alert"
phx-click="lv:clear-flash"
phx-value-key="info">
<%= live_flash(@flash, :info) %>
</p>
</div>
<div>
<p class={"alert alert-danger " <> if is_nil(live_flash(@flash, :error)), do: "hidden", else: ""}
role="alert"
phx-click="lv:clear-flash"
phx-value-key="error">
<%= live_flash(@flash, :error) %>
</p>
</div>
</div>
<%= @inner_content %>
</main>
CRUD画面の追加
最後にCRUD画面を作ります
例のごとくphx.gen.liveで
リレーションまで加えると長くなるので今回は割愛します
mix phx.gen.live Items Item items name:string url:string image:string category:string status:boolean
mix ecto.migrate
出てきたルーティングを追加します
defmodule TrarecordWeb.Router do
use TrarecordWeb, :router
...
scope "/", TrarecordWeb do
pipe_through [:browser, :require_authenticated_user]
get "/users/settings", UserSettingsController, :edit
put "/users/settings", UserSettingsController, :update
get "/users/settings/confirm_email/:token", UserSettingsController, :confirm_email
live "/settings", SettingLive.Index, :index
live "/home", HomeLive
# 以下追加
live "/items", ItemLive.Index, :index
live "/items/new", ItemLive.Index, :new
live "/items/:id/edit", ItemLive.Index, :edit
live "/items/:id", ItemLive.Show, :show
live "/items/:id/show/edit", ItemLive.Show, :edit
end
...
end
confirm modalの実装
ネイティブアプリだとalertやconfirmダイアログがでないので、モーダルで表示するようにします
defmodule TrarecordWeb.ItemLive.ConfirmComponent do
use TrarecordWeb, :live_component
alias Trarecord.Items
alias Phoenix.LiveView.JS
@impl true
def update(%{item: _item} = assigns, socket) do
{:ok, assign(socket, assigns)}
end
@impl true
def handle_event("delete", %{"id" => id}, socket) do
item = Items.get_item!(id)
{:ok, _} = Items.delete_item(item)
socket
|> put_flash(:info, "Item delete successfully")
|> push_redirect(to: socket.assigns.return_to)
|> then(&{:noreply, &1})
end
defp list_items do
Items.list_items()
end
end
cancelボタンは live_hlperに実装されているモーダルの閉じるボタンへのクリックイベントを発火させています
<a id="close" href="#" class="phx-modal-close" phx-click={hide_modal()}>✖</a>
defp hide_modal(js \\ %JS{}) do
js
|> JS.hide(to: "#modal", transition: "fade-out")
|> JS.hide(to: "#modal-content", transition: "fade-out-scale")
end
<div class="card w-80">
<div class="card-body items-center text-center">
<h2 class="card-title mb-4">Are you sure?</h2>
<div class="card-actions justify-end">
<button class="btn btn-ghost" phx-click={JS.dispatch("click", to: "#close")}>Cancel</button>
<button class="btn btn-error"
phx-click="delete"
phx-value-id={@item.id}
phx-target={@myself}
>Delete</button>
</div>
</div>
</div>
indexとshowからのルーティングを追加します
defmodule TrarecordWeb.Router do
use TrarecordWeb, :router
...
scope "/", TrarecordWeb do
pipe_through([:browser, :require_authenticated_user])
get("/users/settings", UserSettingsController, :edit)
put("/users/settings", UserSettingsController, :update)
get("/users/settings/confirm_email/:token", UserSettingsController, :confirm_email)
live("/settings", SettingLive.Index, :index)
live("/home", HomeLive)
live("/items", ItemLive.Index, :index)
live("/items/new", ItemLive.Index, :new)
live("/items/:id/edit", ItemLive.Index, :edit)
live("/items/:id/delete", ItemLive.Index, :delete) # 追加
live("/items/:id", ItemLive.Show, :show)
live("/items/:id/show/edit", ItemLive.Show, :edit)
live("/items/:id/show/delete", ItemLive.Show, :delete) # 追加
end
フォームモーダルのスタイリング
inputとlabelにクラスを追加します
<div>
<h2 class="sm:text-3xl text-2xl font-medium title-font mb-4 text-gray-900"><%= @title %></h2>
<.form
let={f}
for={@changeset}
id="item-form"
phx-target={@myself}
phx-change="validate"
phx-submit="save">
<%= label f, :name, class: "label" %>
<%= text_input f, :name, class: "input input-bordered w-full" %>
<%= error_tag f, :name %>
<%= label f, :url, class: "label" %>
<%= text_input f, :url, class: "input input-bordered w-full" %>
<%= error_tag f, :url %>
<%= label f, :image, class: "label" %>
<%= text_input f, :image, class: "input input-bordered w-full" %>
<%= error_tag f, :image %>
<%= label f, :category, class: "label" %>
<%= text_input f, :category, class: "input input-bordered w-full" %>
<%= error_tag f, :category %>
<%= label f, "Have It", class: "label" %>
<%= checkbox f, :status,class: "checkbox" %>
<%= error_tag f, :status %>
<div>
<%= submit "Save", phx_disable_with: "Saving...", class: "btn btn-primary mt-4" %>
</div>
</.form>
</div>
モーダルのスタイリング
といっても角を丸くするだけです
defmodule TrarecordWeb.LiveHelpers do
import Phoenix.LiveView
import Phoenix.LiveView.Helpers
alias Phoenix.LiveView.JS
def modal(assigns) do
assigns = assign_new(assigns, :return_to, fn -> nil end)
~H"""
<div id="modal" class="phx-modal fade-in" phx-remove={hide_modal()}>
<div
id="modal-content"
class="phx-modal-content fade-in-scale rounded-lg" # rouded-lgを追加
phx-click-away={JS.dispatch("click", to: "#close")}
phx-window-keydown={JS.dispatch("click", to: "#close")}
phx-key="escape"
>
...
</div>
</div>
"""
end
...
end
indexのスタイリング
コンポーネントの追加とモーダルに切り出したdelete部分を改修します
defmodule TrarecordWeb.ItemLive.Index do
use TrarecordWeb, :live_view
alias Trarecord.Items
alias Trarecord.Items.Item
alias TrarecordWeb.Components.Navigation # 追加
...
+ defp apply_action(socket, :delete, %{"id" => id}) do
+ socket
+ |> assign(:page_title, "Delete Item")
+ |> assign(:item, Items.get_item!(id))
+ end
defp apply_action(socket, :index, _params) do
socket
|> assign(:page_title, "Listing Items")
|> assign(:item, nil)
end
- @impl true
- def handle_event("delete", %{"id" => id}, socket) do
- item = Items.get_item!(id)
- {:ok, _} = Items.delete_item(item)
-
- {:noreply, assign(socket, :items, list_items())}
- end
end
Headerはbackは無いのでfalse, actionは新規追加モーダル表示用のリンクを追加します
デザインはECOMMERCEの1つめをベースにします
実際の表示部分はCardコンポーネントを使っています
<Navigation.header
title="Items"
back={false}
action={live_patch "Add Item", to: Routes.item_index_path(@socket, :new), class: "btn btn-ghost normal-case"}
/>
<section class="text-gray-600 body-font relative">
<div class="container px-5 py-24 mx-auto">
<div class="lg:w-1/2 md:w-2/3 flex flex-col mb-4 mx-auto">
<h1 class="sm:text-3xl text-2xl font-medium title-font text-gray-900">Listing Items</h1>
</div>
<section>
<div class="container px-5 py-4 mx-auto">
<div class="flex flex-wrap -m-4">
<%= for item <- @items do %>
<div id={"item-#{item.id}"} class="card card-compact bg-base-100 shadow-2xl lg:w-1/4 md:w-1/2 p-4 w-full mb-4">
<figure><img src={item.image} alt="" /></figure>
<div class="card-body">
<h2 class="card-title">
<%= item.name %>
<%= if item.status do %>
<div class="badge badge-secondary">Have It</div>
<% else %>
<div class="badge badge-secondary">Wants It</div>
<% end %>
<div class="badge"><%= item.category %></div>
</h2>
<a class="link" href={item.url}><%= item.url %></a>
</div>
<div class="card-actions justify-end">
<span><%= live_redirect "Show", to: Routes.item_show_path(@socket, :show, item), class: "btn normal-case" %></span>
<span><%= live_patch "Edit", to: Routes.item_index_path(@socket, :edit, item), class: "btn normal-case" %></span>
<!--- deleteを live_patchにしてモーダルを表示するように変更 --->
<span><%= live_patch "Delete", to: Routes.item_index_path(@socket, :delete, item), class: "btn btn-error normal-case" %></span>
</div>
</div>
<% end %>
</div>
</div>
</section>
<%= if @live_action in [:new, :edit] do %>
<.modal return_to={Routes.item_index_path(@socket, :index)}>
<.live_component
module={TrarecordWeb.ItemLive.FormComponent}
id={@item.id || :new}
title={@page_title}
action={@live_action}
item={@item}
return_to={Routes.item_index_path(@socket, :index)}
/>
</.modal>
<% end %>
<!--- 以下追加 --->
<%= if @live_action in [:delete] do %>
<.modal return_to={Routes.item_index_path(@socket, :index)}>
<.live_component
module={TrarecordWeb.ItemLive.ConfirmComponent}
id={@item.id}
title={@page_title}
action={@live_action}
item={@item}
return_to={Routes.item_index_path(@socket, :index)}
/>
</.modal>
<% end %>
</div>
</section>
<Navigation.bottom_tab title="Items" />
Showのスタイリング
コンポーネントの追加と deleteモーダルのpage_titleを追加します
efmodule TrarecordWeb.ItemLive.Show do
use TrarecordWeb, :live_view
alias Trarecord.Items
alias TrarecordWeb.Components.Navigation # 追加
...
defp page_title(:show), do: "Show Item"
defp page_title(:edit), do: "Edit Item"
defp page_title(:delete), do: "Delete Item" # 追加
end
headerのbackにindex, actionにedit ページ下部に削除ボタンをつけました
削除モーダルですが
.modal の return_toはモーダルを閉じた時に戻る箇所
.live_componentのreturn_toは削除後に戻る箇所になります
閉じたらshow、削除したらindexに戻るようにします
<Navigation.header
title="Items"
back={Routes.item_index_path(@socket, :index)}
action={live_patch "Edit", to: Routes.item_show_path(@socket, :edit, @item), class: "btn btn-ghost normal-case text-xl"}
/>
<section class="text-gray-600 body-font relative">
<div class="container px-5 py-24 mx-auto">
<div class="lg:w-1/2 md:w-2/3 flex flex-col mb-4 mx-auto">
<h1 class="sm:text-3xl text-2xl font-medium title-font text-gray-900">Show Item</h1>
</div>
<%= if @live_action in [:edit] do %>
<.modal return_to={Routes.item_show_path(@socket, :show, @item)}>
<.live_component
module={TrarecordWeb.ItemLive.FormComponent}
id={@item.id}
title={@page_title}
action={@live_action}
item={@item}
return_to={Routes.item_show_path(@socket, :show, @item)}
/>
</.modal>
<% end %>
<%= if @live_action in [:delete] do %>
<.modal return_to={Routes.item_show_path(@socket, :show, @item)}>
<.live_component
module={TrarecordWeb.ItemLive.ConfirmComponent}
id={@item.id}
title={@page_title}
action={@live_action}
item={@item}
return_to={Routes.item_index_path(@socket, :index)}
/>
</.modal>
<% end %>
<ul>
<li>
<strong>Name:</strong>
<%= @item.name %>
</li>
<li>
<strong>Url:</strong>
<a href={@item.url}><%= @item.url %></a>
</li>
<li>
<strong>Image:</strong>
<img class="object-contain w-1/2" src={@item.image} />
</li>
<li>
<strong>Category:</strong>
<%= @item.category %>
</li>
<li>
<strong>Have It:</strong>
<%= @item.status %>
</li>
</ul>
<span><%= live_patch "Delete", to: Routes.item_show_path(@socket, :delete, @item), class: "btn btn-block btn-error normal-case" %></span>
</div>
</section>
<Navigation.bottom_tab title="Items" />
画面は以上になります
DEMO
最後に
スタイリングしたため膨大な記事になりましたが
グローバルナビゲーションとconfirmモーダルをつければ割と簡単にスマホアプリが作れれそうです!
今は NIFを使うライブラリが使えないですが、iOSでEvision(openCV wrapper)が動いたらしいので、
NIFを使うライブラリもそのうち使えるようなるかもしれません本時期は以上になりますありがとうございました
コード
参考サイト
https://qiita.com/the_haigo/items/27bf18acd28971af31e9
https://tailwindcss.com/docs/guides/phoenix
https://daisyui.com/docs/install/
https://daisyui.com/components/hero/#hero-with-overlay-image
https://daisyui.com/components/hero/#centered-hero
https://daisyui.com/components/card/#card-with-badge
https://daisyui.com/components/toast/#toast-with-alert-inside
https://github.com/riverrun/pbkdf2_elixir
https://tailblocks.cc/
https://github.com/mertJF/tailblocks/blob/master/src/blocks/cta/light/b.js
https://github.com/mertJF/tailblocks/blob/master/src/blocks/contact/light/a.js
https://github.com/mertJF/tailblocks/blob/master/src/blocks/ecommerce/light/a.js
https://zenn.dev/k_neko3/articles/1aee5b86d07f4f