Elmを利用して、画像を選択してそれを表示するアプリを作る。
ファイル読み込みの方法#
Select.file
関数を利用する。これはファイル選択用のエクスプローラを開くためのCmd Msg
を作成してくれる。選択したファイルはMsg
に載せられる。
適切なMIMEタイプを指定すると、エクスプローラ上にてそのタイプのファイルしか選択できなくなる。例えば、text/plain
を選択しておけば、拡張子.txt
のファイルしか選択できなくなる。
1
| Select.file "MIMEタイプのリスト" "Msg"
|
画像ファイルへの変換#
こうして得られたファイルはFile
と呼ばれる型で保持される。
もしファイルを文字列として扱いたいなら、File.toString
を利用する。
もし画像として扱いたいなら、File.toUrl
を利用する。これは画像をBase64符号化した文字列を作る。これをimg
タグのsrc
属性に指定すれば、画像が表示される。
画像を選択し、それを表示するアプリの作成#
プロジェクトを作成して、elm/file
をインストール。
$ elm init
$ elm install elm/file
src/Main.elm
の雛形を作る。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
| module Main exposing (..)
import Browser
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
import File exposing (File)
import File.Select as Select
import Task
main =
Browser.element
{ init = init
, update = update
, view = view
, subscriptions = subscriptions
}
type alias Model =
{
}
init : () -> (Model, Cmd Msg)
init _ =
( {
}
, Cmd.none
)
type Msg
= Msg
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
( model
, Cmd.none
)
view : Model -> Html Msg
view model =
div []
[
]
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.none
|
htmlファイルを自分で作りたいので、makeのときはjsファイルを単独で生成させる。
$ elm make src/Main.elm --output=main.js
index.html
を作成し、次のようにする。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| <!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="elm"></div>
<script src="main.js"></script>
<script>
const app = Elm.Main.init({
node: document.getElementById('elm')
})
</script>
</body>
</html>
|
style.css
も作っておく。
これでelm reactorで見ると、真っ白なページが表示されているはず。
model定義#
以降はしばらくsrc/Main.elm
で作業する。
必要なのは画像のURLだから、それ用のレコードを用意する。画像が読み込まれていない時点では存在しないため、型はMaybe
にする。
1
2
3
| type alias Model =
{ url : Maybe String
}
|
それに応じてinit
も編集。
1
2
3
4
5
6
| init : () -> (Model, Cmd Msg)
init _ =
( { url = Nothing
}
, Cmd.none
)
|
view定義#
ボタンが押されたら、ImageRequested
メッセージを送るようにする。
もしmodel.url
が存在すれば、src
属性にそれを指定して画像を表示する。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| view : Model -> Html Msg
view model =
div []
[ button
[ onClick ImageRequested
]
[ text "Select Image"
]
, viewImage model
]
viewImage : Model -> Html Msg
viewImage model =
case model.url of
Nothing ->
p []
[ text "No image" ]
Just url ->
img
[ src url
]
[]
|
update定義#
先ほど書いたImageRequested
に加え、ファイルが取得できたときに送られるメッセージImageSelected
とファイルをurlに変換した時に送られるメッセージImageLoaded
を定義する。
1
2
3
4
| type Msg
= ImageRequested
| ImageSelected File
| ImageLoaded String
|
ImageRequested
が送られてきたとき: Select.file
でエクスプローラを開く。選択し終わるとImageSelected
メッセージが送られる。ImageSelected
が送られてきたとき: File.toUrl
でURLに変換する。これはTask型なので、Task.perform
でCmd Msg
を作成する。変換が終わるとImageLoaded
メッセージが送られる。ImageLoaded
が送られてきたとき: urlを入れたmodelを返す。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
ImageRequested ->
( model
, Select.file ["image/png"] ImageSelected
)
ImageSelected file ->
( model
, Task.perform ImageLoaded (File.toUrl file)
)
ImageLoaded url ->
( { model | url = Just url }
, Cmd.none
)
|
ボタン"Select"を押して画像を選択すると、次のように画像が右に表示される。
取り消しボタンの追加#
×ボタンを追加して、それをクリックすると画像の表示が消えるようにする。
×ボタンはa要素で表現し、記号はCSSで表現することにする。a要素は画像の右上に重なるように配置したいため、CSSでposition: absolute
を指定することになる。などいろいろ考えた結果、以下のように要素を構成する。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| viewImage : Model -> Html Msg
viewImage model =
...
Just url ->
div
[ class "image-wrapper"
]
[ div
[ class "image-container"
]
[ a
[ class "del-btn"
, onClick DeleteClicked
]
[]
, img
[ src url
]
[]
]
]
|
Msg
にDeleteClicked
を追加し、update
関数にも追加をする。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| type Msg
= ...
...
| DeleteClicked
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
...
DeleteClicked ->
( { model | url = Nothing }
, Cmd.none
)
|
style.cssの編集#
これでmakeした後にアプリを動かしても、a要素のサイズが0なのでボタンは現れない。これをCSSで調整する。
まずは画像とa要素をひとまとめにした領域.image-container
をinline-block
にすることで、画像のサイズぴったりに全体のサイズを調整する。a要素の位置を絶対座標にしたいので、position: relative
を指定する。
1
2
3
4
| .image-container {
display: inline-block;
position: relative;
}
|
a要素の領域は20px × 20pxにする。背景はグレーとし、丸みを帯びさせる。位置は右上にする。色は少し透明にしておく。マウスを乗せた時のカーソルの設定をする。
1
2
3
4
5
6
7
8
9
10
11
| .del-btn {
width: 30px;
height: 30px;
border-radius: 10px;
background-color: gray;
position: absolute;
top: 0;
right: 0;
opacity: 0.7;
cursor: pointer;
}
|
バツ印は擬似要素の枠線で指定する。枠線が領域中央になるように移動し、45度傾ける。枠の色は白にする。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| .del-btn::before {
content: "";
width: 20px;
height: 1px;
border-top: 2px solid white;
position: absolute;
top: 15px;
left: 5px;
transform: rotate(45deg);
}
.del-btn::after {
content: "";
width: 20px;
height: 1px;
border-top: 2px solid white;
position: absolute;
top: 15px;
left: 5px;
transform: rotate(-45deg);
}
|
いい感じ。
File - file 1.0.5