riotを通してfluxを理解してみようと思い、自分なりにですが勉強しまして、
そこで得た知識を、改めてまとめてみた、そんな記事となっています。
- Riotでfluxを学ぶ ToDo編①
- Riotでfluxを学ぶ ToDo編②(本記事)
- Riotでfluxを学ぶ ToDo編③
- Riotとfluxでカウンターとカレンダーを試す
それでは今回は、
riot.observableを用い、前回作ったToDoリストを書き直してみたいと思います。
riot.observable を使った設計
まず、riot.observable って何?何ができるの?
って人のために簡単に説明しますと、
- 任意のイベントを作れる
- その任意に作ったイベントを、発生させる事ができる
- その任意に作ったイベントの発生時に、任意の処理を実行させる事ができる
- そんな機能を持つオブジェクトを作れる
コード見た方が理解が速いかもしれません。
下のようにイベントの発生と監視を行うことができます。
// イベント発生させる
observableなオブジェクト.trigger(任意のイベント, 引数)
// イベント発生時に、任意の処理を実行させる
observableなオブジェクト.on(任意のイベント, (引数)=>任意の処理)
このように、
observableなオブジェクトを通して、任意のイベントが作れ、
そのイベントの発火と監視&処理を、自在に行う事ができるようになるのです。
それで、何が便利になるのか?
オブジェクトを通すことで、異なるコンポネント間であってもイベントの発生を検知することが出来るようになります。
つまり遠く離れた場所であっても、必要なタイミングで必要な処理を実行する事が、容易に可能となるのです。
今回のToDoアプリでは、
- ToDoの一覧データを管理するToDoクラス
- ToDoの入力インターフェイスを管理するInputクラス
の2つのクラスを、observableで作ます。
そして、この両クラスはプロパティ値に変更があったタイミングで必ず「changed」というイベントを発火することにします。
これでどこからでも「changed」を監視することができ、データ更新のタイミングで任意の処理を挟むことができるようになります。
前回のToDoリストとの仕様の違い
- 重要度を追加
- 完了済みのタスクを消す機能を追加
- ToDoが入力されないと「追加ボタン」がクリックできない機能を追加
これらの機能を追加した分、コードも若干長く、一見複雑になってしまってます
(あとCSSも増えたし、SASSで書いててランタイムでコンパイルしてますし)
observableを使った書き方に直したから、
「長く複雑そうになったのではない」という事をここで断っておきたいです
これらの機能を追加したのは、今回説明する仕組みを用いれば、
機能の追加が簡単だ、ってことを知ってもらいたかったからです。
カスタムタグ部分
<todo></todo>
<script type="riot/tag">
<todo>
<h1>TODO - observableを使ってみる</h1>
<form onsubmit="{addTask}">
<input value="{task}" onkeyup="{inputting}" placeholder="新規タスクを入力">
<select onchange="{changePriority}">
<option value="low" selected="{priority==='low' }">優先度:低</option>
<option value="mid" selected="{priority==='mid' }">優先度:中</option>
<option value="high" selected="{priority==='high'}">優先度:高</option>
</select>
<button disabled="{task===''}">追加</button>
</form>
<ul>
<li class="{is-done: task.done}" each="{task, i in tasks}" onclick="{toggleTask.bind(this, i)}">
<input type="checkbox" checked="{task.done}"/><span class="icon is-high" if="{task.priority==='high'}">高</span><span class="icon is-mid" if="{task.priority==='mid'}">
</li>
</ul>
<div>
<button disabled="{!hasCompletedTasks}" onclick="{clearCompletedTasks}">完了したタスクを削除</button>
</div>
<style type="sass">
.is-done {
text-decoration: line-through;
color: #ccc;
.icon {
&.is-high, &.is-mid, &.is-low {
background: #ccc;
}
}
}
.icon {
color:white;
display: inline-block;
margin: 0 5px;
padding: 0 3px;
&.is-high{ background: #F66; }
&.is-mid { background: #CC6; }
&.is-low { background: #66F; }
}
</style>
// 宣言と初期化
this.input = opts.input;
this.todo = opts.todo;
this.task = ""
this.priority = ""
this.tasks = []
this.hasCompletedTasks = false;
this.on("mount", function(){
this.input.init()
this.todo.addTask(
{title:"riotで何か作ってみる", priority:"mid", done:true},
{title:"fluxを実践してみる", priority:"low", done:false},
);
})
// DOMイベント関数(モデル操作)
inputting(e) {
this.input.text = e.target.value
}
changePriority(e) {
this.input.priority = e.target.value
}
addTask(e) {
e.preventDefault()
this.todo.addTask({title:this.task, priority:this.priority, done:false});
}
toggleTask(i, e) {
this.todo.toggle(i);
}
clearCompletedTasks(e) {
this.todo.clearCompletedTasks();
}
// 監視&ビュー操作
this.input.on("changed", (text, priority) => {
this.update({
task : text,
priority: priority,
});
});
this.todo.on("changed", (tasks, hasCompletedTasks) => {
this.tasks = tasks
this.hasCompletedTasks = hasCompletedTasks
this.input.init()
this.update()
});
</todo>
</script>
コード解説
最初の処理の流れのとこだけ説明しときます。
- observable化した、InputとTodoクラスのインスタンスが引数として渡されてきてますので、それをthis.input(以降Input)とthis.todo(以降ToDo)に格納してます。
- 次に、このtodoタグのプロパティとして、this.task、this.priority、this.tasks、this.hasCompletedTasks を宣言し、読み書きが行えるようにしてます。
- on("mount")では、InputとToDoの初期値を、それぞれが持つメソッドで設定してます。
- この時この両インスタンスは、自らの値が書き変わったので「change」イベントを発火します。
- Inputはtextとpriorty、ToDoはtasksが書き換わりました。
- this.input.on("changed", (text, priority) => {... という箇所で、Inputのchangedを監視しています。
- 書き換わった値textとpriorityが引数として渡ってきていますので、それぞれをriotタグのプロパティ、this.text、this.priorityに入れつつ、this.update()してviewを更新しています。
- this.todo.on("changed", (tasks, hasCompletedTasks) => {... という箇所で、ToDoのchangedを監視しています。
- 書き換わった値tasksとhasCompletedTasksが引数として渡ってきていますので、それぞれriotタグのプロパティであるthis.tasks、this.hasCompletedTasksに格納し、this.update()しています。
- 飛ばしましたが、Input.init()もしています。Inputを初期化することで、changedが発火され、監視に登録されている処理が実行されます。
- この場合、何らかのToDoが入力されていたのが、Input.init()によって空文字に変更されchanged発火。監視を通して、this.inputに渡り、this.update()で入力されたToDoは空文字へと書き変わり、削除された事になります。
残りの、inputting()、changePriority()、toggleTask()、clearCompletedTasks()も、全て同じ仕組みで実装されているので細かい説明は省きます。
まとめて説明しますと、
onclickなどのDOMイベントのタイミングで、Input、ToDoに両クラスが持つメソッドからデータ操作を行い、両クラスはchangedを発火→監視によりビューが更新される
全てこの流れです。
マウント部分
<script src="https://cdn.jsdelivr.net/npm/riot@3.6.0/riot+compiler.min.js"></script>
<script src="https://cdn.rawgit.com/medialize/sass.js/v0.6.3/dist/sass.js"></script>
<script>
// riotタグ内でSASSが使えるようにするための処理
riot.parsers.css.sass = function(tagName, css) {
var result = Sass.compile(css);
return result;
};
// Inputクラスを定義
class Input {
DEFAULT_TEXT() { return "" }
DEFAULT_PRIO() { return "mid" }
get text() { return this._text }
set text(text) { this._text = text; this.changed() }
get priority() { return this._priority }
set priority(priority) { this._priority = priority; this.changed() }
constructor() {
riot.observable(this)
this._text = this.DEFAULT_TEXT();
this._priority = this.DEFAULT_PRIO();
}
changed() { //console.log(this.text)
this.trigger("changed", this.text, this.priority)
}
init() {
this._text = this.DEFAULT_TEXT();
this._priority = this.DEFAULT_PRIO();
this.changed();
}
}
// Todoクラスを定義
class Todo {
get tasks() { return this._tasks; }
constructor() {
riot.observable(this)
this._tasks = [];
}
changed() { //console.log(this.tasks)
this.trigger("changed", this.tasks, this.hasCompletedTasks())
}
addTask(...tasks) { //console.log(tasks)
for ( let task of tasks ) {
this._tasks.push(task);
}
this.changed();
}
toggle(i) {
let task = this._tasks[i];
if ( task ) {
task.done = !task.done;
this.changed();
}
}
clearCompletedTasks() {
let tasks = this.tasks.filter(function(task, i){
return !task.done
});
this._tasks = tasks;
this.changed();
}
hasCompletedTasks() {
let completed = this.tasks.some(function(task, i){
return task.done
});
return completed;
}
}
// InputとTodoのインスタンス生成
let todo = new Todo();
let input = new Input();
//todo.addTask({title:"Test",done:false}, {title:"Hoge", done:false});
// riotタグをマウント
riot.mount("todo", {todo:todo, input:input});
</script>
こちらも長いですが、やってる事は下記の2つです。
- ToDoクラス、Inputクラスの作成とインスタンス生成。
- riot.mount()でカスタムタグに両クラスを渡す。
改めて見てみてください。
それほど難しくないですよね?
クラス設計について
-
Inputクラス
- プロパティ
- text
- priority
- メソッド
- init()
- プロパティ
-
ToDoクラス
- プロパティ
- tasks
- hasCompletedTasks
- メソッド
- addTask
- toggle
- clearCompletedTasks
- プロパティ
ぞれぞれ、observable化されているので、triggerとonを実装していて、
プロパティの更新時には、changedを発火させる処理も実装済みです。
クラス設計する時は、InputまたはToDoというオブジェクトが、どんなプロパティを持ち、どんなメソッドが必要か、
を、純粋に考れば良いのです。
アプリ設計についての解説
まず、今回のコードでは、
InputとToDoのオブジェクトを操作する事が、DOMイベントで実行されるコールバック関数内の処理の中心になっているという事に注目して欲しいです。
次に、表示に関連する操作が全て、on(changed)で監視している箇所に集約されているという点にも目を見張って頂きたいです。
モデルとストア
InputとTodoの両クラスは、一般的にモデルと呼ばれるものに相当します。
上でも言いましたが今回の書き方で、
DOMイベント時のコールバック関数内では、このモデルに対しての操作だけを、行うようにしています。
例えば「追加」というボタンが押されたら、ToDoモデルにaddTask()を使ってToDoを追加してあげるだけ、といった具合です。
今までのように、このコールバック関数内でビューを操作することは、一切不要です。
ビューへのデータ反映は、on(changed)の監視が、よしなにやってくれます。
DOMイベントのコールバック関数がやるべき仕事は、モデルを操作することだけで、
極端な話、この後更新されたデータがビューに反映されようがされまいが、ここでは知ったことではないのです。
っで、
このモデルは、on/trigger、changedeを発火するという特殊なイターフェイスを備えたモデルです。
この特別なモデルは、ストアと呼ばれます。
ビュー
更新されたデータを取得し、表示し直すのが、ビューの役割となります。
observableを用いることで、
ビューは、ストアの更新を監視し、更新があったら最新のデータを取得し、その最新のデータで自らを再描写する事が、容易に可能となります。
これで要件は満たされ、
ビューは、どこからでも、誰からでも、書き換えや更新が行われる存在ではなく、
自らが更新を検知し、自らを再描写する存在となりました。
受動的なものなのではなく、能動的なものへと変わり、
他所から、つどつど面倒を見てあげる必要がなくなったのです。
まとめ
observableを導入することで、意味のある処理を1つ場所にまとめ、分離し、その役割に名前をつける事ができました。
秩序が生まれた瞬間です。
そして、fluxを構成する、4要素のうち、ストア、ビューの2つが登場しました。
まだ見ぬアクション、ディスパッチャとは一体なんなのか?
一体何をやってくれ、何が便利になるのか?
次回はその辺りを言及していきたいと思います。
fluxへの道はもう近い。