背景
- elm form decoderがよいという記事をいくつか見かけて気になっていた。
では実際にはどんな感じで使われるのだろうと、サンプルを見にいく。
挫折。 orz
-
サンプル
-
Atom.Inputって何? packageにもないし定義しているとこ見つからない。。。- →
srcフォルダではなくlib/srcフォルダ以下にいた。 -
cd lib && npm i && npm startとドキュメントみれるよと教えていただく。感謝。
- →
-
Attributes.boolAttribute、、? ...あー。Css.classWithPrefix "form__"、、? ...へー。
-
知らない手法がたくさん使われている。
勉強にはなるけれど、気が付いたら、そちらを調べてばかりいる。
もともとのelm form decoderの使い方にまだ辿り着いていない。
もっと簡単なサンプルとして、以下を教えていただいたので試してみる。
環境
- windows 10
- gitbash
- elm 0.19.1
- vscode
準備
elm reactorを使ってねと書かれていた。
elm reactor、、聞いたことはあるが、、。
調べてみたら、elmについてくる実行環境的なものの模様。(elm インストール) / ( elm reactorとwebpackを使って快適なElm開発環境を構築する)
仮想環境上でwebpackでコンパイルしてたので今まで使ったことなかった。
改めて、windowsにelmをインストールして、試してみる。
npm install -g elm elm-format
git clone git@github.com:miyamoen/elm-form-decoder-katakata.git
cd elm-form-decoder-katakata
elm reactor
バージョンが違うって怒られた。
以下を編集。
{
"type": "application",
"source-directories": [
"src"
],
- "elm-version": "0.19.0",
+ "elm-version": "0.19.1",
"dependencies": {
elm reactor
http://localhost:8000/src/KataKata01HelloForm.elm にアクセス。
今度はうまくいった!
実践
form decoderに至る前の段階から順を踏んでいく模様。
丁寧。
KataKata01HelloForm.elm
reactorで表示されたブラウザのページにアクセスすると、以下の課題が。
- 🆖 textは10文字以下の文字列です
update : Msg -> Model -> Model
update msg model =
case msg of
ChangeText string ->
-- 何か実装し忘れているような。`replace____me string`を書き換えましょう
{ model | text = replace____me string }
replace___meという関数を書き換えればよい模様。
stringとは、、と調べるとChangeText Stringということなので、文字列。
textとは、、{ text : String }。
なので、 replace___me: String -> Stringの関数を考えてやればよい。
関数の中身でやりたいことは、入力された文字を10文字以下にすること。
elmではどう書けば、とドキュメントをしらべる。
String.lengthとString.sliceが使えそう。
replace____me : String -> String
replace____me s =
if String.length s > 10 then
String.slice 0 10 s
else
s
クリア!
KataKata02SeparateModel
今回の課題。
- 🆖 textはformTextから変換されます
- 🆖 formTextが10文字より長いとき、変換は失敗します
ConvertText ->
{ model
-- `replace____me model.text`を書き換えましょう
| text = replace____me model.text
}
まずは、型の確認。{ text : Maybe String, formText : String } 。
textはformTextから変換されますってあるので、 text = replace____me model.formTextとなる。
なので、今回実装の必要な型はreplace___me: String -> Maybe Stringとなる。
MaybeはJust aとNothingを返せばよいっぽい。
type Maybe a
= Just a
| Nothing
よって、以下。
replace____me : String -> Maybe String
replace____me s =
if String.length s > 10 then
Nothing
else
Just s
クリア!
KataKata03IntAndRecord
課題が多い!
- 🆖valueはformから変換されます
- valueがformから変換されるように
replace____meを書き換えましょう
- valueがformから変換されるように
- 🆖form.textが10文字より長いとき、変換は失敗します
- form.textが10文字より長いときは変換が失敗するようにしましょう
- 🆖 form.textが空文字のとき、変換は失敗します
- form.textが空文字のときは変換が失敗するようにしましょう
- 🆖 form.numberが整数に変換できなければ、変換は失敗します
- form.numberが整数にできないときは変換が失敗するようにしましょう
- 🆖 form.numberが1以上10以下の整数に変換できなければ、変換は失敗します
- form.numberが1以上10以下の整数に変換できなければ失敗するようにしましょう
ConvertForm ->
{ model
-- `replace____me model.value`を書き換えましょう
| value = replace____me model.value
}
課題が多いけれど、順番にやっていこう。
落ち着くんだ。
型の確認。
type alias Model =
{ value : Result String Value, form : Form }
type alias Value =
{ text : String, number : Int }
type alias Form =
{ text : String, number : String }
value = replace____me model.formですね。
型はreplace____me : Form -> Result String Value。
Resultってなんだっけとelm Resultを読む。
type Result error value
= Ok value
| Err error
今回のreplace____meは、OK ValueとErr Stringを返せばよいってことですね。
まずは、前回の経験を活かせる文字列長のチェックから。いったんValueは固定値とする。
replace____me : Form -> Result String Value
replace____me form =
let
len =
String.length form.text
in
if len == 0 then
Err "必須項目です"
else if len > 10 then
Err "10文字以内で入力してください"
else
Ok (Value "" 0)
- ✅ form.textが10文字より長いとき、変換は失敗します
- ✅ form.textが空文字のとき、変換は失敗します
数値の変換は、String.toIntが使えそうです。
replace____me : Form -> Result String Value
replace____me form =
let
len =
String.length form.text
in
if len == 0 then
Err "必須項目です"
else if len > 10 then
Err "10文字以内で入力してください"
else
case String.toInt form.number of
Just i ->
if i > 0 && i < 11 then
Ok (Value form.text i)
else
Err "1以上10以下で入力してください"
Nothing ->
Err "整数を入力してください"
クリア!
追加課題
- 🆖 form.textがnumber文字より長いとき、変換は失敗します
- form.textがnumber文字より長いときは変換が失敗するようにしましょう
if len == 0 then
Err "必須項目です"
- else if len > 10 then
- Err "10文字以内で入力してください"
-
else
case String.toInt form.number of
Just i ->
- if i > 0 && i < 11 then
+ if len > i then
+ Err (form.number ++ "文字以内で入力してください")
+
+ else if i > 0 && i < 11 then
Ok (Value form.text i)
else
クリア!
KataKata04HelloFormDecoder
ここで form-decoder登場!
- 🆖 textはformTextから変換されます
- textがformTextから変換されるように
replace____me*を書き換えましょう- replace____me1はDecoder.runとdecoderを使ってformTextを変換しましょう
- replace____me2はDecoder.identityで
Decoder String err Strigを作りましょう
- textがformTextから変換されるように
- ✅ formTextが空文字のとき、変換は失敗します
- 🆖 formTextが10文字より長いとき、変換は失敗します
- formTextが10文字以上のときはTextTooLongのエラーで失敗するようにdecoderにバリデーションを足しましょう
update : Msg -> Model -> Model
update msg model =
case msg of
ChangeText string ->
{ model | formText = string }
ConvertText ->
-- `replace____me1 model.value`を書き換えましょう
replace____me1 model
decoder : Decoder String Error String
decoder =
-- `replace____me2 <| Decoder.always "虎にならない"`を書き換えましょう
replace____me2 <| Decoder.fail TextEmpty
型の確認。
type alias Model =
{ text : String, formText : String }
replace____me1: Model -> Modelreplace____me2: Decoder String Error String
小さいほうから。
replace____me2はDecoder.identityを使えと書いてあります。
elm form decoderを見に行って、identityで検索したら、以下がヒット。
name : Decoder String Error String
name =
Decoder.identity
|> Decoder.assert (Decoder.minLength NameRequired 1)
そのまま使えそうですね。
replace____me2 : Decoder String Error String
replace____me2 =
Decoder.identity
|> Decoder.assert (Decoder.minLength TextEmpty 1)
で、これ何やってるの?
キーワードの型を調べて並べてみる。
identity : Decoder input never inputassert : Validator a err -> Decoder input err a -> Decoder input err aminLength : err -> Int -> Validator String err
assertは2つの引数をとると。
今回の場合
- 1つ目の引数は、
(Decoder.minLength TextEmpty 1)。型はValidator String Error。-
a = String、err = Error
-
- 2つ目の引数は、
Decoder.identity |>。identityの型はDecoder input never input-
assertの2つ目の引数なので、Decoder a err aとなる - 1つめの引数により、
a = String, err = Errorなので、Decoder String Error String
-
-
Validator String Error -> Decoder String Error String -> Decoder String Error Stringとなる。
こんな感じかなー。
これで以下はOK。
- ✅formTextが空文字のとき、変換は失敗します
次は以下のエラーを消すために、バリデータを足す。
- 🆖 formTextが10文字より長いとき、変換は失敗します
Decoder.maxLength もあるようなので、それを使用する。
assert: Validator a err -> Decoder input err a -> Decoder input err aの2つ目の引数と戻り値に注目。
Decoder input err a を受け取って、Decoder input err a を返すことができる。
1つ目の空文字で失敗するDecoderに、
2つ目の10文字以上はエラーとする処理を加えて新たなDecoderにすることができる。
要するに、以下。
replace____me2 : Decoder String Error String
replace____me2 =
Decoder.identity
|> Decoder.assert (Decoder.minLength TextEmpty 1)
+ |> Decoder.assert (Decoder.maxLength TextTooLong 10)
最後に、変換。
ヒントにrunの結果はResult (List err) aなのでパターンマッチで欲しい値を手に入れるとあったので、以下。
replace____me1 : Model -> Model
replace____me1 model =
case Decoder.run decoder model.formText of
Ok text ->
{ model | text = text }
Err errorList ->
model
クリア!
KataKata05BuildUpDecoder
NGがいっぱい、、、!
気圧されそうになるけれど、一つ一つ片づけてゆく。
まずは、型の確認から、の前に、今回学ぶことを確認。
## 今回学ぶこと
- Int型のDecoder
- Decoder.intで
Decoder String err Intが作れる- 最大値を最小値のバリデーションを適用する
- minBoundとmaxBoundでできる
- Decoderの組み合わせ方
- top, fieldを使った組み合わせ方
- liftを使って入力の型を合わせる方法
上部コメントに、.liftの説明なども書いてあるので読む。
型の確認。
type alias Model =
{ value : Value, form : Form }
type alias Value =
{ text : String, number : Int }
type alias Form =
{ text : String, number : String }
decoder : Decoder Form Error Value
decoder =
-- この行全体を書き換えましょう
replace____me3 <| Decoder.always { text = "initial", number = 1 }
textDecoder : Decoder String Error String
textDecoder =
-- katakata 04のdecoderの実装をそのまま使いましょう
replace____me1 <| Decoder.always "虎にならない"
numberDecoder : Decoder String Error Int
numberDecoder =
-- この行全体を書き換えましょう
replace____me2 <| Decoder.always -5
まずはtextDecoderを置き換え。
textDecoder : Decoder String Error String
textDecoder =
Decoder.identity
|> Decoder.assert (Decoder.minLength TextEmpty 1)
|> Decoder.assert (Decoder.maxLength TextTooLong 10)
次に、Decoder.intの型を確認。int : err -> Decoder String err Int
Decoder.identityはidentity : Decoder input never inputだったので、比較すると、引数のerrを1つ追加してやる必要がある。
よって、以下。
numberDecoder : Decoder String Error Int
numberDecoder =
Decoder.int NumberInvalidInt
|> Decoder.assert (Decoder.minBound NumberBelow 1)
|> Decoder.assert (Decoder.maxBound NumberOver 10)
つぎに、この2つのdecoderを組み合わせる。
上部コメントに、以下のサンプルがあるので、それに倣う。
decoder : Decoder TigerForm Error Tiger
decoder =
Decoder.top Tiger
|> Decoder.field (Decoder.lift .name nameDecoder)
|> Decoder.field (Decoder.lift .species speciesDecoder)
decoder : Decoder Form Error Value
decoder =
Decoder.top Value
|> Decoder.field (Decoder.lift .text textDecoder)
|> Decoder.field (Decoder.lift .number numberDecoder)
クリア!
追加課題
文字列の長さはnumber以下にすることにしました。つまり、numberは1以上10以下の整数でtextはnumber文字以下の空文字ではない文字列です。
まずは考え方を読む。
Validatorを作る専用APIはありませんが、Validatorの定義をみれば作り方はわかると思います。
i -> Result (List err) ()を作ればいいわけです。customを使えば作ることができます。custom : (input -> Result (List err) a) -> Decoder input err a tigerValidator : Validator Tiger Err tigerValidator = custom <| \tiger -> if hoge tiger then Ok () else Err [ TigerTooBig ]
このようにデコードしたあとの虎を使っていくらでもバリデーションできます。
Decoder.customを確認。
custom <| \n ->が混乱したが、よく見ると上でやってることと変わらない。
customの第一引数にはinput -> Result (List err) aを渡せばよくて、それを無名関数で作って渡してるだけね。
<| ->が一行にあるのと、\nが改行コードがまず思い浮かんでしまうので混乱したっぽい。閑話休題。
回答は以下。
valueValidator : Decoder.Validator Value Error
valueValidator =
Decoder.custom <|
\value ->
if String.length value.text < value.number then
Ok ()
else
Err [ TextTooLong ]
後は、これを組み合わせてやるだけ。
decoder : Decoder Form Error Value
decoder =
Decoder.top Value
|> Decoder.field (Decoder.lift .text textDecoder)
|> Decoder.field (Decoder.lift .number numberDecoder)
+ |> Decoder.assert valueValidator
クリア!
バリデータを使っている部分を見る
これで、使い方は分かった。
あとは、それを出力している部分だが。。
let
errors =
Decoder.errors decoder form
in
Form.view []
[ Form.text
{ label = "text"
, onChange = ChangeText
, attrs = [ htmlAttribute <| Html.Attributes.autofocus True ]
, form = form.text
, value = value.text
}
, Form.errors
[ ( "必須です", List.member TextEmpty errors )
, ( "10文字以下にしてください", List.member TextTooLong errors )
]
, Form.text
{ label = "number"
, onChange = ChangeNumber
, attrs = []
, form = form.number
, value = String.fromInt value.number
}
, Form.errors
[ ( "整数を入力してください", List.member NumberInvalidInt errors )
, ( "1以上の整数を入力してください", List.member NumberBelow errors )
, ( "10以下の整数を入力してください", List.member NumberOver errors )
]
, Form.button
{ label = "変換する"
, msg = ConvertForm
, enable = List.isEmpty errors
}
]
Form.errosってなんじゃろう。
Componentフォルダにあった。
errors : List ( String, Bool ) -> List (Element msg)
errors messages =
[ none
, column [ spacing 12, paddingXY 12 0, Font.size 16, Font.color <| rgb255 234 122 184 ] <|
List.map
(\( message, invalid ) ->
if invalid then
Element.text message
else
none
)
messages
, none
, none
]
Elementとは、、?
elm-uiの部品ぽい。
ここからは、form decoderでなくて、elm uiのほうのことになりそうなので割愛。
Decoder.errors decoder formで取り出して、List.memberで表示/非表示の切り替えをすれば良さそう。