PureScriptで素朴なDOM操作 (Hello, world!)

```
$ purs --version
0.12.5
```

# 前書き

PureScript v0.12の記事がなかなか見つからなかったのでやってみましたが、低水準APIでDOM操作をすると結構大変でした。
このアプローチはほどほどにして、早めに高水準なUIライブラリを使い始めるのが良いと思います。

UIライブラリに関しては [@hiruberuto](https://qiita.com/hiruberuto) さんの [PureScriptのUIライブラリまとめ](https://qiita.com/hiruberuto/items/ec3f58c7742e79e3885a) が参考になります。

今回使うパッケージは低水準APIの`purescript-web-html`です。

- `Web.HTML`
  - https://pursuit.purescript.org/packages/purescript-web-html
- `Web.DOM`
 - https://pursuit.purescript.org/packages/purescript-web-dom
- `Web.Event`
  - https://pursuit.purescript.org/packages/purescript-web-events

# プロジェクトの作成

`purescript`, `pulp`はnpm経由でインストールできます。

今回はパッケージマネージャに`psc-package`を使いますが、bowerでも代用可能です。
spagoという選択肢もあるようです。

```
$ pulp --psc-package init
$ pulp build
```

ルートにindex.htmlを作成します。

```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を一旦削除し、以下のように書き換えます。

```app.js
require("./output/Main").main();
```

後はparcelにバンドルしてもらうだけです。

```
$ parcel index.html
```

たったこれだけで**ホットリロード付きの開発環境**が手に入ります。かなりお手軽です。

# \<p>Hello, world!\</p>

PureScriptでElementを作ってbodyにappendChildするサンプルです。

```hs
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`を使います。

```hs
window :: Effect Window
window = --
-- const window = () => window;
```

### `window.document`の取得

`Web.HTML.Window.document`を使います。

```hs
document :: Window -> Effect HTMLDocument
document w = --
-- const document = w => () => w.document;
```

### `document.body`の取得

`Web.HTML.HTMLDocument.body`を使います。

```hs
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`の橋渡しです。

```hs
toNode :: HTMLElement -> Node
toNode = unsafeCoerce
```

### bodyの取得まとめ

```hs
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つに分けても同じことです。

```hs
main :: Effect Unit
main = do
  window       <- HTML.window
  htmlDocument <- HTML.document window
  maybeBody    <- HTML.body htmlDocument
```

## DOM操作

`Web.DOM`の話がメインです。

### createElement

`Web.DOM.Document.createElement`を使います。

```hs
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`でキャストする必要があります。

```hs
toDocument :: HTMLDocument -> Document
toDocument = unsafeCoerce
```

### setTextContent

`Node`にTextを貼るには`Web.DOM.Node.setTextContent`を使います。

```hs
setTextContent :: String -> Node -> Effect Unit
setTextContent = --
-- const setTextContent = val => node => () => {
--   node.textContent = val;
-- };
```

### `Element`を`Node`として使う

PureScript上で`Element`を`Node`にキャストするための`Web.DOM.Element.toNode`があります。

```hs
toNode :: Element -> Node
toNode = unsafeCoerce
```

`Web.HTML.HTMLElement.toNode`と混同しないように注意です。

### appendChild

`Web.DOM.Node.appendChild`を使います。
ノードは子 → 親の順で指定します。戻り値は子ノードです。

```hs
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>

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