$ 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が参考になります。