Om(React)でsvgを描画する

## Omとは

facebook製 javascript frameworkである[React](https://github.com/facebook/react)のclojurescript wrapper ライブラリです．

ReactについてはQiitaにも様々な記事があげられているので詳細は割愛しますが，端的にその機能を説明するなら，「ページの状態を保持しているモデル（JSObject）」を「VirtualDOM」に流し込んで「実際に表示するDOM」を構築する，というものです（この辺りの解説は[narutoさんの記事](http://qiita.com/naruto/items/fdb61bc743395f8d8faf)が大変分りやすかったです）．

ページの構造（VirtualDOM）とその中身(JSObject)を分離できるため，これまでJQuery等で生じてきたような，情報の追加，削除の際のAppendTo(),text(),Children()etc..DOM操作地獄を避けることができます．

OmはそうしたReactの機能のうち，Componentを作成するために組み合わせて使えるような，JS Object操作等の機能をまとめたものです．Virtual DOMの生成については[sablono](https://github.com/r0man/sablono)のようなFrameworkを併用することが多いです．Omについても様々わかりやすい解説記事が出ていますので，そちらをご覧いただければと思います．

## OmでSVGを描画する

上述のように，Om(React)を用いると「構造」と「内容」を分離することができるので，SVGの構造を簡潔に書くことができます．簡単なサンプルを用意してみました．Omが初めての方は[公式チュートリアル](https://github.com/omcljs/om/wiki/Basic-Tutorial)も読んでみてください．

### 環境

- OSX
- Leiningen 2.7.0 on Java 1.8.0_101 

### プロジェクトセットアップ

#### Omプロジェクト作成

まず，[公式チュートリアル](https://github.com/omcljs/om/wiki/Basic-Tutorial)に従って

```
lein new figwheel svg-test -- --om
````

を走らせると，プロジェクトが作成されるので，プロジェクトファイルを修正します．変更部分は各コンポーネントのバージョンと，`om-tools`の追加です．

```clj

(defproject svg-test "0.1.0-SNAPSHOT"
  :description "FIXME: write this!"
  :url "http://example.com/FIXME"
  :license {:name "Eclipse Public License"
            :url "http://www.eclipse.org/legal/epl-v10.html"}

  :min-lein-version "2.6.1"

  :dependencies [[org.clojure/clojure "1.8.0"]
                 [org.clojure/clojurescript "1.9.89"]
                 [org.clojure/core.async "0.2.385"
                  :exclusions [org.clojure/tools.reader]]
                 [cljsjs/react "15.2.1-1"]
                 [cljsjs/react-dom "15.2.1-1"]
                 [sablono "0.7.4"]
                 [org.omcljs/om "1.0.0-alpha40"]
                 [prismatic/om-tools "0.4.0"]]

  :plugins [[lein-figwheel "0.5.4-7"]
            [lein-cljsbuild "1.1.3" :exclusions [[org.clojure/clojure]]]]

  :source-paths ["src"]

  :clean-targets ^{:protect false} ["resources/public/js/compiled" "target"]

  :cljsbuild {:builds
              [{:id "dev"
                :source-paths ["src"]
                :figwheel {:on-jsload "svg-test.core/on-js-reload"
                           :open-urls ["http://localhost:3449/index.html"]}

                :compiler {:main svg-test.core
                           :asset-path "js/compiled/out"
                           :output-to "resources/public/js/compiled/svg_test.js"
                           :output-dir "resources/public/js/compiled/out"
                           :source-map-timestamp true
                           :preloads [devtools.preload]}}
               {:id "min"
                :source-paths ["src"]
                :compiler {:output-to "resources/public/js/compiled/svg_test.js"
                           :main svg-test.core
                           :optimizations :advanced
                           :pretty-print false}}]}
  :figwheel {:css-dirs ["resources/public/css"]}
:profiles {:dev {:dependencies [[binaryage/devtools "0.7.2"]
                                  [figwheel-sidecar "0.5.4-7"]
                                  [com.cemerick/piggieback "0.2.1"]]
                   :source-paths ["src" "dev"]
                   :repl-options {
                                  :init (set! *print-length* 50)
                                  :nrepl-middleware [cemerick.piggieback/wrap-cljs-repl]}}})

```

#### figwheelの実行

上記の修正が終わったら，`lein figwheel`でfigwheelを走らせて，`http://localhost:3449/index.html`にアクセスします．

Omが正常に動いているなら

![image](https://qiita-image-store.s3.amazonaws.com/0/59864/f1f97917-c256-a61a-5e4a-627d5e272216.png)

が表示されているはずです．

#### sablonoを適用

`src/svg-test/core.cljs`を開き，requireを修正します．

``` clj
(ns svg-test.core
  (:require [om.core :as om :include-macros true]
            [om-tools.core :refer-macros [defcomponent]]
            [sablono.core :as html :refer-macros [html]]))
```

applicationコンポーネントをrootから分離し，sablono，om-toolsを適用して書き換えます．

``` clj
(defcomponent application [app owner]
  (render [_]
    (html [:h1 {} (:text app)])))

(om/root application  app-state
  {:target (. js/document (getElementById "app"))})
```

`(defonce app-state (atom {:text "Hello world!"}))`のtextを書き換えると表示される文字も変わるかと思います．

### SVGを描画してみる

SVGで国旗の色を変えるサンプルを作成してみます．
まず，app-stateに，国名と旗の色をもたせます．

``` clj
(defonce app-state (atom {:flags [{:name :germany :color ["#1D1D1F" "#E71920" "#FAB40A"]}
                                  {:name :bulgaria :color ["#FFFFFF" "#00AE0F" "#E60000"]}
                                  {:name :luxembourg :color ["#EF3340" "#00A3E0" "#FFCD00"]}]}))
```

次に，旗の色情報が渡すVirtualDOMを作成します．

```clj
(defcomponent flag-rect [{:keys [index color]} owner]
  (render [_]
   (html [:rect {:x 0 :y (* index 40) :width 150 :height 40
           :stroke "none" :fill color}])))

(defcomponent flag-view [{:keys [color]} owner]
  (render [_]
   (let [colors (map #(assoc {} :index %1 :color %2) (range) color)]
     (html [:svg {:width "200px" :height "120px"
                  :viewBox "0 0 200 120"}
            (om/build-all flag-rect colors {:key :index})]))))
```

つぎにapplicationを下記のように書き換え，flagsのfirstをflag viewに渡すと，VirtualDOMにcolor情報が流し込まれ，ドイツ国旗が表示されます．

```clj
(defcomponent application [{:keys [flags]} owner]
  (init-state [_]
   {:chosen :germany})
  (render-state [_ {:keys [chosen]}]
   (html 
    [:div
     (om/build flag-view (first flags))])))
```

最後に，applicationにselectorをつけてあげれば，DOM操作は行わずに国旗の色を変更できるようになります．

``` clj
(defn- set-key! [val owner]
  (om/set-state! owner :chosen (keyword val)))

(defcomponent application [{:keys [flags]} owner]
  (init-state [_]
   {:chosen :germany})
  (render-state [_ {:keys [chosen]}]
   (html 
    [:div
     (om/build flag-view (first (filter #(= (:name %) chosen) flags)))
     [:div [:select {:on-change #(set-key! (.. % -target -value) owner)}
            (om/build-all selector flags {:key :name})]]])))
```

![image](https://qiita-image-store.s3.amazonaws.com/0/59864/57f8e39b-94ee-0914-a249-6419ef6d1c35.png)


flag viewに渡すオブジェクトを変更するだけで，React側が新旧の状態を見比べて差分を計算し，必要な箇所だけを更新してくれます．