Observer(オブザーバー)の勉強として、チェックした要素の数だけカウントする動きを実装したのでメモ。
例としてイン●タグラム風のページを用意しました(笑)。
「いいね」や「ブックマーク」のアイコンをチェックすると、実行結果のアイコンがアニメーションし、チェックした総数をカウントします。
今回SNS風のページに実装しましたが、どちらかというとフォームやゲーム等で使う機会がありそうな動きです。
完成したページを確認する(デモ)
実際の動きはこちらで確認できます。(新しいタブが開きます)
ソースコードを確認
上記のデモページから、カウントに必要な部分を抜き出したコードがこちらです。
今回はJavascriptのコードのみ抜き出しました。
長いコードなので、忘れそうなポイントだけメモしておきます。。
全体
"use strict";
// イベント監視
const snsStack = (function() {
// イベントを登録
let stackfn = new Map();
return {
// イベント追加
add: function(type,fn){
const fnStack = stackfn.get(type) || new Set();
fnStack.add(fn);
stackfn.set(type,fnStack);
},
// イベント実行
emit: function(type, targetElm, _this){
const fnStack = stackfn.get(type);
if(fnStack) {
for(let item of fnStack) {
item.call(_this,targetElm);
}
}
},
//SNSの種類
snsNames:{
like:0,
comment:0,
send:0,
bookmark:0
}
}
})();
// イベントを通知
class SnsView {
constructor(el) {
this.targetEml = el;
this.initialize();
this.handleEvents();
}
initialize() {
// 関数登録
snsStack.add('elmChange',this.iconChange);
snsStack.add('elmChange',this.noticAppear);
}
handleEvents() {
const self = this;
// 関数実行
this.targetEml.addEventListener('click', function() {
snsStack.emit('elmChange',this,self);
});
}
// SNSアイコンのオンオフ
iconChange(targetElm) {
const classValue = targetElm.classList;
let toggle;
if(classValue.value.match('far')) {
classValue.replace('far','fas');
toggle = true;
} else if(classValue.value.match('fas')) {
classValue.replace('fas','far');
toggle = false;
}
// アイコンのオンオフを実行結果にカウント
this.countChange(targetElm,toggle);
}
// アイコンのオンオフを実行結果にカウント
countChange(targetEml,toggle) {
for(let snsName in snsStack.snsNames) {
const snsData = targetEml.dataset.snsGenre;
if(snsData.match(snsName)) {
const count_area = document.querySelector(`.${snsName}-count`);
let snsCount = this.count(toggle);
count_area.textContent = snsCount(snsName);
}
}
}
// カウントの増減
count(toggle) {
return function(snsName) {
let countResult = toggle ? ++snsStack.snsNames[snsName] : --snsStack.snsNames[snsName];
return countResult;
}
}
// アイコンアニメーション
noticAppear(targetEml) {
for(let snsName in snsStack.snsNames) {
const snsData = targetEml.dataset.snsGenre;
if(snsData.match(snsName)) {
const count_area = document.querySelector(`.${snsName}-btn`);
count_area.classList.add('animete-btn');
setTimeout(function() {
count_area.classList.remove('animete-btn');
},1000)
}
}
}
}
// イベント設定
document.querySelectorAll('.sns-btn').forEach(function(snsBtn) {
new SnsView(snsBtn);
});
Observer(オブザーバー)とは
Observerは対象の状態の変化を監視するデザインパーターンの一種です。
そのもそもデザインパターンとはなんぞや?ですが、よく使われるプログラムの設計をパターン化して名前をつけたものらしいです。
Observerがどんなパターンかというと、まず監視者と通知者という存在があり、さらにAという監視対象があるとします。
監視対象Aの状態が変化したら、通知者が監視者に「Aの状態が変化したよー。」と教えてあげます。
そして監視者は事前に登録されていたイベント(関数)を実行します。
正直、抽象的すぎて調べても良く分かりませんでしたww
とりあえず、今回のサンプルでは投稿のSNSアイコンが監視対象です。
監視者と通知者は、次のオブジェクト名になります。
イベント名 | |
snsStack(監視者) | イベントを監視するオブジェクト。 SNSアイコンがON/OFFになったら、事前に登録された関数を実行する |
SnsView(通知者) | イベントを通知するオブジェクト SNSアイコンがON/OFFになったら、監視者に通知する |
今回は、投稿の「いいね」アイコンがONの状態になったら次のような動きをします。
動き自体は関数にまとめます。これを監視者に事前に登録します。
- クリックしたアイコンの色が反転する
- 実行結果のカウント数が増減する
- 実行結果のアイコンがアニメーションする
イベントを監視するオブジェクト
// イベント監視
const snsStack = (function() {
// 関数を登録
let stackfn = new Map();
return {
// 関数追加
add: function(type,fn){
const fnStack = stackfn.get(type) || new Set();
fnStack.add(fn);
stackfn.set(type,fnStack);
},
// 関数実行
emit: function(type, targetElm, _this){
const fnStack = stackfn.get(type);
if(fnStack) {
for(let item of fnStack) {
item.call(_this,targetElm);
}
}
},
//SNSの種類
snsNames:{
like:0,
comment:0,
send:0,
bookmark:0
}
}
})();
こちらが対象を監視するオブジェクトです。通知を受け取り次第、関数の登録&実行を行います。
あと、SNSボタンのカウント数を管理するオブジェクトも設定しました。
関数を登録
// 関数を登録
let stackfn = new Map();
// 関数追加
add: function(type,fn){
const fnStack = stackfn.get(type) || new Set();
fnStack.add(fn);
stackfn.set(type,fnStack);
}
キーワードとイベントのセットをMapオブジェクトで管理します。キーワードごとに関数を管理するのは、将来追加する関数を想定しています。
関数は配列ではなくSetオブジェクトで管理します。Setオブジェクトだと重複イベントが登録されないのが理由です。
関数が何も登録されていなかったら変数fnStackにSetオブジェクトを代入します。
//Setオブジェクトにイベントを追加
const fnStack = stackfn.get(type) || new Set();
以下のコードでMapオブジェクトにキーワードとイベントを追加します。
//Mapオブジェクトにキーワードとイベントを設定
stackfn.set(type,fnStack);
関数を実行
// 関数実行
emit: function(type, targetElm, _this){
const fnStack = stackfn.get(type);
if(fnStack) {
for(let item of fnStack) {
item.call(_this,targetElm);
}
}
}
ここでは、先ほどMapに登録した関数を実行します。キーワードが存在すればコールバック関数で実行します。
その時、関数に渡される引数は次の通りです。
type | Mapに登録されたキーワード |
targetElm | SNSアイコンの要素(監視対象) |
_this | SnsView(通知者) |
SnsView(通知者)のオブジェクトを引数で渡すことで、通知者側に登録されている関数を使用できます。使用するときはコールバック関数のthisをSnsView(通知者)のオブジェクトにします。
また、コールバック関数にSNSアイコンの要素(監視対象)を引数として渡します。
要素に設定したデータ属性を使うのが目的です。
イベントを通知するオブジェクト
// イベントを通知
class SnsView {
// 初期化
constructor(el) {
this.targetEml = el;
this.initialize();
this.handleEvents();
}
initialize() {
// 関数登録
snsStack.add('elmChange',this.iconChange);
snsStack.add('elmChange',this.noticAppear);
}
handleEvents() {
const self = this;
// 関数実行
this.targetEml.addEventListener('click', function() {
snsStack.emit('elmChange',this,self);
});
}
// SNSアイコンのオンオフ
iconChange(targetElm) {
const classValue = targetElm.classList;
let toggle;
if(classValue.value.match('far')) {
classValue.replace('far','fas');
toggle = true;
} else if(classValue.value.match('fas')) {
classValue.replace('fas','far');
toggle = false;
}
// アイコンのオンオフを実行結果にカウント
this.countChange(targetElm,toggle);
}
// アイコンアニメーション
noticAppear(targetEml) {
for(let snsName in snsStack.snsNames) {
const snsData = targetEml.dataset.snsGenre;
if(snsData.match(snsName)) {
const count_area = document.querySelector(`.${snsName}-btn`);
count_area.classList.add('animete-btn');
setTimeout(function() {
count_area.classList.remove('animete-btn');
},1000)
}
}
}
}
初期化constructorの部分で引き渡される値elは監視対象のSNSアイコンの要素です。
関数の登録はinitialize()、実行はhandleEvents()にまとめました。
登録する関数を監視者に通知する
initialize()の関数実行で、snsStack(監視者)に登録する関数を設定します。
今回、2つの関数を設定しています。
initialize() {
// 関数登録
snsStack.add('elmChange',this.iconChange);
snsStack.add('elmChange',this.noticAppear);
}
監視対象の状態を監視者に通知する
handleEvents()の関数実行では、クリックイベントが設定されています。
このクリックイベントで、アイコンのON、OFFの状態をsnsStack(監視者)に通知します。
handleEvents() {
const self = this;
// 関数実行
this.targetEml.addEventListener('click', function() {
snsStack.emit('elmChange',this,self);
});
}
登録する関数の詳細
snsStack(監視者)に登録する関数は、SnsView(通知者)側で管理しました。
具体的にはこの動きです。
- クリックしたアイコンの色が反転する
- 実行結果のカウント数が増減する
- 実行結果のアイコンがアニメーションする
こちらが「クリックしたアイコンの色が反転する」関数です。
「実行結果のカウント数が増減する」の動きも含めるとコードが長くなりそうなので、別の関数で管理しました。
// SNSアイコンのオンオフ
iconChange(targetElm) {
const classValue = targetElm.classList;
let toggle;
if(classValue.value.match('far')) {
classValue.replace('far','fas');
toggle = true;
} else if(classValue.value.match('fas')) {
classValue.replace('fas','far');
toggle = false;
}
// アイコンのオンオフを実行結果にカウント
this.countChange(targetElm,toggle);
}
クリックされた監視対象のCSSのクラス名を変更して、アイコンの色を反転させています。
ON/OFFの動きを実装するというモノです。
最後に記述しているcountChangeは「実行結果のカウント数が増減する」処理をまとめた関数です。
こちらが「実行結果のアイコンがアニメーションする」関数です。
// アイコンアニメーション
noticAppear(targetEml) {
for(let snsName in snsStack.snsNames) {
const snsData = targetEml.dataset.snsGenre;
if(snsData.match(snsName)) {
const count_area = document.querySelector(`.${snsName}-btn`);
count_area.classList.add('animete-btn');
setTimeout(function() {
count_area.classList.remove('animete-btn');
},1000)
}
}
}
詳しいことは省略しますが、HTMLに設定されたデータ属性を取得し、同じSNSジャンルのアイコンだったらアニメーションするというものです。
これらの関数をsnsStack(監視者)に登録します。
SnsViewオブジェクトをインスタンス化する
最後に、SnsView(通知者)オブジェクトをインスタンス化します。
インスタンス化の際、SNSアイコンの要素を引数として渡します。
// イベント設定
document.querySelectorAll('.sns-btn').forEach(function(snsBtn) {
new SnsView(snsBtn);
});
以上で完成となります!
分からなかったところ
なんとか希望通りに動くものが作成できましたが、よく分からなかった点も。。
それは、snsStack(監視者)に登録する関数はどこにまとめればよかったのか。。です。
具体的にはこの動きをまとめた関数ですね。
- クリックしたアイコンの色が反転する
- 実行結果のカウント数が増減する
- 実行結果のアイコンがアニメーションする
今回、通知側のオブジェクトのメソッドとしてまとめましたが、監視側で関数を実行する時の処理が煩雑になった気がします。
普通に関数で書いた方がコードが見やすかったような気が?
また、SNSのカウント数を管理するオブジェクトはsnsStack(監視者)のオブジェクトに含めました。これもどこに記述すれば読みやすいかよく分からなかったです。。
まとめ
分からなかった点は複数ありますが、Observer(オブザーバー)を使えば、動きのあるウェブページに応用できそうです。