上記サイトでは LiveView のバージョンは0.10.0でしたが、記事公開時にセットアップがうまく行かなかったので0.8.0を用いています。
Phoenixプロジェクトに、TailwindCSSを適用した例を紹介します。
この記事では以下ついて記載しています。
TailwindCSS はCSSフレームワークのひとつです。
以下に公式ページがあります。
このフレームワークの特徴は、以下のようなところです。
例えば以下のように指定した要素があったとすると、
<div class="foo">Some element</div>
.foo {
background-color: #edf2f7;
color: #2d3748;
margin: 0.5rem;
padding: 0.5rem;
}
.foo:hover {
background-color: #a0aec0;
}
TailwindCSS が定義したクラスを使用して、以下のようにスタイリングします。
<div class="bg-gray-200 hover:bg-gray500 text-gray-800 m-2 p-2">Some element</div>
スタイルのバリエーションは TailwindCSS で良さげな感じに規定されているので、欲しいスタイルをクラスに追加していけば良さげな感じになります。
Phoenix プロジェクトに適用できないかと思い、TailwindCSS に入門してみました。
このようなものを作ります。
これは、TailwindCSSの公式ページにある以下のスクリーンキャストを見て実装しました。
BUILDING A RESPONSIVE NAVBAR
BUILDING A DROPDOWN MENU
これらのスクリーンキャストでは、Vueを使用して実装しています。
この Vue
で作られている部分を今回 Phoenix.LiveView
で実装し直しました。
プロジェクト内で使用されている画像については、こちらで用意しました。
以下の要素が含まれています。
もともとのソースコード自体は、以下にあります。
今回作成したソースコードは以下にあります。
記事公開時点でのそれぞれのバージョンは以下です。
項目 | バージョン |
---|---|
erlang | 22.3 |
elixir | 1.10.2-otp-22 |
Phoenix | 1.4.16 |
Phoenix.LliveView | 0.8.0 |
※ 諸事情により、言語系のパッケージマネージャーは Homebrew から asdf に変更しました。
以下の手順で、構築していきます。
例によってプロジェクトの作成から行います。
DBを使用しないので、 --no-ecto
オプションをつけます。
$ mix phx.new tlwind_ddm --no-ecto
Phoenix.LiveViewを組み込みます。
公式のページに従って組み込んでいきます。
Phoenix 1.4.16 を使用している場合は以下の手順が不要になりました。
config.exs
で signing_salt
を指定するところendpoint.ex
で @session_options
を作るところapp.html.eex
で <%= csrf_meta_tag() %>
を追加するところ以下のページを参考に組み込みました。
Adding Tailwind CSS to Phoenix 1.4
手順は以下のようになります。
カスタマイズ等の設定は、今回は必要ないので行わないことにします。
tailwindcss と postcss-loader をインストールします。
以下のコマンドは、プロジェクトのルートで行っています。
# @Project root
$ npm i -D tailwindcss postcss-loader --prefix assets
以下のように postcss.config.js を作成します。
├── assets
│ ├── css
│ │ └── app.css
│ ├── js
│ │ └── app.js
│ ├── package.json
│ ├── postcss.config.js│ └── webpack.config.js
上記ファイルでの記述は以下のようにします。
// assets/postcss.config.js
module.exports = {
plugins: [
require('tailwindcss'),
require('autoprefixer')
]
};
css ファイルに対して、最初に postcss-loader を通すように設定します。
Webpackでは逆順に読まれるので、以下のように最後に記述します。
// assts/webpack.config.js
module: {
rules: [
:
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader'] }
]
},
もともとの記述は必要ないので全て削除し、新たに以下の記述を追加します。
/* assets/css/app.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
Vue で記述されたコードを Phoenix に変換していきます。
Vue の文法等には特に立ち入らず、Phoenix.LiveView で実現できるところに焦点をあてて作業を行っていきます。
各変換方法は、初出のところにポイントを絞って説明します。
コンポーネントとしてのエントリポイントである App.vue からPhoenix化していきます。
コンポーネントのレンダリングに対してそれぞれの違いを認識しながら変換します。
以下の Vue ファイルでは Navbar というコンポーネントをレンダリングしています。
<!-- src/App.vue -->
<template>
<div id="app" class="antialiased text-gray-900">
<div class="bg-gray-200 min-h-screen">
<Navbar/> </div>
</div>
</template>
<script>
import Navbar from './components/Navbar'export default {
name: 'app',
components: { Navbar },}
</script>
<style src="./assets/tailwind.css"></style>
Phoenix の場合は、以下のハイライト箇所のように live_render
でレンダリングします。
# lib/tlwnd_ddm_web/templates/layout/app.html.eex
<div id="app" class="antialiased text-gray-900">
<div class="bg-gray-300" >
<%= live_render @conn, TlwndDdmWeb.NavBar %> </div>
<main role="main" class="container">
<%= render @view_module, @view_template, assigns %>
</main>
</div>
次に、先程レンダリングした NavBar コンポーネントです。
以下のように isOpen
が初期化されています。
<!-- src/components/NavBar.vue -->
<script>
import AccountDropdown from './AccountDropdown'
export default {
:
data() {
return {
isOpen: false, }
},
:
</script>
これは Phoenix.LiveView では、 mount/3
で行います。
# lib/tlwnd_ddm_web/live/nav_bar.ex
def mount(_assigns, _session, socket) do
{:ok, assign(socket, isOpen: false)}
end
以下で @click
イベントハンドラにより、ボタンが押されるたびにトグル動作が行われるようになっています。
<!-- src/components/NavBar.vue -->
<button @click="isOpen = !isOpen" type="button" class="block text-gray-500 hover:text-white focus:text-white focus:outline-none">
LiveView では、phx-click
を handle_event/3
で受けて行います。
# lib/tlwnd_ddm_web/templates/nav_bar/index.html.leex
<button type="button" phx-click="toggle_menu" class="block text-gray-500 hover:text-white focus:text-white focus:outline-none">
# lib/tlwnd_ddm_web/live/nav_bar.ex
def handle_event("toggle_menu", _, socket) do
{:noreply, assign(socket, isOpen: !socket.assigns.isOpen)}
end
以下のように isOpen
に対応したクラスが指定されています。
<nav :class="isOpen ? 'block' : 'hidden'" class="sm:block">
同じような記述で実現できます。
<div class="<%= if @isOpen do "block" else "hidden" end %> sm:block">
isOpen
の状態によって、表示させる要素を変えている記述があります。
以下では、isOpen
が true
のときに表示される要素と、false
のときに表示される要素を並列に記述しています。
<!-- src/components/NavBar.vue -->
<svg class="h-6 w-6 fill-current" viewBox="0 0 24 24">
<path v-if="isOpen" fill-rule="evenodd" d="M18.278 16.864a1 1 0 0 1-1.414 1.414l-4.829-4.828-4.828 4.828a1 1 0 0 1-1.414-1.414l4.828-4.829-4.828-4.828a1 1 0 0 1 1.414-1.414l4.829 4.828 4.828-4.828a1 1 0 1 1 1.414 1.414l-4.828 4.829 4.828 4.828z"/>
<path v-if="!isOpen" fill-rule="evenodd" d="M4 5h16a1 1 0 0 1 0 2H4a1 1 0 1 1 0-2zm0 6h16a1 1 0 0 1 0 2H4a1 1 0 0 1 0-2zm0 6h16a1 1 0 0 1 0 2H4a1 1 0 0 1 0-2z"/>
</svg>
Phoenix では条件分岐で記述しています。
View 側にパターンマッチに対応した関数を設けることで条件分岐をせずに記述できますが、コードの見た目の互換性を考え今回はこのようにしました。
# lib/tlwnd_ddm_web/templates/nav_bar/index.html.leex
<svg class="h-6 w-6 fill-current" viewBox="0 0 24 24">
<%= if @isOpen do %>
<path fill-rule="evenodd" d="M18.278 16.864a1 1 0 0 1-1.414 1.414l-4.829-4.828-4.828 4.828a1 1 0 0 1-1.414-1.414l4.828-4.829-4.828-4.828a1 1 0 0 1 1.414-1.414l4.829 4.828 4.828-4.828a1 1 0 1 1 1.414 1.414l-4.828 4.829 4.828 4.828z"/>
<%= else %>
<path fill-rule="evenodd" d="M4 5h16a1 1 0 0 1 0 2H4a1 1 0 1 1 0-2zm0 6h16a1 1 0 0 1 0 2H4a1 1 0 0 1 0-2zm0 6h16a1 1 0 0 1 0 2H4a1 1 0 0 1 0-2z"/>
<% end %>
</svg>
NavBar コンポーネントから AccountDropdown コンポーネントをレンダリングしています。
コンポーネントには複数のクラスが指定されています。
<AccountDropdown class="hidden sm:block sm:ml-6"/>
Phoenix 側では、live_render
で直接クラスが指定できないので、div
で囲むことによって実現しています。
<div class="hidden sm:block sm:ml-6">
<%= live_render @socket, TlwndDdmWeb.AccountDropdown, id: "account" %>
</div>
先程レンダリングしたAccountDropdownです。
create()
のタイミングで keydown
用のイベントハンドラを登録しています。
イベントハンドラではエスケープキーの場合に isOpen
を false
に設定しています。
<script>
export default {
:
created() {
const handleEscape = (e) => {
if (e.key === 'Esc' || e.key === 'Escape') {
this.isOpen = false
}
}
document.addEventListener('keydown', handleEscape)
this.$once('hook:beforeDestroy', () => {
document.removeEventListener('keydown', handleEscape)
})
}
}
</script>
phx-window-keydown
というWindowレベルのキーバインディングを行い、
<div class="relative" phx-window-keydown="keydown">
イベントをハンドリングした後、パターンマッチによりエスケープキーが押された場合の関数で処理を行っています。
def handle_event("keydown", arg = %{"code" => "Escape"}, socket) do
{:noreply, assign(socket, isOpen: false)}
end
def handle_event("keydown", _, socket) do
{:noreply, socket}
end
変換作業はこれで終了です。
Tailwind CSS は、思った以上に自分にとってとっつきやすいCSSフレームワークでした。
Phoenix.LiveView でも効果的に使えそうです。
Bootstrap 等のように、Javascript を使用しているような UI について Phoenix.LiveView でどのように実現するかは頭の使い所ですね。
今回作成したソースコードは以下に置きました。