11
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

はじめに

この記事は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を作成してください

lib/live_map_web/live/page_live.ex
defmodule LiveMapWeb.PageLive do
  use LiveMapWeb, :live_view

  def mount(_params, _session, socket) do
    {
      :ok,
      socket
    }
  end
end
lib/live_map_web/live/page_live.html.heex
<div>
  LiveMap
</div>

ルーティングを追加します
LiveViewの場合は
live エンドポイント名, LiveViewモジュール名で指定します

lib/live_map_web/router.ex
lib/live_map_web/router.ex
defmodule LiveMapWeb.Router do
...
  scope "/", LiveMapWeb do
    pipe_through :browser

    live "/", PageLive
  end
...
end

これでLiveViewページが表示されました
スクリーンショット 2021-12-14 15.23.55.png

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のキーを取得してください

assets/js/hooks.js
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 で追加します

assets/js/app.js
...
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"を指定します

lib/live_map_web/live/page_live.html.heex
<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が表示されないので忘れないようにしましょう

assets/css/phoenix.css
.container{
  margin: 0 auto;
  max-width: 120.0rem; /* 80 -> 120 */
  padding: 0 2.0rem;
  position: relative;
  width: 100%
}
#map{
  height: 600px
}

GoogleMapが表示されました!

スクリーンショット 2021-12-14 16.07.35.png

PlaceSearch

Places APIを使って施設や店舗を検索してその結果を表示します

検索フォーム

phx-submitでsubmitボタンを押した時に search eventを発火させます

lib/live_map_web/live/page_live.html.heex
<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を送信しています

lib/live_map_web/live/page_live.ex
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にして発火させています

assets/js/hooks.js
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;
lib/live_map_web/live/page_live.ex
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)は{}それ以外は<%=%>で表記します

lib/live_map_web/live/page_live.html.heex
<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>

Image from Gyazo

Markerの追加

phx-clickにJS.pushを発火するようにします
前はphx-valueで値をセットしたりしていましたが、
JS.pushを使うことでvalueとかをまとめて指定してhandle_eventを発火させます
JS hooksのイベント発火をやってくれそうな字面ですがElixirのhandle_eventが実行されます

lib/live_map_web/live/page_live.html.heex
<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を発火させます

lib/live_map_web/live/page_live.ex
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に追加して、センターを変更します

assets/js/hooks.js
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;

コードができたので動作確認をしましょう!

Image from Gyazo

最後に

JS Hookを通して、Map API, Place APIを使って簡単にデータの取得してElixirとのやり取りができるのがわかったのではないでしょうか?

Canvasも問題なく使えるのでWebGL2やThreeJSでいろいろできてと楽しそうです
本記事は以上になります

次は @tomoaki-kimura さんの Railsエンジニアの比較用Phoenixコマンド集 になります

コード

11
3
2

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?