概要
Elm 0.18で作るTodoアプリ(1)を参考に、elm0.19でタイトルを追加するサンプルを作ってみた。
以下の部分を気を付ければ、元の記事はelm0.19でも十分に参考になる。
-
a ! []
がTuple.pair a Cmd.None
になった -
Elm.Main.embed(mountNode)
がElm.Main.init({flags: 6, node: mountNode})
になった -
Html.program
がBrowser.element
になった
ここまでやった感想としては、elmはエラーメッセージがとても親切で嬉しい。
String.fromIntにしたら?ってメソッドまで教えてくれる。
elm_1 | 52| Tuple.pair { model | handoutList = model.handoutList ++ [ newHO ], nextId = model.nextId + 1 } (toJs ("Add Handout : " ++ model.nextId ++ ":" ++ title))
elm_1 | ^^^^^^^^^^^^
elm_1 | Try using String.fromInt to turn it into a string? Or put it in [] to make it a
タイトル以外も追加して一応動く形にしたのが以下。
開発環境
- Windows 10
- Vagrant 2.1.5
- Virtualbox 5.2.18
- Ubuntu 18.04 LTS (Bionic Beaver)
- Docker version 18.06.0-ce, build 0ffa825
- docker-compose version 1.22.0, build f46880fe
前回記事で作成したものを使う。
ファイル構成
+ bin # dockerの起動などのコマンドをシェルスクリプトにして保存
- docker
- elm
- Dockerfile # 開発環境ツール
- docker-compose.yml # dockerコンテナにディレクトリやファイルをマッピングする設定
- app
- .babelrc # babel用設定
- package.json # jsのパッケージ
- elm.json # elmのパッケージ
- webpack.config.js # webpack設定
- src
- styles.scss # スタイルシート
- card.js # elmを読み込むためのjsファイル
- html
- card.html # jsファイルを読み込むhtmlファイル
- Card
- Main.elm # 大元のプログラム
- HandoutList.elm # ハンドアウト一覧
- Handout.elm # ハンドアウト本体
- HandoutCreate.elm # 入力部分
ファイル
materialize.cssを使おうとしたが、elmで作られた部分にはjsがうまく動作しない模様。
elmのマテリアルデザインのコンポーネントを使ったほうがよさそうだが、0.19対応のものを見つけられなかった。
<div id="cards"></div>
のノードにelmを紐づける。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0-rc.2/css/materialize.min.css" />
<script src='https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0-rc.2/js/materialize.min.js'></script>
<title>ハンドアウト</title>
</head>
<h1>ハンドアウト</h1>
<body>
<div class="main-content">
<div id="cards"></div>
</div>
</body>
</html>
jsファイルはelmの読み込みを主に行う。
上記htmlで用意したnodeへの紐づけを行っている。
elm0.18のときとは、Elm.Main.init
が書き方が異なっていた。
'use strict';
require("./styles.scss");
const {Elm} = require('./Card/Main');
const mountNode = document.getElementById('cards');
const app = Elm.Main.init({flags: 6, node: mountNode});
app.ports.toJs.subscribe(data => {
console.log(data);
})
// Use ES2015 syntax and let Babel compile it for you
var testFn = (inp) => {
let a = inp + 1;
return a;
}
Mainで基本的にやっていることは、各要素に渡しているだけ。
module Main exposing (main)
import Browser
import Card.HandoutCreator as HandoutCreator
import Card.HandoutList as HandoutList
import Html exposing (..)
import Html.Attributes exposing (..)
main : Program Int Model Msg
main =
Browser.element
{ init = init
, update = update
, view = view
, subscriptions = \_ -> Sub.none
}
-- model
type alias Model =
{ handoutCreator : HandoutCreator.Model
, handoutListModel : HandoutList.Model
}
initialModel : Model
initialModel =
{ handoutCreator = HandoutCreator.initialModel
, handoutListModel = HandoutList.initialModel
}
init : Int -> ( Model, Cmd Msg )
init flags =
( initialModel, Cmd.none )
type Msg
= HandoutCreatorMsg HandoutCreator.Msg
| HandoutListMsg HandoutList.Msg
-- update
update : Msg -> Model -> ( Model, Cmd Msg )
update message model =
case message of
HandoutCreatorMsg subMsg ->
let
( updatedCreator, handoutCreatorCmd ) =
HandoutCreator.update subMsg model.handoutCreator
in
( { model | handoutCreator = updatedCreator }, Cmd.map HandoutCreatorMsg handoutCreatorCmd )
HandoutListMsg subMsg ->
let
( updatedHandoutListModel, handoutListCmd ) =
HandoutList.update subMsg model.handoutCreator.inputStr model.handoutListModel
in
( { model | handoutListModel = updatedHandoutListModel }, Cmd.map HandoutListMsg handoutListCmd )
-- subscription
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.none
-- view
view : Model -> Html Msg
view model =
div [ class "container" ]
[ Html.map HandoutCreatorMsg (HandoutCreator.view model.handoutCreator)
, Html.map HandoutListMsg (HandoutList.view model.handoutListModel)
]
model ! []
の書き方が使えなくなっていたので、 Tuple.pair model Cmd.none
に変更している。
module Card.HandoutCreator exposing (Item, Model, Msg(..), handoutInput, initialModel, update, view)
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (onClick, onInput)
type alias Item =
String
type alias Model =
{ inputTitle : Item
}
initialModel : Model
initialModel =
{ inputTitle = ""
}
type Msg
= NoOp
| UpdateInput String
-- update
update : Msg -> Model -> ( Model, Cmd Msg )
update message model =
case message of
NoOp ->
Tuple.pair model Cmd.none
UpdateInput s ->
Tuple.pair { model | inputTitle = s } Cmd.none
-- view
view : Model -> Html Msg
view model =
div []
[ handoutInput model
]
handoutInput : Model -> Html Msg
handoutInput model =
Html.form [ class "no-autoinit" ]
[ label [ attribute "for" "inputTitle" ]
[ text "タイトル"
]
, input
[ onInput UpdateInput, value model.inputTitle, id "inputTitle" ]
[]
]
ここは特にいみなくportを試している以外は、参考にしたページとほぼ変わらない。
port module Card.HandoutList exposing (Model, Msg(..), initialModel, toJs, update, updateButton, view, viewList)
import Browser
import Browser.Navigation as Nav
import Card.Handout exposing (Handout, Msg, insaneHandout, new, update)
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
import Url exposing (Url)
import Url.Parser as UrlParser
port toJs : String -> Cmd msg
type alias Model =
{ handoutList : List Handout
, nextId : Int
}
initialModel : Model
initialModel =
{ handoutList =
[ Handout 1 "item1" False
]
, nextId = 2
}
-- UPDATE
type Msg
= NoOp
| AddNew
| HandoutMsg Card.Handout.Msg
update : Msg -> String -> Model -> ( Model, Cmd Msg )
update message title model =
case message of
NoOp ->
( model, Cmd.none )
AddNew ->
let
newHO =
Card.Handout.new model.nextId title False
in
Tuple.pair { model | handoutList = model.handoutList ++ [ newHO ], nextId = model.nextId + 1 } (toJs ("Add Handout : " ++ String.fromInt model.nextId ++ ":" ++ title))
HandoutMsg subMsg ->
let
itemIsNotDeleted m =
not m.del
updatedHandoutList =
List.map (Card.Handout.update subMsg) model.handoutList
in
Tuple.pair { model | handoutList = List.filter itemIsNotDeleted updatedHandoutList } Cmd.none
-- VIEW
view : Model -> Html Msg
view model =
div []
[ p [] [ text "ハンドアウト一覧" ]
, updateButton model.handoutList
, div [ class "print" ]
[ viewList model.handoutList
]
]
updateButton : List Handout -> Html Msg
updateButton models =
div []
[ button [ onClick AddNew ] [ text "Add" ] ]
viewList : List Handout -> Html Msg
viewList models =
ul [] (List.map insaneHandout models) |> Html.map HandoutMsg
viewで無理やりインセインのハンドアウト作っているのでノイズになってしまっていて申し訳ない。
本質はview以外なので、とはいえこちらも元のページと大きな違いはない。
module Card.Handout exposing (Handout, Msg, insaneHandout, new, update)
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
type alias Handout =
{ id : Int
, title : String
, del : Bool
}
type Msg
= NoOp
| OnDelete Int
update : Msg -> Handout -> Handout
update message model =
case message of
NoOp ->
model
OnDelete id ->
if id == model.id then
{ model | del = True }
else
model
new : Int -> String -> Bool -> Handout
new i s d =
{ id = i
, title = s
, del = d
}
insaneHandout : Handout -> Html Msg
insaneHandout model =
li []
[ div [ class "insane-card card" ]
[ div [ class "f1" ]
[ open model.title
]
, div [ class "f1" ]
[ secret
]
]
, viewDeleteButton model
]
viewDeleteButton : Handout -> Html Msg
viewDeleteButton ho =
span []
[ button [ onClick (OnDelete ho.id) ] [ text "x" ]
]
secret =
div [ class "card black insane-wrapper", style "width" "190px", style "height" "300px" ]
[ div [ class "card-title white-text", style "flex" "1", style "text-align" "center" ] [ text "Handout" ]
, secretInnerCard
]
secretInnerCard =
div
[ class "card white"
, style "display" "flex"
, style "width" "180px"
, style "height" "290px"
, style "flex-direction" "column"
, style "justify-content" "center"
, style "align-items" "center"
]
[ div
[ class "card black"
, style "width" "170px"
, style "height" "280px"
, style "margin-bottom" "0.5rem"
]
[ div [ class "white-text font-s", style "flex" "1", style "text-align" "center" ] [ text "秘密" ]
, secretInnerWrapper
, div [ class "white-text font-s insane-secret-footer" ] [ text "この秘密を自分から明らかに" ]
, div [ class "white-text font-s insane-secret-footer" ] [ text "することはできない" ]
]
]
secretInnerWrapper =
div [ class "insane-wrapper" ]
[ secretMain
]
secretMain =
div
[ class "card white"
, style "width" "160px"
, style "height" "180px"
, style "margin" "1px"
]
[ secretShock
, secretContent
]
secretShock =
div
[ class "row font-m"
, style "border-bottom" "3px double #bbb"
, style "margin-bottom" "3px"
]
[ div [ class "col s4", style "border-right" "solid 1px #bbb", style "padding" "0" ] [ text "ショック" ]
, div [ class "col s8 font-ss" ] [ text "" ]
]
secretContent =
div [ class "card-content font-ss", style "padding" "0", style "margin" "2px" ]
[ text "てすと"
]
--
open title =
div [ class "card white insane-wrapper", style "width" "190px", style "height" "300px" ]
[ div [ class "card-title black-text", style "flex" "1", style "text-align" "center" ]
[ text title
]
, openInnerCard
]
openInnerCard =
div
[ class "card black"
, style "display" "flex"
, style "width" "180px"
, style "height" "290px"
, style "flex-direction" "column"
, style "justify-content" "center"
, style "align-items" "center"
]
[ div
[ class "card white"
, style "width" "170px"
, style "height" "280px"
, style "margin-bottom" "0.5rem"
]
[ div [ class "black-text font-s", style "flex" "1", style "text-align" "center" ] [ text "使命" ]
, openInnerWrapper
]
]
openInnerWrapper =
div [ class "insane-wrapper" ]
[ openMain
]
openMain =
div
[ class "card white"
, style "width" "160px"
, style "height" "220px"
, style "margin" "1px"
]
[ openContent
]
openContent =
div [ class "card-content font-ss", style "padding" "0", style "margin" "2px" ]
[ text "てすと"
]
作業履歴
環境構築
HTMLファイル追加
jsファイル追加
elmを一部適用
elmを一部適用 *
ハンドアウト作成
ハンドアウトを別ファイルに分割
ハンドアウト一覧を別ファイルに分割
入力欄表示
処理の移譲
追加メソッド切り出し
削除機能追加