2013年6月14日金曜日

Playing with Dart Web UI again

最後にWeb UIを触ってからしばらく経って、Dart自体のライブラリやらも色々と大きな変更があったようなのでまた久しぶりにWeb UIまわりを触ってみた。作ったものは前回とおなじというか、それを修正・拡張したもの。使ったのはDart Editor version 0.5.16_r23799とWeb UI version 0.4.11+1で、できあがったサンプルはここ

できたもの

動物の名前と色をリストで表示するだけのもの。


表示した直後

スライダー動かすと色も変わる

前と比べてどう変わったか

前やったときにはできなかった(もしくはめんどくさそうでやらなかった)ことがいくつかできるようになってた。具体的には以下の2つ。

  • 子のコンポーネントと親のコンポーネントをイベントでつなげられるようになった

    前試したときはちょうどStreams APIまわりができたばっかりくらいのタイミングで、それをどうカスタムなイベントに転用すればいいかよくわからなかった。その後情報がいくらか整備されてEventStreamProviderとCustomEvent使えばできるようになることがわかった、ていう感じ。個人的にこれは結構大きい。

  • input type="range"とのバインドができるようになった

    前ためしたときは確かinput type="range"とのバインドはうまく動かなかったけど、今回試したら普通に動いてた。

大きな変更があったのはanimal-summaryとcolor-editorの中だけなので、そこだけ解説する。

animal-summary

<!doctype html>
<html>
    <head>
        <link rel="import" href="color-editor.html">
    </head>
    <body>
        <element name="animal-summary" constructor="AnimalSummaryViewModel" extends="div">
            <template>
                <div>{{header}}</div>
                <color-editor id="pallet" model="{{model.color}}"></color-editor>
                <canvas id="summary" width="300" height="200"></canvas>
            </template>
            <script type="application/dart">
            import 'dart:json' as json;
            import 'package:web_ui/web_ui.dart';
            import 'package:dart_web_ui_sample/src/AnimalSummary.dart';
            import 'package:dart_web_ui_sample/src/Color.dart';

            class AnimalSummaryViewModel extends WebComponent {

                AnimalSummaryViewModel() {
                }

                String get header => "${model.name}:${model.count}";

                void increment(e) {
                    model.count++;
                }

                void created() {
                }

                void inserted() {
                    _initializeCanvas(model.color);

                    var p = query("#pallet");
                    p.xtag.onColorChanged.listen((event) {
                        // sync canvas color with the editor color
                        String str = event.detail;
                        var obj = json.parse(str);
                        Color c = new Color(obj["r"], obj["g"], obj["b"]);
                        _initializeCanvas(c);
                    });
                }

                void _initializeCanvas(Color color) {
                    var c = query("#summary");
                    var ctx = c.getContext('2d');
                    ctx.fillStyle = color.cssString;
                    ctx.fillRect(0,0,300,200);
                }

                AnimalSummary model;
            }
            </script>
        </element>
    </body>
</html>

前と変わったところで重要なのはinserted()の中。inserted()はanimal-summaryがDOMに追加された段階で呼ばれてくるんだけど、上のコードではその段階でcolor-editorが独自に定義したonColorChangedイベントにリスナーを登録してる。これの意図はcolor-editorの中で色に変更が加わったら、それをanimal-summary側で察知して表示されてる色を更新したい、というもの。

途中であらわれる謎のxtagというプロパティはこの辺に記述がある。queryから返ってきたElementはただのdivなので、自分で独自に定義したonColorChangedにアクセスするにはxtagを経由して実際のWeb Componentインスタンスにアクセスしないといけない、らしい。

ちなみに、そのうちinsertedとかはなくなるかも

color-editor

<!doctype html>
<html>
    <body>
        <element name="color-editor" constructor="ColorViewModel" extends="div">
            <template>
                <ul>
                    <li>R:<input type="range" bind-value="red" min="0" max="255" ><input type="text" readonly bind-value="red"></li>
                    <li>G:<input type="range" bind-value="green" min="0" max="255" ><input type="text" readonly bind-value="green"></li>
                    <li>B:<input type="range" bind-value="blue" min="0" max="255" ><input type="text" readonly bind-value="blue"></li>
                </ul>
            </template>
            <script type="application/dart">
            import 'dart:html';
            import 'dart:async';
            import 'dart:json' as json;
            import 'package:web_ui/web_ui.dart';
            import 'package:dart_web_ui_sample/src/Color.dart';

            class ColorViewModel extends WebComponent {
                ColorViewModel() {
                }

                static const EventStreamProvider<CustomEvent> colorChangedEvent = 
                    const EventStreamProvider<CustomEvent>('colorChanged');

                Stream<CustomEvent> get onColorChanged => colorChangedEvent.forTarget(this);

                String get red => "${model.red}";
                set red(String r) {
                    int nr = int.parse(r);
                    if (model.red == nr) {
                        return;
                    }
                    model.red = nr;
                    _notifyColorChange(model);
                }

                String get green => "${model.green}";
                set green(String g) {
                    int ng = int.parse(g);
                    if (model.green == ng) {
                        return;
                    }
                    model.green = ng; 
                    _notifyColorChange(model);
                }

                String get blue => "${model.blue}";
                set blue(String b) {
                    int nb = int.parse(b);
                    if (model.blue == nb) {
                        return;
                    }
                    model.blue = nb;
                    _notifyColorChange(model);
                }

                void created() {
                }

                void inserted() {
                }

                void _notifyColorChange(Color newColor) {
                    var str = json.stringify(newColor);
                    CustomEvent ev = new CustomEvent('colorChanged', canBubble: true, cancelable: true, detail: str);
                    dispatchEvent(ev);
                }

                Color model;
            }
            </script>
        </element>
    </body>
</html>

こっちで重要なのはEventStreamProviderとCustomEventでカスタムなイベントを定義してるところと、notifyColorChangeで実際にイベントを発行してるところ。

定義するところはもう作法が決まってるので、作法にのっとって書けば大丈夫。個人的にこの辺はWPFでDependencyPropertyを定義するときと少し似てるなーと思った。イベントを発行するところも大体作法通りなんだけど、1つ注意が必要なのはdetailの引数を渡すところ。これはドキュメントを見るとObject型を受け付けてるんだけど、実際は勝手に文字列に変換されるっぽい。最初インスタンスをそのまま渡したらリスナー側で例外が飛んで困った。そのため、今は先にこちらでjson文字列に変えてから渡してる。もちろんリスナー側でもjson文字列が来ることを前提でparseしてる。

なお、Colorをjson化するためColor側ではtoJson()ていう専用の関数を定義している。stringifyの中でそのtoJsonが呼ばれてるっぽい。

感想

しばらく放置してた間に色々変化があったようで、当初やりたかったことができるようになってきた印象。もう少し遊んでみるかも。

0 件のコメント:

コメントを投稿