$ purs --version
0.12.5
前書き
PureScript v0.12の記事がなかなか見つからなかったのでやってみましたが、低水準APIでDOM操作をすると結構大変でした。
このアプローチはほどほどにして、早めに高水準なUIライブラリを使い始めるのが良いと思います。
UIライブラリに関しては @hiruberuto さんの PureScriptのUIライブラリまとめ が参考になります。
今回使うパッケージは低水準APIのpurescript-web-html
です。
-
Web.HTML
Web.DOM
- https://pursuit.purescript.org/packages/purescript-web-dom
-
Web.Event
プロジェクトの作成
purescript
, pulp
はnpm経由でインストールできます。
今回はパッケージマネージャにpsc-package
を使いますが、bowerでも代用可能です。
spagoという選択肢もあるようです。
$ pulp --psc-package init
$ pulp build
ルートにindex.htmlを作成します。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>PureScript</title>
</head>
<body>
<script type="text/javascript" src="./app.js"></script>
</body>
</html>
お好みでmetaタグを追加したりcssを追加したりしてください。
次にバンドルします。
$ pulp browserify -O --to app.js
ブラウザでindex.htmlを開いてコンソールにHello sailor!と表示されていれば成功です。
必要なパッケージをインストールして、一応ビルドしておきます。
パッケージはweb-html
だけ追加すれば十分です。
$ psc-package install web-html
$ pulp build
(任意) Parcel
基本はpulp browserify -O --to app.js
で事足りますが、parcel-bundlerを使うのもアリです。
$ npm init
$ npm i -D parcel-bundler
次に、app.jsを一旦削除し、以下のように書き換えます。
require("./output/Main").main();
後はparcelにバンドルしてもらうだけです。
$ parcel index.html
たったこれだけでホットリロード付きの開発環境が手に入ります。かなりお手軽です。
<p>Hello, world!</p>
PureScriptでElementを作ってbodyにappendChildするサンプルです。
module Main where
import Prelude
import Data.Maybe (Maybe(..))
import Effect (Effect)
import Effect.Console (error) as Console
import Web.DOM.Document (createElement) as DOM
import Web.DOM.Element (toNode) as DOM
import Web.DOM.Node (appendChild, setTextContent) as DOM
import Web.HTML (window) as HTML
import Web.HTML.HTMLDocument (body, toDocument) as HTML
import Web.HTML.HTMLElement (toNode) as HTML
import Web.HTML.Window (document) as HTML
main :: Effect Unit
main = do
htmlDocument <- HTML.window >>= HTML.document
maybeBody <- HTML.body htmlDocument
let document = HTML.toDocument htmlDocument
case maybeBody of
Nothing -> Console.error "bodyが無い"
Just body -> do
pNode <- DOM.toNode <$> DOM.createElement "p" document
DOM.appendChild pNode (HTML.toNode body) >>=
DOM.setTextContent "Hello, world!"
感想
低水準APIでのDOM操作はまあまあ辛かったです。頭の体操にはなりました。
ボタンなどはWeb.DOM.Element.setAttribute
や Web.Event.EventTarget.addEventListener
を使うと一応作れると思いますが、PureScriptでやる利点をあまり感じません。
FFIを使うというのもアリだと思います。
むしろ、高水準なUIライブラリとpurescript-affjax
、purescript-simple-json
あたりの使い方の勉強に時間を投資するのが良いと思います。
解説
一応解説です。
bodyを取得するまで
Web.HTML
の話がメインです。
window
の取得
Web.HTML.window
を使います。
window :: Effect Window
window = --
-- const window = () => window;
window.document
の取得
Web.HTML.Window.document
を使います。
document :: Window -> Effect HTMLDocument
document w = --
-- const document = w => () => w.document;
document.body
の取得
Web.HTML.HTMLDocument.body
を使います。
body :: HTMLDocument -> Effect (Maybe HTMLElement)
body = map toMaybe <<< _body
-- const _body = d => () => d.body;
bodyをHTMLElementとして取得します。戻り値はMaybe HTMLElement
です。
仕様に疎いのでMaybe
になっている有難味があまり分かりませんでした。
HTMLElement
をNode
として使う
Web.HTML.HTMLElement.toNode
を使うとWeb.DOM.Node
にキャストできます。
Web.HTML
とWeb.DOM
の橋渡しです。
toNode :: HTMLElement -> Node
toNode = unsafeCoerce
bodyの取得まとめ
module Main where
import Prelude
import Data.Maybe (Maybe(..))
import Effect (Effect)
import Effect.Console (error) as Console
import Web.DOM (Node) as DOM
import Web.HTML (HTMLElement, window) as HTML
import Web.HTML.HTMLDocument (body) as HTML
import Web.HTML.HTMLElement (toNode) as HTML
import Web.HTML.Window (document) as HTML
main :: Effect Unit
main = do
maybeBody <- HTML.window >>= HTML.document >>= HTML.body
case maybeBody of
Nothing -> Console.error "bodyが無い"
Just (bodyAsElement :: HTML.HTMLElement) -> do
let (bodyAsNode :: DOM.Node) = HTML.toNode bodyAsElement
--
bodyAsElement
とbodyAsNode
の実体は同じです。
また、maybeBody <- HTML.window >>= HTML.document >>= HTML.body
と一行で書いてしまっていますが、以下のように3つに分けても同じことです。
main :: Effect Unit
main = do
window <- HTML.window
htmlDocument <- HTML.document window
maybeBody <- HTML.body htmlDocument
DOM操作
Web.DOM
の話がメインです。
createElement
Web.DOM.Document.createElement
を使います。
createElement :: String -> Document -> Effect Element
createElement = --
-- const createElement = name => d => () => d.createElement(name);
補足(document.createElement
)
JSでconst elem = document.createElement("div");
とやるとします。
これをFFIをせずにやるにはWeb.HTML.Window
でdocument :: HTMLDocument
を取得した後、Web.HTML.HTMLDocument.toDocument
でキャストする必要があります。
toDocument :: HTMLDocument -> Document
toDocument = unsafeCoerce
setTextContent
Node
にTextを貼るにはWeb.DOM.Node.setTextContent
を使います。
setTextContent :: String -> Node -> Effect Unit
setTextContent = --
-- const setTextContent = val => node => () => {
-- node.textContent = val;
-- };
Element
をNode
として使う
PureScript上でElement
をNode
にキャストするためのWeb.DOM.Element.toNode
があります。
toNode :: Element -> Node
toNode = unsafeCoerce
Web.HTML.HTMLElement.toNode
と混同しないように注意です。
appendChild
Web.DOM.Node.appendChild
を使います。
ノードは子 → 親の順で指定します。戻り値は子ノードです。
appendChild :: Node -> Node -> Effect Node
appendChild = --
-- const appendChild = node => parent => () => {
-- return parent.appendChild(node);
-- }
(おまけ) Halogen
bodyはAff環境下でHalogen.Aff.awaitBody
することで取得することができます。
コンポーネントはHalogen.mkComponent
で作ることができ、initialState
、render
、eval
の3つが必要です。
この内のrenderに関してですが、テキストノードはHalogen.HTML.text
で作れます。pタグはHalogen.HTML.p
など、全て用意されています。
作ったコンポーネントはHalogen.VDom.Driver.runUI
で表示させることができます。
Halogenで<p>Hello, world!</p>
module Main where
import Prelude
import Effect (Effect)
import Halogen as H
import Halogen.Aff as HA
import Halogen.VDom.Driver (runUI)
import Halogen.HTML as HH
main :: Effect Unit
main = HA.runHalogenAff do
body <- HA.awaitBody
runUI component unit body
where
component = H.mkComponent
{ initialState: const {}
, render: const $ HH.p_ [HH.text "Hello, world!"]
, eval: H.mkEval H.defaultEval
}
StateやActionの管理は公式exampleが参考になります。