チェックした要素の数をそれぞれカウントする動きを実装しました【javascript/Observer】

JavaScript

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に登録した関数を実行します。キーワードが存在すればコールバック関数で実行します。

その時、関数に渡される引数は次の通りです。

typeMapに登録されたキーワード
targetElmSNSアイコンの要素(監視対象)
_thisSnsView(通知者)

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(オブザーバー)を使えば、動きのあるウェブページに応用できそうです。