RailsエンジニアはPhoenixでもStimulusを使いたい

## はじめに

この記事は、Elixir Advent Calendar 2023 14日目となります。

https://qiita.com/advent-calendar/2023/elixir

前日13日目は、

@mnishiguchi さんで Elixir Erlang match_spec の文法 でした。

https://qiita.com/mnishiguchi/items/b597f70a186220bf6ec7

## 作るもの

今年は、Phoenix触る時間が全然なくて、ネタに困ったので、ズバリ・・・

#### PhoenixでもStimulusを使いたい

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/96913/ae8aa7fc-4aed-fffe-7206-d71544483d24.gif" width="200">

です。

いや、LiveView使えよというツッコミはご容赦いただきたく・・・

今年はRails７漬けになってて全然Phoenix使ってなかったのですが、いつの間にやらPhoenixが超絶洗練されておりまして・・・。

いや、ほんと私程度が言うのもなんですが、ディレクトリ構造が直感的になっているというか、以前の何処に何があんねんというのが皆無。

##### 一言で言うとめちゃめちゃ使いやすくなってる！！！！

これは普段Rails使っている人でも違和感なくない？
ということで、せっかくなので、今年もまたRailsエンジニア目線のPhoenixやります。

なので、

#### Phoenix で Stimulus を使って GoogleMapApi を動かしてみようではないか え・・LiveView？　知らない子ですね。

<img src="https://i.gyazo.com/a4215c69777316f688b4bc41c38828cf.gif" width="300">

と、こんな感じの入力補助からピン表示までの操作をやってみたいと思います。

## 環境

```
elixir
 1.15.7-otp-26
erlang
 26.1.2
Phoenix
 v1.7.10
```

## プロジェクト作成

```zsh:zsh
% mkdir google_map_phx
% cd google_map_phx
% mix phx.new . --no-mailer --no-live
```

ログが流れます。

```zsh:log
（略）
* creating assets/vendor/heroicons/LICENSE.md
* creating assets/vendor/heroicons/UPGRADE.md
* extracting assets/vendor/heroicons/optimized

Fetch and install dependencies? [Yn] Y
* running mix deps.get
* running mix assets.setup
* running mix deps.compile

We are almost there! The following steps are missing:

    $ cd .

Then configure your database in config/dev.exs and run:

    $ mix ecto.create

Start your Phoenix app with:

    $ mix phx.server

You can also run your app inside IEx (Interactive Elixir) as:

    $ iex -S mix phx.server
```

言われた通り実行します。

```zsh:zsh
% mix ecto.create
% mix phx.server
```

```zsh:log
Compiling 14 files (.ex)
Generated phx_map app
The database for PhxMap.Repo has already been created
 phx_map % mix phx.server
[info] Running PhxMapWeb.Endpoint with cowboy 2.10.0 at 127.0.0.1:4000 (http)
[info] Access PhxMapWeb.Endpoint at http://localhost:4000
[debug] Downloading esbuild from https://registry.npmjs.org/@esbuild/darwin-arm64/0.17.11

Rebuilding...

Done in 240ms.
[watch] build finished, watching for changes...
[info] GET /
[debug] Processing with PhxMapWeb.PageController.home/2
  Parameters: %{}
  Pipelines: [:browser]
[info] Sent 200 in 59ms
```

良さそうです。

[![Image from Gyazo](https://i.gyazo.com/c09772983554e3a929060032972283b8.png)](https://gyazo.com/c09772983554e3a929060032972283b8)

## モデル作成

### phx.gen.html

htmlでMVC作ります。Railsで言うscaffoldです。

```zsh:zsh
% mix phx.gen.html Maps Point points name:string latitude:float longitude:float address:string
```

```zsh:log
* creating lib/google_map_phx_web/controllers/point_controller.ex
* creating lib/google_map_phx_web/controllers/point_html/edit.html.heex
* creating lib/google_map_phx_web/controllers/point_html/index.html.heex
* creating lib/google_map_phx_web/controllers/point_html/new.html.heex
* creating lib/google_map_phx_web/controllers/point_html/show.html.heex
* creating lib/google_map_phx_web/controllers/point_html/point_form.html.heex
* creating lib/google_map_phx_web/controllers/point_html.ex
* creating test/google_map_phx_web/controllers/point_controller_test.exs
* creating lib/google_map_phx/maps/point.ex
* creating priv/repo/migrations/20231213204606_create_points.exs
* creating lib/google_map_phx/maps.ex
* injecting lib/google_map_phx/maps.ex
* creating test/google_map_phx/maps_test.exs
* injecting test/google_map_phx/maps_test.exs
* creating test/support/fixtures/maps_fixtures.ex
* injecting test/support/fixtures/maps_fixtures.ex

Add the resource to your browser scope in lib/google_map_phx_web/router.ex:

    resources "/points", PointController


Remember to update your repository by running migrations:

    $ mix ecto.migrate

```

また途中で何か言われますが、ルーティングの記述とマイグレーションの実行を言われているようです。

### ルーティング

ですので、まずはルーティングを

```elixir:lib/google_map_phx_web/router.ex(before)
（略）
  scope "/", GoogleMapPhxWeb do
    pipe_through :browser

    get "/", PageController, :home
  end
（略）
```

このように変更しておきます。

```elixir:lib/google_map_phx_web/router.ex(after)
(略)
  scope "/", GoogleMapPhxWeb do
    pipe_through :browser

    get "/", PointController, :index
    resources "/points", PointController
  end
（略）
```

### マイグレーション

次に、マイグレーション

```zsh:zsh
% mix ecto.migrate
```

```zsh:log
05:51:32.894 [info] == Running 20231213204606 GoogleMapPhx.Repo.Migrations.CreatePoints.change/0 forward

05:51:32.896 [info] create table points

05:51:32.902 [info] == Migrated 20231213204606 in 0.0s
```

通りました。

### seeds

マストではないのですが、最初のプレビュー用にダミーデーター入れておきます。

```elixir:priv/repo/seeds.exs
# Script for populating the database. You can run it as:
#
#     mix run priv/repo/seeds.exs
#
# Inside the script, you can read and write to any of your
# repositories directly:
#
#     GoogleMapPhx.Repo.insert!(%GoogleMapPhx.SomeSchema{})
#
# We recommend using the bang functions (`insert!`, `update!`
# and so on) as they will fail if something goes wrong.

alias GoogleMapPhx.Repo
alias GoogleMapPhx.Maps.Point

points = [
  %Point{
    name: "都庁",
    latitude: 35.6895014,
    longitude: 139.6917337,
    address: "日本、〒163-8001 東京都新宿区西新宿２丁目８−１"
  },
  %Point{
    name: "後楽園",
    latitude: 35.7054551,
    longitude: 139.7535553,
    address: "日本、〒112-0004 東京都文京区後楽１丁目３"
  },
  %Point{
    name: "Home Taco Bar",
    latitude: 35.3076198,
    longitude: 139.4937805,
    address: "日本、〒248-0033 神奈川県鎌倉市腰越２丁目１０−２５"
  }
]

for point <- points, do:
  Repo.insert!(point)
```

ファイルに書いてあるコマンドでデーターを流します。

```zsh:zsh
% mix run priv/repo/seeds.exs
```

```zsh:log
[debug] QUERY OK source="points" db=4.9ms decode=2.0ms queue=1.0ms idle=6.5ms
INSERT INTO "points" ("name","address","longitude","latitude","inserted_at","updated_at") VALUES ($1,$2,$3,$4,$5,$6) RETURNING "id" ["都庁", "日本、〒163-8001 東京都新宿区西新宿２丁目８−１", 139.6917337, 35.6895014, ~U[2023-12-13 20:55:14Z], ~U[2023-12-13 20:55:14Z]]
↳ Enum."-map/2-lists^map/1-1-"/2, at: lib/enum.ex:1693
[debug] QUERY OK source="points" db=0.6ms queue=0.8ms idle=24.1ms
INSERT INTO "points" ("name","address","longitude","latitude","inserted_at","updated_at") VALUES ($1,$2,$3,$4,$5,$6) RETURNING "id" ["後楽園", "日本、〒112-0004 東京都文京区後楽１丁目３", 139.7535553, 35.7054551, ~U[2023-12-13 20:55:14Z], ~U[2023-12-13 20:55:14Z]]
↳ Enum."-map/2-lists^map/1-1-"/2, at: lib/enum.ex:1693
[debug] QUERY OK source="points" db=0.5ms queue=0.5ms idle=25.7ms
INSERT INTO "points" ("name","address","longitude","latitude","inserted_at","updated_at") VALUES ($1,$2,$3,$4,$5,$6) RETURNING "id" ["Home Taco Bar", "日本、〒248-0033 神奈川県鎌倉市腰越２丁目１０−２５", 139.4937805, 35.3076198, ~U[2023-12-13 20:55:14Z], ~U[2023-12-13 20:55:14Z]]
↳ Enum."-map/2-lists^map/1-1-"/2, at: lib/enum.ex:1693
```

通りました。確認します。

[![Image from Gyazo](https://i.gyazo.com/c85aa2c6ec39e96a6c3f648d8ba1c65e.png)](https://gyazo.com/c85aa2c6ec39e96a6c3f648d8ba1c65e)

OKのようです。

## JS周り

では、JS周りいきましょう。

Rails７で正式採用されたJSフレームワーク [Stimulus](https://stimulus.hotwired.dev/handbook/origin) を使います。

実は元ネタは、かなり前からあって・・・

ほんとは、LiveViewでやろうかと思ってたら、 @the_haigo さんの記事が

https://qiita.com/the_haigo/items/f2035a086f4753537237

すでにあり・・・

https://medium.com/@loganbbres/adding-stimulus-js-to-an-elixir-phoenix-project-f492a4630f22

この記事がかなり前なので、ちょっと検証してみようかというのが発端です。

### assetsに移動

```zsh:zsh
cd assets
```

### Stimulusのインストール

```zsh:zsh
% npm install --save stimulus
```

```zsh:log
added 3 packages in 3s
```

### GoogleMapApi

ついでにGooGleMapApiのライブラリも入れておきましょう。
NPM js-api-loader パッケージを使います。

https://developers.google.com/maps/documentation/javascript/load-maps-js-api?hl=ja

```zsh:zsh
% npm install @googlemaps/js-api-loader
```

階層を戻します。

```zsh:zsh
cd ..
```

### Stimulusコントローラーの作成

StimulusはJS階層下にコントローラーを作成しますので、そのファイルを作成します。

- 一覧表示
- 個別表示
- フォーム

用に３つ作ってみたいと思います。
これにapplication_controllerを含め、４つ新規作成します。

ディレクトリを作っておきましょう。
```zsh:zsh
% mkdir assets/js/controllers
% mkdir assets/js/controllers/google_maps
```

`controllers/` 直下でも構わないのですが、 `controllers/google_map/` に纏めます。

では、それぞれ作っていきましょう。

### application_controller.js

```javascript:assets/js/controllers/google_map/application_controller.js
import { Controller } from "stimulus"
import { Loader } from "@googlemaps/js-api-loader"

export default class extends Controller {
  setLoader() {
    return new Loader({
      apiKey: "xxxxxxxxYOUR_API_KEYxxxxxxxxx",//GooGleのApiキー
      version: "weekly",
    });
  }
}
```

ここで、先程のライブラリを読んでいます。

```javascript
import { Controller } from "stimulus"
import { Loader } from "@googlemaps/js-api-loader"
```

その他３つはこの `application_controller.js` を継承するようにします。

あと、GoogleのApiキーに関しては取得後、必ず各必要環境に絞って制限を掛けておきましょう。

では、残り３つです。先頭には、

```javascript
import ApplicationController from "./application_controller";
```

と書く事で継承されます。

### index_controller.js

```javascript:assets/js/controllers/google_map/index_controller.js
import ApplicationController from "./application_controller";

let map
let markers = []
let infoWindows = []

// Connects to data-controller="google-map--index"
export default class extends ApplicationController {
  static values = { location: { 
                        lat: 38.041184121,
                        lng: 137.1063077823
                    },
                    zoom: 5,
                    points: [] }

  static targets = [ 'map' ]

  connect() {
    this.setPoints()
    this.newMap()
  }

  newMap() {
    const loader = this.setLoader()
    loader.load().then(async () => {
      const { Map } = await google.maps.importLibrary("maps");

      map = new Map(this.mapTarget, {
        center: this.locationValue,
        zoom: this.zoomValue,
      })
      this.addMarkersToMap()
    })
  }

  addMarkersToMap() {
    this.pointsValue.forEach((o, i) => {
      this.addMarkerToMarkers(o)
      this.addInfoWindowToInfoWindows(o)
      this.addEventToMarker(i)
    })
  }

  addMarkerToMarkers(o) {
    this._marker = new google.maps.Marker({
      position: { lat: o.lat, lng: o.lng },
      map,
      name: o.name
    })
    markers.push(this._marker)
  }

  addInfoWindowToInfoWindows(o) {
    this._infoWindow = new google.maps.InfoWindow({
      content: `
        <a href="/points/${o.id}">
          ${o.name}
        </a>
      `
    })
    infoWindows.push(this._infoWindow)
  }

  addEventToMarker(i) {
    markers[i].addListener('click', () => {
      infoWindows[i].open(map, markers[i]);
    });
  }

  setPoints() {
    this.pointsValue = JSON.parse(this.mapTarget.dataset.json)
    this.getLastPointLocation()
  }

  getLastPointLocation() {
    if (this.pointsValue.length > 0) { 
      this._lastPoint = this.pointsValue.sort((a, b) => { a.id - b.id }).reverse()[0]
      this.locationValue = this._lastPoint
      this.zoomValue = 12
    } 
  }
}
```

### show_controller.js

```javascript:assets/js/controllers/google_map/show_controller.js
import ApplicationController from "./application_controller";

// Connects to data-controller="google-map--show"
export default class extends ApplicationController {

  static targets = [ 'map' ]

  connect() {
    this.newMap()
  }

  newMap() {
    const loader = this.setLoader()
    loader.load().then(async () => {
      const { Map } = await google.maps.importLibrary("maps");
      this._location = JSON.parse(this.mapTarget.dataset.json)
      this._map = new Map(this.mapTarget, {
        center: { lat: this._location.lat, lng: this._location.lng },
        zoom: 15,
      })
      new google.maps.Marker({
        map: this._map,
        position: this._location
      })
    })
  }
}
```

### form_controller.js

```javascript:assets/js/controllers/google_map/form_controller.js
import ApplicationController from "./application_controller";

let map
let marker

// Connects to data-controller="google-map--form"
export default class extends ApplicationController {

  static values = { location: {
                      lat: 35.6895014,
                      lng: 139.6917337
                    },
                    zoom: 15,
                    addressList: [] }

  static targets = [ 'map', 'keyword', `address`,
                     'latitude', 'longitude', 'addressList' ]

  connect() {
    this.newMap()
  }

  newMap() {
    const loader = this.setLoader()
    loader.load().then(async () => {
      const { Map } = await google.maps.importLibrary("maps");
      map = new Map(this.mapTarget, {
        center: this.locationValue,
        zoom: this.zoomValue,
      })
      this.initMarker()
      map.setCenter(this.locationValue)
      this.keywordTarget.addEventListener('change', () => {
        this.changeKeywordAction()
      })
      google.maps.event.addListener(map, 'click', (e) => {
        this.clickMapAction(e)
      });
    })
  }

  changeKeywordAction() {
    this.geoCoding(this.keywordTarget.value)
  }

  clickMapAction(e) {
    this._location = { lat: e.latLng.lat(), lng: e.latLng.lng() }
    this.clearMarker()
    this.setLocation(this._location)
    this.newMarker()
    this.keywordTarget.value = ""
    this.reverseGeoGoding() 
  }

  geoCoding(keyword) {
    this._geocoder = new google.maps.Geocoder()
    this._geocoder.geocode({ address: keyword }, (results, status) => {
      if (status == 'OK') {
        this._result = results[0].geometry.location
        this._location = { lat: this._result.lat(), lng: this._result.lng() }
        this.clearMarker()
        this.setLocation(this._location)
        this.newMarker()
        map.setCenter(this._location)
        this.reverseGeoGoding()
      } else {
        this.clearLocationForm()
      }
    }) 
  }

  reverseGeoGoding() {
    this._geocoder = new google.maps.Geocoder()
    this._geocoder.geocode({ location: this.locationValue }, (results, status) => {
      if (status == 'OK') {
        this.setAddresList(results)
      } else {
        this.clearAddressList()
      }
    })
    this.addressTarget.value = ""
  }

  clearAddressList() {
    this.addressListValue = []
    this.addressListTarget.innerHTML = ""
  }

  setAddresList(result) {
    this.clearAddressList()
    this.setAddressListValue(result)
    this.addressListValue.forEach(address => {
      this._option = document.createElement('option')
      this._option.value = address
      this.addressListTarget.append(this._option)
    })
  }

  setAddressListValue(result) {
    this._addressList = []
    result.forEach(o => {
      this._address = o.formatted_address
      this._addressList.push(this._address)
    });
    this.addressListValue = this._addressList
  }

  clearMarker() {
    if (marker != null) {
      marker.setMap(null)
    }
  }

  newMarker() {
    marker = new google.maps.Marker({
      map: map,
      position: this.locationValue
    })
  }
  
  writeToLocationForm(location) {
    this.latitudeTarget.value = location.lat
    this.longitudeTarget.value = location.lng
  }

  clearLocationForm() {
    this.latitudeTarget.value = ""
    this.longitudeTarget.value = "" 
  }

  initMarker() {
    this._latitude = this.latitudeTarget.value
    this._longitude = this.longitudeTarget.value
    if (this._latitude != "" && this._longitude != "") { 
      this.locationValue = { lat: parseFloat(this._latitude), lng: parseFloat(this._longitude) } 
      this.newMarker(this.locationValue)
    }
  }

  setLocation(location) {
    this.locationValue = location
    this.writeToLocationForm(location)
  }
}
```

では、最後にこれらを機能させる為にStimulusに登録する設定を書いておきます。

### app.js

```javascript:assets/js/app.js
（略）

//＊以下を末尾に追記

// Configure Stimulus development experience
import { Application } from "stimulus"
// import ApplicationController from "./controllers/google_map/application_controller"
const application = Application.start()
export { application }

// controllers
import GoogleMap__ApplicationController from "./controllers/google_map/application_controller"
application.register("google-map--application", GoogleMap__ApplicationController)

import GoogleMap__IndexController from "./controllers/google_map/index_controller"
application.register("google-map--index", GoogleMap__IndexController)

import GoogleMap__ShowController from "./controllers/google_map/show_controller"
application.register("google-map--show", GoogleMap__ShowController)

import GoogleMap__FormController from "./controllers/google_map/form_controller"
application.register("google-map--form", GoogleMap__FormController)
```

これでJS周りはOKです。

## テンプレート

テンプレートですが、元々 `gen.html` で用意されているものに追加していきます。
それにしても、いつのまにかこんなものまで・・・。

https://petal.build/recipes/forms

では、index show form の３点を変更していきます。

変更後のみ記載していますので、そのまま置き換えましょう。

### index.html.heex

ここでは、全ポイントのピンを見る事ができます。

```heex:lib/google_map_phx_web/controllers/point_html/index.html.heex
<.header>
  Listing Points
  <:actions>
    <.link href={~p"/points/new"}>
      <.button>New Point</.button>
    </.link>
  </:actions>
</.header>

<div data-controller="google-map--index" class="mt-4">
  <div data-google-map--index-target="map" data-json={@points_json} style="height:50vh;max-width:800px;"></div>
</div>

<.table id="points" rows={@points} row_click={&JS.navigate(~p"/points/#{&1}")}>
  <:col :let={point} label="Name"><%= point.name %></:col>
  <:col :let={point} label="Latitude"><%= point.latitude %></:col>
  <:col :let={point} label="Longitude"><%= point.longitude %></:col>
  <:col :let={point} label="Address"><%= point.address %></:col>
  <:action :let={point}>
    <div class="sr-only">
      <.link navigate={~p"/points/#{point}"}>Show</.link>
    </div>
    <.link navigate={~p"/points/#{point}/edit"}>Edit</.link>
  </:action>
  <:action :let={point}>
    <.link href={~p"/points/#{point}"} method="delete" data-confirm="Are you sure?">
      Delete
    </.link>
  </:action>
</.table>
```

この部分がコントローラーとの接続部分となります。

```html
<div data-controller="google-map--index"></div>
```
`google-map--index` で、
`google_map` ディレクトリの `index.js` に接続です。

`data-google-map--index-target` の部分は、該当のDomを `static targets` として登録できるようにしている部分です。

また、使用するデーターは、 `@points_json` として渡されています。

### show.html.heex

ここでは、個別のピンを見る事ができます。

```heex:lib/google_map_phx_web/controllers/point_html/show.html.heex
<.header>
  Point <%= @point.id %>
  <:subtitle>This is a point record from your database.</:subtitle>
  <:actions>
    <.link href={~p"/points/#{@point}/edit"}>
      <.button>Edit point</.button>
    </.link>
  </:actions>
</.header>

<div data-controller="google-map--show">
  <div data-google-map--show-target="map" data-json={@point_json} style="height:30vh;max-width:400px;"></div>
</div>

<.list>
  <:item title="Name"><%= @point.name %></:item>
  <:item title="Latitude"><%= @point.latitude %></:item>
  <:item title="Longitude"><%= @point.longitude %></:item>
  <:item title="Address"><%= @point.address %></:item>
</.list>

<.back navigate={~p"/points"}>Back to points</.back>
```

使用するデーターは、 `@point_json` として渡されています。

### point_form.html.heex

ここでは、キーワード検索や、マップをクリックする事で、緯度経度の入力を行ったり、住所データーの候補を `datalist` タグに出力します。

```heex:lib/google_map_phx_web/controllers/point_html/point_form.html.heex
<div data-controller="google-map--form">

  <.simple_form :let={f} for={@changeset} action={@action}>
    <.error :if={@changeset.action}>
      Oops, something went wrong! Please check the errors below.
    </.error>
    
    <label> ※ You can search by address or keyword, or by clicking on the map.
      <input type="search" data-google-map--form-target='keyword' class="mt-2 block w-full rounded-lg text-zinc-900 focus:ring-0 sm:text-sm sm:leading-6 phx-no-feedback:border-zinc-300 phx-no-feedback:focus:border-zinc-400 border-zinc-300 focus:border-zinc-400"> 
    </label>

    <.input field={f[:name]} type="text" label="Name" step="any" />

    <.input field={f[:address]} type="text" label="Address(datalist)" list="address_list" data-google-map--form-target='address' />
    <datalist id="address_list" data-google-map--form-target="addressList"></datalist>

    <div data-google-map--form-target="map" style="height:20vh;max-width:300px;"></div>

    <div class="grid grid-cols-2 gap-2"> 
      <div class="col-span-1">
        <.input field={f[:latitude]} type="number" label="Latitude(readonly)" step="any" data-google-map--form-target='latitude' />
      </div>
      <div class="col-span-1">
        <.input field={f[:longitude]} type="number" label="Longitude(readonly)" step="any" data-google-map--form-target='longitude' />
      </div>
    </div>

    <:actions>
      <.button>Save Point</.button>
    </:actions>
  </.simple_form>
</div>
```

これでテンプレートはOKです。

## コントローラー

それにしても、コントローラーとテンプレート、ルーターが同じ階層ってすごく扱いやすいですね。
頻度の高いものがそれぞれ固まっていて素敵です。（laravelとか触るとマジ発狂しそうになる民）

[![Image from Gyazo](https://i.gyazo.com/2068ac83a8622006bbe32bbf5a504672.png)](https://gyazo.com/2068ac83a8622006bbe32bbf5a504672)

では、追記していきましょう。

ここでは、`data-json` に渡すデーターをゴニョゴニョします。

### point_controller.ex

DBをからの情報が必要なのは `index` と `show` です。

それぞれ、 JS側が要求する形にしていきます。

```elixir
  def index(conn, _params) do
    points = Maps.list_points()
    points_json = Jason.encode!( points |> Enum.map( fn o -> %{ id: o.id, name: o.name, lat: o.latitude, lng: o.longitude } end ) )
    render(conn, :index, points: points, points_json: points_json)
  end
```

```elixir
  def show(conn, %{"id" => id}) do
    point = Maps.get_point!(id)
    point_json = %{ id: point.id, name: point.name, lat: point.latitude, lng: point.longitude }
      |> Jason.encode!
    render(conn, :show, point: point, point_json: point_json)
  end
```

全体だとこのような感じになります。

```elixir:lib/google_map_phx_web/controllers/point_controller.ex
defmodule GoogleMapPhxWeb.PointController do
  use GoogleMapPhxWeb, :controller

  alias GoogleMapPhx.Maps
  alias GoogleMapPhx.Maps.Point

  def index(conn, _params) do
    points = Maps.list_points()
    points_json = Jason.encode!( points |> Enum.map( fn o -> %{ id: o.id, name: o.name, lat: o.latitude, lng: o.longitude } end ) )
    render(conn, :index, points: points, points_json: points_json)
  end

  def new(conn, _params) do
    changeset = Maps.change_point(%Point{})
    render(conn, :new, changeset: changeset)
  end

  def create(conn, %{"point" => point_params}) do
    case Maps.create_point(point_params) do
      {:ok, point} ->
        conn
        |> put_flash(:info, "Point created successfully.")
        |> redirect(to: ~p"/points/#{point}")

      {:error, %Ecto.Changeset{} = changeset} ->
        render(conn, :new, changeset: changeset)
    end
  end

  def show(conn, %{"id" => id}) do
    point = Maps.get_point!(id)
    point_json = %{ id: point.id, name: point.name, lat: point.latitude, lng: point.longitude }
      |> Jason.encode!
    render(conn, :show, point: point, point_json: point_json)
  end

  def edit(conn, %{"id" => id}) do
    point = Maps.get_point!(id)
    changeset = Maps.change_point(point)
    render(conn, :edit, point: point, changeset: changeset)
  end

  def update(conn, %{"id" => id, "point" => point_params}) do
    point = Maps.get_point!(id)

    case Maps.update_point(point, point_params) do
      {:ok, point} ->
        conn
        |> put_flash(:info, "Point updated successfully.")
        |> redirect(to: ~p"/points/#{point}")

      {:error, %Ecto.Changeset{} = changeset} ->
        render(conn, :edit, point: point, changeset: changeset)
    end
  end

  def delete(conn, %{"id" => id}) do
    point = Maps.get_point!(id)
    {:ok, _point} = Maps.delete_point(point)

    conn
    |> put_flash(:info, "Point deleted successfully.")
    |> redirect(to: ~p"/points")
  end
end
```

完成です。

確認してみましょう。

```zsh:zsh
% mix phx.server
```

[![Image from Gyazo](https://i.gyazo.com/1e53e1706da842abd2a6de745bd75aa1.png)](https://gyazo.com/1e53e1706da842abd2a6de745bd75aa1)

はい、完了です。お疲れ様でした。

## 最後に

今回実はRailsで動かしたStimulusをほぼコピペ状態で移植してみました。

Webアプリの移植でJSのフレームワーク周りがネックだったりするのですが、この手間なら全然良いよなぁ・・という感じでした。

#### 明日15日は、

 @g_kenkun さんで、

「3年ぶりにElixirのAtCoder用Mixタスクライブラリを更新しました」

です。よろしくお願いいたします。

最後までご覧いただき、ありがとうございます。