はじめに
この記事はfukuoka.ex Elixir/Phoenix Advent Calendar 2021の13日目の記事です。
12日目は @the_haigo LiveViewとNxで画像処理の記事でした
LiveViewとJS Hookを使用してGoogleMapでいろいろします
環境
macOS Monterey m1
phoenix 1.6.4
erlang 24.1.7
elixir 1.13.0
setup
DBは使わないので --no-ectoでプロジェクト作成
mix phx.new live_map --no-ecto
空のLiveViewページを作成
hello world的に作ります
LiveViewは最低mount関数で{:ok, socket}
を返して
render関数か同名のhtml.heexを作成してください
defmodule LiveMapWeb.PageLive do
use LiveMapWeb, :live_view
def mount(_params, _session, socket) do
{
:ok,
socket
}
end
end
<div>
LiveMap
</div>
ルーティングを追加します
LiveViewの場合は
live エンドポイント名, LiveViewモジュール名で指定します
lib/live_map_web/router.ex
defmodule LiveMapWeb.Router do
...
scope "/", LiveMapWeb do
pipe_through :browser
live "/", PageLive
end
...
end
npm ライブラリの追加
phoenix 1.6になってからwebpackからesbuildに移行してnpm非依存になりましたが
npm installすれば普通に使用できます
assetsに移動してGoogleMap loaderを追加します
cd assets
npm install @googlemaps/js-api-loader
#JS HookでGoogleMapの表示
LiveViewにはJS Hookというものがあって、
backendとfrontendでデータをやり取りする場合は APIを通すか、それ用のライブラリをインストールする必要があり、面倒くさいものですが
- JS側のhandleEvent, pushEvent
- Elixir側のpushEvent
という関数を使って相互に JSONとMapのやり取りができます
mount時にGoogleMapを表示するようにします
試す場合は GoogleMapAPIのキーを取得してください
import { Loader } from "@googlemaps/js-api-loader";
let Hooks = {}
const apiKey = "your api key"
Hooks.Map = {
mounted() {
const loader = new Loader({
apiKey: apiKey,
version: "weekly",
})
loader.load().then(() => {
const map = new google.maps.Map(
document.getElementById("map"),
{
center: { lat: 33.30639, lng: 130.41806 },
zoom: 9
}
);
window.map = map;
})
}
}
export default Hooks;
mount()関数が LiveView のmount後に実行されるので、
初期データの作成他に待ち受けるイベントを記述したりします
hooksファイルができたら,liveSocketのoptionsの箇所に hooks: Hooks
で追加します
...
import Hooks from "./hooks"
let liveSocket = new LiveSocket("/live", Socket, {hooks: Hooks, params: {_csrf_token: csrfToken}})
...
Hooksを読み込む際の注意点ですが
Hooksを読み込むタグにIDをつけて読み込むモジュールをphx-hook="Map"
で指定します
また、canvasなどタグ内が頻繁に変化する箇所はLiveViewが変更を検知して再レンダリングされるので、
LiveViewが変更しないようにphx-update="ignore"
を指定します
<div class="row">
<div class="column column-20">
<h1>Show Map</h1>
</div>
<div class="column column-80">
<div id="googleMap" phx-update="ignore" phx-hook="Map">
<div id="map"></div>
</div>
</div>
</div>
max widthを増やして、
google mapの表示領域の高さを600pxに指定します。
heightを指定しないとmapが表示されないので忘れないようにしましょう
.container{
margin: 0 auto;
max-width: 120.0rem; /* 80 -> 120 */
padding: 0 2.0rem;
position: relative;
width: 100%
}
#map{
height: 600px
}
GoogleMapが表示されました!
PlaceSearch
Places APIを使って施設や店舗を検索してその結果を表示します
検索フォーム
phx-submitでsubmitボタンを押した時に search eventを発火させます
<div class="row">
<div class="column column-20">
<!--- 以下追加 --->
<form phx-submit="search">
<input type="text" name="keyword" value={@text}>
<button type="submit">Search</button>
</form>
</div>
...
</div>
paramsはname属性がそのまま使用されるので keywordで取得できます
検索ワードをassignで保持させて
push_eventでJS Hookのsearchイベントを {keyword: keyword}なJSONを送信しています
defmodule LiveMapWeb.PageLive do
use LiveMapWeb, :live_view
def mount(_params, _session, socket) do
{
:ok,
socket
|> assign(:text, "")
}
end
def handle_event("search", %{"keyword" => keyword}, socket) do
{
:noreply,
socket
|> assign(:text, keyword)
|> push_event("search", %{keyword: keyword})
}
end
end
JS Hook Search Event
Google Places API を叩いて結果を必要なものだけにして、
pushEventでElixir側の search_result
イベントをJSONをMapにして発火させています
import { Loader } from "@googlemaps/js-api-loader";
let Hooks = {}
const apiKey = "your api key"
Hooks.Map = {
mounted() {
const loader = new Loader({
apiKey: apiKey,
version: "weekly",
libraries: ["places"] // places apiを追加
})
loader.load().then(() => {
const map = new google.maps.Map(
document.getElementById("map"),
{
center: { lat: 33.30639, lng: 130.41806 },
zoom: 9
}
);
// serviceを作成してwindow追加
const service = new google.maps.places.PlacesService(map);
window.map = map;
window.service = service
})
this.handleEvent("search", ({keyword}) => {
const that = this
const request = { query: keyword };
window.service.textSearch(request, function(results, status) {
if (status === google.maps.places.PlacesServiceStatus.OK) {
let result = results.map((r) => {
return {
name: r.name,
address: r.formatted_address.split(/\s/).pop(),
location: r.geometry.location,
icon: r.icon,
place_id: r.place_id
};
})
that.pushEvent("search_result" , {result: result});
}
});
})
}
}
export default Hooks;
defmodule LiveMapWeb.PageLive do
use LiveMapWeb, :live_view
def mount(_params, _session, socket) do
{
:ok,
socket
|> assign(:text, "")
|> assign(:search_result, [])
}
end
...
def handle_event("search_result", %{"result" => result}, socket) do
{
:noreply,
socket
|> assign(:search_result, result)
}
end
end
assignをしてstateの変更を検知して検索結果をリスト表示します
heexの変数展開はパラメーター(src)は{}
それ以外は<%=%>
で表記します
<div class="row">
<div class="column column-20">
...
<%= for spot <- @search_result do %>
<div class="row">
<img src={spot["icon"]} width="16px" height="16px" />
<span><%= spot["name"] %></span>
</div>
<div class="row">
<%= spot["address"] %>
</div>
<br />
<% end%>
</div>
...
</div>
Markerの追加
phx-click
にJS.pushを発火するようにします
前はphx-value
で値をセットしたりしていましたが、
JS.push
を使うことでvalueとかをまとめて指定してhandle_event
を発火させます
JS hooksのイベント発火をやってくれそうな字面ですがElixirのhandle_event
が実行されます
<div class="row">
<div class="column column-20">
<%= for spot <- @search_result do %>
<div class="row">
<img src={spot["icon"]} width="16px" height="16px" />
<span><%= spot["name"] %></span>
</div>
<div class="row">
<%= spot["address"] %>
</div>
<!--- 以下追加 --->
<div class="row">
<button
class="column"
phx-click={JS.push("add_marker", value: %{location: spot["location"]})}
>
add marker
</button>
</div>
<!--- ここまで --->
<br />
<% end%>
</div>
</div>
JS.pushを使う場合はalias Phoenix.LiveView.JS
で短縮しておくといいでしょう
add_markerはそのままJS Hookを発火させます
defmodule LiveMapWeb.PageLive do
use LiveMapWeb, :live_view
alias Phoenix.LiveView.JS
def handle_event("add_marker", location, socket) do
{
:noreply,
socket
|> push_event("add_marker", location)
}
end
end
マーカー作ってmapに追加して、センターを変更します
import { Loader } from "@googlemaps/js-api-loader";
let Hooks = {}
const apiKey = "your api key"
Hooks.Map = {
mounted() {
...
this.handleEvent("add_marker", ({location}) => {
let marker = new google.maps.Marker({position: location})
marker.setMap(window.map)
window.map.setCenter(location)
})
}
}
export default Hooks;
コードができたので動作確認をしましょう!
最後に
JS Hookを通して、Map API, Place APIを使って簡単にデータの取得してElixirとのやり取りができるのがわかったのではないでしょうか?
Canvasも問題なく使えるのでWebGL2やThreeJSでいろいろできてと楽しそうです
本記事は以上になります
次は @tomoaki-kimura さんの Railsエンジニアの比較用Phoenixコマンド集 になります
コード