Help us understand the problem. What is going on with this article?

elm & servantでwebアプリ - その3.Elmで動的ページの作成

郵便番号をキーにして住所を表示する

前回Elmを導入しtop.htmlを表示させるところまで行いました。
今回はtop.htmlに動きをつけてみたいと思います。

Webサイトでの会員登録やお買い物などで、郵便番号を入力すると住所を埋めてくれるフォームがありますよね、これを作ってみたいと思います。
このような感じの画面で郵便番号を入力すると番地の途中まで埋まるようにしたいです。
20191228_elm&Haskell_03-01.png
clearボタンは入力された内容を消すために付けたものです。

Elmアーキテクチャ

Model, Update, Viewという3本柱を用意し、Msgを通じてこれらの連携を行うのが基本となります。
ご存じでない方は是非こちらを先にご一読ください。
では早速Modelから書いて行きたいと思います。

Model

画面には「郵便番号(先頭3桁 + 後ろ4桁)」および「住所(都道府県 + 市区町村 + 番地 + マンション名等)」の入力フォームを設けます。
Elmではこのような画面が持つ状態などをModelという型の中で表現します。上記の情報をModelのレコードとして定義します。

-- MODEL


type alias Model =
    { zip1 : String -- 郵便番号 前半3桁
    , zip2 : String -- 郵便番号 後半4桁
    , pref : String -- 都道府県
    , city : String -- 市区町村
    , hnum : String -- 番地
    , other : String -- マンション名等
    }

init : Model
init =
    Model "" "" "" "" "" ""

initはモデルの初期化を行う関数です。

MSG&UPDATE

MSGは画面上のボタンが押されたり、フォームに入力があったり、といったイベントを表現します。
今回はフォームへの入力clearボタンを押すというイベント発生が要アクションなイベントだと想定されますので、これをMSGとして定義します。
各フォームごとにMSGを用意します。

type Msg
    = InputZip1 String
    | InputZip2 String
    | InputPref String
    | InputCity String
    | InputHnum String
    | InputOther String
    | ClearForm

イベントが発生すると、このMsgが作られてUpdateに渡されます。
Updateでは受け取ったMsgに応じて行う処理を記述していきます。
今回は以下のような「イベント発生:処理」となります。

フォーム入力時:入力された情報をMODELに保持
郵便番号入力時:郵便番号が入力されたら、該当の住所を表示する
clearボタン押下時:フォームに入力済みの情報を消す

ということでUpdateを記述しましょう。

update : Msg -> Model -> Model
update msg model =
    case msg of
        InputZip1 str ->
            -- 郵便番号前半の入力時の処理

        InputZip2 str ->
            -- 郵便番号後半の入力時の処理

        InputPref str ->
            -- 都道府県入力時の処理

        InputCity str ->
            -- 市区町村の入力時の処理

        InputHnum str ->
            -- 番地の入力時の処理

        InputOther str ->
            -- その他(マンション名など)の入力時の処理

        ClearForm ->
            -- Clearボタンが押された時の処理

型を見ての通りupdateが返すのは「新しいMODEL」です。
「新しいMODEL」とはupdate関数で処理した結果できあがったMODELです。
(これは処理前のMODELと内容に差異がなくても構いません。)

フォーム入力時の処理を記述

まず入力フォームにユーザがテキストを入力されたときのことを考えてみます。

        InputZip1 str ->
            -- 郵便番号前半の入力時の処理

の部分ですね。
入力された郵便番号前半の文字列はMSGのstrに格納されて渡されてきます。
それをMODELに取り込みます。

        InputZip1 str ->
            { model | zip1 = str }

modelのzip1をstrで置き換えたものを新しいModelとして返しています。

この調子で他の処理も記述していくとupdate関数は以下のようになります。

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
    case msg of
        InputZip1 str ->
            { model | zip1 = str }

        InputZip2 str ->
            { model | zip2 = str }

        InputPref str ->
            { model | pref = str }

        InputCity str ->
            { model | city = str }

        InputHnum str ->
            { model | hnum = str }

        InputOther str ->
            { model | other = str }

        ClearForm ->
            init

ClearFormのときは入力フォームを初期化するのでinitを使っています。

郵便番号から住所を検索する処理がないので実装しましょう。
まず、郵便番号と住所のリストがないと始まりませんのでこちらから入手しました。
全都道府県でも良かったのですが、今回は香川県の分だけ使います。
香川県を選択したのは単に一番ファイルサイズが小さかっただけで特に意味はありません。
このようなcsvファイルになっています。

37201,"760 ","7600002","カガワケン","タカマツシ","アカネチョウ","香川県","高松市","茜町",0,0,0,0,0,0
37201,"760 ","7600064","カガワケン","タカマツシ","アサヒシンマチ","香川県","高松市","朝日新町",0,0,0,0,0,0
37201,"760 ","7600065","カガワケン","タカマツシ","アサヒマチ","香川県","高松市","朝日町",0,0,1,0,0,0

3, 7, 8, 9フィールド目を使います。
郵便番号以外については住所のデータ型として以下を用意してそのレコードとしました。

type alias Address =
    { pref : String
    , city : String
    , hnum : String
    }

これを使って郵便番号と住所のマップを用意します。
このマップを返すaddressesは以下のような定義になります。

addresses : Dict String Address
addresses =
    Dict.fromList
        [ ("7600000", Address "香川県高松市" "以下に掲載がない場合")
        , ("7600002", Address "香川県高松市" "茜町")
        , ("7600064", Address "香川県高松市" "朝日新町")
        -- 以下省略
        ]

これで郵便番号をキーとした住所のマップが手に入りました。
(「以下に掲載がない場合」とかありますが、そのままにしておきます。)
Dict.get関数がkey(郵便番号)に対応する住所(Address)を返してくれます。

コード中にこんなデータが埋め込まれているのはナンセンスだと思いますが、
今回はMain.elm内で完結させたいためこのような形としています。

updateの処理を追加します。

 update : Msg -> Model -> Model
 update msg model =
     case msg of
         InputZip1 str ->
             case Dict.get (str ++ model.zip2) addresses of
                 Just address ->
                     { model | zip1 = str, pref = address.pref, city = address.city, hnum = address.hnum }

                 Nothing ->
                     { model | zip1 = str }

         InputZip2 str ->
             case Dict.get (model.zip1 ++ str) addresses of
                 Just address ->
                     { model | zip2 = str, pref = address.pref, city = address.city, hnum = address.hnum }

                 Nothing ->
                     { model | zip2 = str }

InputZip1、InputZip2でやっていることはほとんど一緒です。
case Dict.get addresses (str ++ model.zip2) of
入力された郵便番号前半3桁とmodel内にある後半4桁をくっつけて、これをキーにしてaddressesから対応するAddressを取得しています。

Just address ->
    { model | zip1 = str, pref = address.pref, city = address.city, hnum = address.hnum }

取得したaddressのpref、city、hnumをmodelのpref、city、hnumに入れてあげます。

Nothing ->
    { model | zip1 = str }

取得できなかった場合は仕方ないのでzip1だけ変えておきます。

VIEW

VIEWには画面の内容を書いていきます。

-- VIEW

view : Model -> Html Msg
view model =
    div
        []
        [ div
            []
            [ label
                []
                [ text "郵便番号" ]
            , input
                [ type_ "text"
                , onInput InputZip1
                , value model.zip1
                ]
                []
            , span
                []
                [ text "-" ]
            , input
                [ type_ "text"
                , onInput InputZip2
                , value model.zip2
                ]
                []
            ]
        , div
            []
            [ label
                []
                [ text "都道府県" ]
            , input
                [ type_ "text"
                , onInput InputPref
                , value model.pref
                ]
                []
            ]
        , div
            []
            [ label
                []
                [ text "市区町村" ]
            , input
                [ type_ "text"
                , onInput InputCity
                , value model.city
                ]
                []
            ]
        , div
            []
            [ label
                []
                [ text "番地" ]
            , input
                [ type_ "text"
                , onInput InputHnum
                , value model.hnum
                ]
                []
            ]
        , div
            []
            [ label
                []
                [ text "アパート・マンション名・部屋番号" ]
            , input
                [ type_ "text"
                , onInput InputOther
                , value model.other
                ]
                []
            ]
        , button
            [ onClick ClearForm ]
            [ text "clear" ]
        ]

要素名 [ 属性 属性値 ] [ 要素の値 ]の形で書いていけばOKです。
※typeはelmでは型定義に使用するため、type_となっているのだと思います。

各インプットフォームの属性定義にonInput InputXXXXとあります。
これによってフォームへの入力イベントが発生したら"InputXXXX 入力文字"のMSGが作られ、現在のMODELとともにupdateへ引き渡されます。clearボタンも考え方は同じです。

main他

これでModel、Update、Viewが定義できました。残りの部分も記載したコード全量は以下となります。

Main.hs
module Main exposing (main)

import Browser
import Dict exposing (Dict)
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (onClick, onInput)



-- MAIN


main =
    Browser.sandbox { init = init, update = update, view = view }



-- MODEL


type alias Model =
    { zip1 : String
    , zip2 : String
    , pref : String
    , city : String
    , hnum : String
    , other : String
    }


init : Model
init =
    Model "" "" "" "" "" ""



-- UPDATE


type Msg
    = InputZip1 String
    | InputZip2 String
    | InputPref String
    | InputCity String
    | InputHnum String
    | InputOther String
    | ClearForm


update : Msg -> Model -> Model
update msg model =
    case msg of
        InputZip1 str ->
            case Dict.get (str ++ model.zip2) addresses of
                Just address ->
                    { model | zip1 = str, pref = address.pref, city = address.city, hnum = address.hnum }

                Nothing ->
                    { model | zip1 = str }

        InputZip2 str ->
            case Dict.get (model.zip1 ++ str) addresses of
                Just address ->
                    { model | zip2 = str, pref = address.pref, city = address.city, hnum = address.hnum }

                Nothing ->
                    { model | zip2 = str }

        InputPref str ->
            { model | pref = str }

        InputCity str ->
            { model | city = str }

        InputHnum str ->
            { model | hnum = str }

        InputOther str ->
            { model | other = str }

        ClearForm ->
            init



-- VIEW


view : Model -> Html Msg
view model =
    div
        []
        [ div
            []
            [ label
                []
                [ text "郵便番号" ]
            , input
                [ type_ "text"
                , onInput InputZip1
                , value model.zip1
                ]
                []
            , span
                []
                [ text "-" ]
            , input
                [ type_ "text"
                , onInput InputZip2
                , value model.zip2
                ]
                []
            ]
        , div
            []
            [ label
                []
                [ text "都道府県" ]
            , input
                [ type_ "text"
                , onInput InputPref
                , value model.pref
                ]
                []
            ]
        , div
            []
            [ label
                []
                [ text "市区町村" ]
            , input
                [ type_ "text"
                , onInput InputCity
                , value model.city
                ]
                []
            ]
        , div
            []
            [ label
                []
                [ text "番地" ]
            , input
                [ type_ "text"
                , onInput InputHnum
                , value model.hnum
                ]
                []
            ]
        , div
            []
            [ label
                []
                [ text "アパート・マンション名・部屋番号" ]
            , input
                [ type_ "text"
                , onInput InputOther
                , value model.other
                ]
                []
            ]
        , button
            [ onClick ClearForm ]
            [ text "clear" ]
        ]


-- HELPER


type alias Address =
    { pref : String
    , city : String
    , hnum : String
    }


addresses : Dict String Address
addresses =
    Dict.fromList
        [ ( "7600000", Address "香川県" "高松市" "以下に掲載がない場合" )
        , ( "7600002", Address "香川県" "高松市" "茜町" )
        , ( "7600064", Address "香川県" "高松市" "朝日新町" )
        , ( "7600065", Address "香川県" "高松市" "朝日町" )
        , ( "7610130", Address "香川県" "高松市" "庵治町" )
        , ( "7618033", Address "香川県" "高松市" "飯田町" )
        , ( "7618002", Address "香川県" "高松市" "生島町" )
        , ( "7600038", Address "香川県" "高松市" "井口町" )
        -- 省略
        ]

ここまでに触れなかった部分を見ていきます。

module Main exposing (main)

module Main exposing (main)

モジュール名(Main)の宣言と公開する関数(main)を定義しています。
もし作成したaddressesなどを他のモジュールから使いたい場合はexpoisng (main, addresses)のように書きます。全て公開するならexposing (..)となります。

import

import Browser
import Dict exposing (Dict)
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (onClick, onInput)

利用するモジュール名に続けて利用する関数などをexposingに書きます。exposing (..)とすると、そのモジュールが公開しているすべての関数やデータ型をインポートします。exposing (..)としなくても、Browser.sandboxのように指定してやればそのモジュールが公開している関数などは利用できます。

main

main =
    Browser.sandbox { init = init, update = update, view = view }

Browserモジュールにはsandbox、element、document、applicationといった関数が用意されており、
作りたいアプリケーションに応じて適切なものを選択できます。
右に行くに連れてできることが増えるようですが、今回はsandboxで十分です。

コンパイル&動作確認

Main.elmをコンパイルしてtop.htmlを差し替えたら、その1で作ったサーバを起動してブラウザから見てみます。

[user@remote ~/webapp/frontend/src]$ elm make Main.elm --output=../top.html
Success! Compiled 1 module.

    Main ───> ../top.html

[user@remote ~/webapp/frontend/src]$ cd ../../backend
[user@remote ~/webapp/frontend/src]$ stack exec backend-exe

20191228_elm&Haskell_03-02.png
見た目はちょっとあれですが、とりあえず想定どおり動くか試してみます。
郵便番号を入力すると、、、
20191228_elm&Haskell_03-03.png
ちゃんと住所が表示されました。clearボタンも想定どおり動作しています。

今回はここまでとして、次は見た目を整えるためにCSSファイルを別途用意して読み込ませます。
また、elm makeで.htmlファイルを生成してましたが、次からは.jsを生成する方法に変えたいと思います。
そのため、servant側へcssファイルやjsファイルを読み込ませるための修正を加えていきます。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away