Elmで入力フォームを作るメモ

## 概要

* Elmで入力フォームを作成。手探りメモ。
* todoのサンプルよりも、もう少し複雑なことをしたい。
* もしもよいサンプルあれば教えてください。

## ソース

以下のモデルの、Array Stringの部分を可変で入力できるようなフォームを作りたい。
入力欄の3番目を編集、などを考えてListよりもArrayで持ってみた。

```Elm
type alias Character =
    { name : String
    , kana : String
    , organ : String
    , traits : Array String
    }
```

可変にしようとすると、追加・編集・削除が必要になるので以下のようなメッセージが必要になる。

```Elm
type Msg
    = InputName String
    | InputKana String
    | InputOrgan String
    | AddTrait
    | InputTrait Int String
    | DeleteTrait Int
```

ビューはこんな感じで。

```Elm
editArea : Character -> EditorModel -> Html Msg
editArea character editor =
    div [ class "character-edit-area" ]
        [ inputArea "name" "名前" character.name InputName
        , inputArea "kana" "フリガナ" character.name InputKana
        , inputAreaWithAutocomplete "organ" "変異器官" character.organ InputOrgan "organs" (List.map (\o -> o.name) editor.organs)
        , inputAreas "traits" "特性" character.traits InputTrait AddTrait DeleteTrait
        ]
```
追加・入力・削除のメッセージを渡すと、以下のように引数ながくなってしまうけれど、よいのかしら。

```Elm
inputAreas : String -> String -> Array String -> (Int -> String -> msg) -> msg -> (Int -> msg) -> Html msg
inputAreas fieldId labelName arrays updateMsg addMsg deleteMsg =
    div []
        [ div []
            (List.concat
                [ Array.toList <| Array.indexedMap (\i v -> updateArea i fieldId labelName v updateMsg deleteMsg) arrays
                , addButton labelName addMsg
                ]
            )
        ]
````

更新のときに、指定したインデックスの要素を削除するような関数ってArrayにないのだなと思った。
[Array](https://package.elm-lang.org/packages/elm/core/latest/Array)
sliceでこんな感じでよいかしら。もっとスマートなやり方ないかなぁ。


```Elm
update : Msg -> Character -> ( Character, Cmd Msg )
update msg char =
    let
        deleteAt i arrays =
            let
                len =
                    Array.length arrays

                head =
                    Array.slice 0 i arrays

                tail =
                    Array.slice (i + 1) len arrays
            in
            Array.append head tail
    in
    case msg of
        AddTrait ->
            let
                c =
                    { char | traits = Array.push "" char.traits }
            in
            ( c, Cmd.none )

        InputTrait i s ->
            let
                c =
                    { char | traits = Array.set i s char.traits }
            in
            ( c, Cmd.none )

        DeleteTrait i ->
            let
                c =
                    { char | traits = deleteAt i char.traits }
            in
            ( c, Cmd.none )
```

### ソース全体
```Elm:ModelCharacter.elm
module Models.Character exposing (Character, EditorModel, characterDecoder, encodeCharacter, initCharacter)

import Array exposing (Array)
import GoogleSpreadSheetApi as GSAPI exposing (Organ)
import Json.Decode as Decode exposing (Decoder, Value, decodeString, field, string, succeed)
import Json.Decode.Pipeline exposing (hardcoded, optional, required)
import Json.Encode as E


type alias Character =
    { storeUserId : String
    , characterId : String
    , name : String
    , kana : String
    , organ : String
    , traits : Array String
    }


type alias EditorModel =
    { organs : List Organ
    }


initCharacter : String -> Character
initCharacter storeUserId =
    Character storeUserId "" "" "" "" (Array.fromList [ "" ])


encodeCharacter : Character -> String
encodeCharacter c =
    -- エンコード後のインデント0。
    c |> encodeCharacterToValue |> E.encode 0


encodeCharacterToValue : Character -> E.Value
encodeCharacterToValue c =
    E.object
        [ ( "storeUserId", E.string c.storeUserId )
        , ( "characterId", E.string c.characterId )
        , ( "name", E.string c.name )
        , ( "kana", E.string c.kana )
        , ( "organ", E.string c.organ )
        , ( "traits", E.array E.string c.traits )
        ]


characterDecoder : Decoder Character
characterDecoder =
    Decode.succeed Character
        |> Json.Decode.Pipeline.required "storeUserId" Decode.string
        |> Json.Decode.Pipeline.required "characterId" Decode.string
        |> Json.Decode.Pipeline.required "name" Decode.string
        |> Json.Decode.Pipeline.required "kana" Decode.string
        |> Json.Decode.Pipeline.required "organ" Decode.string
        |> Json.Decode.Pipeline.optional "traits" (Decode.array Decode.string) (Array.fromList [ "" ])

```


```Elm:CharacterEditor.elm
port module Page.MyPages.CharacterEditor exposing (Msg(..), editArea, update)

import Array exposing (Array)
import GoogleSpreadSheetApi as GSAPI exposing (Organ)
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (onClick, onInput)
import Models.Character exposing (..)
import Url
import Url.Builder


type Msg
    = InputName String
    | InputKana String
    | InputOrgan String
    | AddTrait
    | InputTrait Int String
    | DeleteTrait Int


update : Msg -> Character -> ( Character, Cmd Msg )
update msg char =
    let
        deleteAt i arrays =
            let
                len =
                    Array.length arrays

                head =
                    Array.slice 0 i arrays

                tail =
                    Array.slice (i + 1) len arrays
            in
            Array.append head tail
    in
    case msg of
        InputName s ->
            let
                c =
                    { char | name = s }
            in
            ( c, Cmd.none )

        InputKana s ->
            let
                c =
                    { char | kana = s }
            in
            ( c, Cmd.none )

        InputOrgan s ->
            let
                c =
                    { char | organ = s }
            in
            ( c, Cmd.none )

        AddTrait ->
            let
                c =
                    { char | traits = Array.push "" char.traits }
            in
            ( c, Cmd.none )

        InputTrait i s ->
            let
                c =
                    { char | traits = Array.set i s char.traits }
            in
            ( c, Cmd.none )

        DeleteTrait i ->
            let
                c =
                    { char | traits = deleteAt i char.traits }
            in
            ( c, Cmd.none )



-- 入力エリア


editArea : Character -> EditorModel -> Html Msg
editArea character editor =
    div [ class "character-edit-area" ]
        [ inputArea "name" "名前" character.name InputName
        , inputArea "kana" "フリガナ" character.name InputKana
        , inputAreaWithAutocomplete "organ" "変異器官" character.organ InputOrgan "organs" (List.map (\o -> o.name) editor.organs)
        , inputAreas "traits" "特性" character.traits InputTrait AddTrait DeleteTrait
        ]



-- 単純な入力


inputArea : String -> String -> String -> (String -> msg) -> Html msg
inputArea fieldId labelName val toMsg =
    div [ class "input-field" ]
        [ input [ placeholder labelName, id fieldId, type_ "text", class "validate", value val, onInput toMsg ] []
        , label [ for fieldId ] [ text labelName ]
        ]



-- 単純な入力。オートコンプリート付き


inputAreaWithAutocomplete : String -> String -> String -> (String -> msg) -> String -> List String -> Html msg
inputAreaWithAutocomplete fieldId labelName val toMsg listId autocompleteList =
    div [ class "input-field" ]
        [ input [ placeholder labelName, id fieldId, type_ "text", class "validate", value val, onInput toMsg, autocomplete True, list listId ] []
        , label [ for fieldId ] [ text labelName ]
        , datalist [ id listId ]
            (List.map (\s -> option [ value s ] [ text s ]) autocompleteList)
        ]



-- 可変の入力欄


inputAreas : String -> String -> Array String -> (Int -> String -> msg) -> msg -> (Int -> msg) -> Html msg
inputAreas fieldId labelName arrays updateMsg addMsg deleteMsg =
    div []
        [ div []
            (List.concat
                [ Array.toList <| Array.indexedMap (\i v -> updateArea i fieldId labelName v updateMsg deleteMsg) arrays
                , addButton labelName addMsg
                ]
            )
        ]



-- インデックス付きの編集


updateArea : Int -> String -> String -> String -> (Int -> String -> msg) -> (Int -> msg) -> Html msg
updateArea index fieldId labelName val updateMsg deleteMsg =
    let
        fid =
            fieldId ++ String.fromInt index
    in
    div [ class "input-field" ]
        [ input [ placeholder labelName, id fid, type_ "text", class "validate", value val, onInput (updateMsg index) ] []
        , label [ for fid ] [ text labelName ]
        , deleteButton deleteMsg index
        ]



-- 削除ボタン


deleteButton : (Int -> msg) -> Int -> Html msg
deleteButton deleteMsg index =
    button [ class "btn-floating btn-small halfway-fab waves-effect waves-light grey", onClick (deleteMsg index) ] [ i [ class "material-icons" ] [ text "delete" ] ]



-- 追加ボタン


addButton : String -> msg -> List (Html msg)
addButton labelName addMsg =
    [ text (labelName ++ "を追加  ")
    , button [ class "btn-floating btn-small waves-effect waves-light green", onClick addMsg ] [ i [ class "material-icons" ] [ text "add" ] ]
    ]
```

[この時点のソース](https://github.com/hibohiboo/garden/tree/3818676038f1f58b55757cb6440e2109099fca37)