Edited at

Om(React)でsvgを描画する

More than 1 year has passed since last update.


Omとは

facebook製 javascript frameworkであるReactのclojurescript wrapper ライブラリです.

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

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

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


OmでSVGを描画する

上述のように,Om(React)を用いると「構造」と「内容」を分離することができるので,SVGの構造を簡潔に書くことができます.簡単なサンプルを用意してみました.Omが初めての方は公式チュートリアルも読んでみてください.


環境


  • OSX

  • Leiningen 2.7.0 on Java 1.8.0_101


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


Omプロジェクト作成

まず,公式チュートリアルに従って

lein new figwheel svg-test -- --om

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



(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が正常に動いているなら

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


sablonoを適用

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

(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を適用して書き換えます.

(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に,国名と旗の色をもたせます.

(defonce app-state (atom {:flags [{:name :germany :color ["#1D1D1F" "#E71920" "#FAB40A"]}

{:name :bulgaria :color ["#FFFFFF" "#00AE0F" "#E60000"]}
{:name :luxembourg :color ["#EF3340" "#00A3E0" "#FFCD00"]}]}))

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

(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情報が流し込まれ,ドイツ国旗が表示されます.

(defcomponent application [{:keys [flags]} owner]

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

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

(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})]]])))

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