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側が新旧の状態を見比べて差分を計算し,必要な箇所だけを更新してくれます.