概要
- Elmで入力フォームを作成。手探りメモ。
- todoのサンプルよりも、もう少し複雑なことをしたい。
- もしもよいサンプルあれば教えてください。
ソース
以下のモデルの、Array Stringの部分を可変で入力できるようなフォームを作りたい。
入力欄の3番目を編集、などを考えてListよりもArrayで持ってみた。
type alias Character =
{ name : String
, kana : String
, organ : String
, traits : Array String
}
可変にしようとすると、追加・編集・削除が必要になるので以下のようなメッセージが必要になる。
type Msg
= InputName String
| InputKana String
| InputOrgan String
| AddTrait
| InputTrait Int String
| DeleteTrait Int
ビューはこんな感じで。
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
]
追加・入力・削除のメッセージを渡すと、以下のように引数ながくなってしまうけれど、よいのかしら。
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
sliceでこんな感じでよいかしら。もっとスマートなやり方ないかなぁ。
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 )
ソース全体
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 [ "" ])
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" ] ]
]