それでは、プロトタイプを利用したクラスについて少し学んだ所で、身近な機能について実装してみましょう。
左右にドアが開くアニメーションを実装してみる
ところが、学んだことを実際の課題で利用するというのが最大のハードルです。
いくら自己啓発本を読んだとしても、実際の生活で活かすのは難しいものです。
クラスの実装は、簡単なアニメーションなどでも応用することが可能です。
ここではページを開いたら、左と右にパネルが時間差で移動する簡単なアニメーションを実装してみましょう。
完成品
https://codepen.io/1shiharaT/pen/NWPZPKQ
See the Pen 研修_ドアアニメーションデモ by 1shiharaT (@1shiharaT) on CodePen.
まずはHTMLとCSSから
HTMLは、左と右にパネルが移動するので、単純なHTMLで問題なさそうです。
FLOCSSで書いてみます。
<div class="js-introduction"> <div class="js-introduction__left"></div> <div class="js-introduction__right"></div> </div>
CSS左と右にパネルを配置して、それぞれ is-open というクラスを当てたら左右に移動するようにしてみます。
.js-introduction { width: 100vw; height: 100vh; position: fixed; top: 0; left: 0; z-index: 100; &.is-loaded { display: none; } &__left { position: absolute; left: 0; width: 50vw; height: 100vw; background: #000; transition: all ease .5s; &.is-open { left: -50vw; transition: all ease .5s; } } &__right { position: absolute; right: 0; width: 50vw; height: 100vw; background: #ccc; transition: all ease .5s; &.is-open { right: -50vw; transition: all ease .5s; } } } .js-introduction-reset { display: block; margin-left: auto; margin-right: auto; width: 300px; background: #224555; color: #fff; padding: 10px 24px; font-size: 18px; border: none; text-decoration: none; text-align: center; margin-top: 45vh; }
上記は完成形のCSSを記述していますが、実際の実装時は後述のJavaScriptと行ったり来たりしながら実装します。
主な仕様
- アニメーションを実行する要素は .js-introduction
- .js-introduction 内にはそれぞれ左右に開く .js-introduction__left、.js-introduction__right という要素を配置
- .js-introduction__left、.js-introduction__right は is-open という class が付与されるとアニメーションが実行される
JavaScriptを記述してみる
さて本題のJavaScriptの実装です。
今回はせっかくなのでバニラJS(jQuery等を利用しない素のJavaScript)で書いてみます。
1. まずはコンストラクタから実装する
とりあえずコンストラクタを定義します。
コンストラクタって何?という方は、前回の記事を参照ください。
new 演算子をつけて呼び出した時に最初に実行される関数です。
// コンストラクタ function Introduction() { } // jQueryの $(function(){]) と同じような意味合いのイベント // DOMが生成された後に実行 addEventListener("DOMContentLoaded", function () { var introInstance = new Introduction(); });
Introduction という関数を定義し、new 演算子をつけることで introInstance は Introductionのインスタンスとして生成されます。つまり、Introductionの prototype を継承します。
2. 設定値を考えてみよう
さて、期待するアニメーションを実装するのにどんな情報が必要になるでしょうか?
要件を満たすために必要な情報
- アニメーションを実行するDOM要素
- それを取得するのに必要なセレクタ
- 左のドアパネルのDOM要素
- 右のドアパネルのDOM要素
- 左のドアが開いてから、右のドアが開くまでの時間
- 左右のドアにアニメーションを実行する時に付与するclass
とはいえいきなりすべてを事前に考えきるのは難しいものです。これも実装を進める中で「これは設定値としてクラスの中で参照できるように this に格納した方がいいのではないか」という考えから後付で設定することも多々あります。
ポイントとしては、【1回以上参照する可能性があるか】【後から変更する確率が高いか】です。
検討した設定値をコンストラクタ内に this のプロパティとして定義します。
そうすることで、今後定義するクラスのprototypeに紐付けたメソッド内で、簡単に参照することができます。
function Introduction() { this.options = { // セレクタ targetClassName: ".js-introduction", // 左のドアが開いてから右のドアがあくまでの時間 openDelay: 1000, // 開いているドアに付与するclass名 doorActiveClassName: 'is-open' } // domを取得しセット this.el = document.querySelector(this.options.targetClassName); // 左、右のドアを格納 this.elDoors = { left: this.el.querySelector(this.options.targetClassName + "__left"), right: this.el.querySelector(this.options.targetClassName + "__right") } return this; } addEventListener("DOMContentLoaded", function () { var introInstance = new Introduction(); });
これで、このアニメーションで利用する値がどこで定義されているか一目瞭然です。
3. 機能を考えてみる
次は、アニメーションを実装する上で必要になりそうな機能を考えてみます。
左右のドアが時間差で移動するというシンプルなアニメーションなので、次の機能があれば実装できそうです。
- ドアを開く機能 == is-open というclassを付与する機能
function Introduction() { this.options = { // セレクタ targetClassName: ".js-introduction", // 左のドアが開いてから右のドアがあくまでの時間 openDelay: 1000, // 開いているドアに付与するclass名 doorActiveClassName: 'is-open' } // domを取得しセット this.el = document.querySelector(this.options.targetClassName); // 左、右のドアを格納 this.elDoors = { left: this.el.querySelector(this.options.targetClassName + "__left"), right: this.el.querySelector(this.options.targetClassName + "__right") } return this; } // ドアを開くメソッドの定義 Introduction.prototype.start = function(){ // 注意 : setTimeoutやイベントなど、コールバックを指定する場合、スコープが区切られるため // 変数等に代入する必要があります。 var self = this; setTimeout(function(){ self.elDoors.left.classList("is-open"); setTimeout(function(){ self.elDoors.left.classList("is-open"); }, 1000); }, 1000); } addEventListener("DOMContentLoaded", function () { var introInstance = new Introduction(); // 呼び出す introInstance.start(); });
ここで注意したのが、メソッド内で関数を定義する時です。
関数を定義するとスコープが区切られるため、関数内でthisの参照先がwindowになってしまいます。
これではせっかく洗い出してthisに格納した値が取得できないので、selfといった変数にthisを代入し、間接的に参照するようにします。
※ ES6等ではアロー関数 ( () => {} )という、thisを束縛しない仕組みが追加されています。
4. メソッドを切り分けてみる
さて、晴れてアニメーション自体は実装することができましたが、まだ改善の余地があります。
start メソッドでは、次の似たようなコードが2回呼び出されています。
self.elDoors.left.classList("is-open"); self.elDoors.right.classList("is-open");
DRY (Don’t Repeat Yourself : 一度書いたことは二度と書かない)の精神で、上記のコードを別のメソッドとしてまとめてみます。
~~~~略~~~~ Introduction.prototype.start = function(){ var self = this; setTimeout(function(){ self.open("left") setTimeout(function(){ self.open("right") }, 1000); }, 1000); } /** * ドアを開く * @param string target left or right * @return this */ Introduction.prototype.open = function (target) { if (typeof this.elDoors[target] !== "undefined") { this.elDoors[target].classList.add("is-open"); } return this; } ~~~~略~~~~
新たに open というメソッドを定義し、this.elDoorsというプロパティに引数に渡された値のプロパティがあったら、is-openというclass属性を追加。start メソッドでは、open メソッドを呼び出す形に変えています。
一見、さほど変わらないかもしれませんが、例えば次のようなケースで役に立ちます
- is-open というclass属性の他に追加したい属性が出てきた場合
- 扉を4つに分割する必要が出てきた場合
などなど…
二度、三度利用する可能性のある機能は、メソッドとして切り分けて定義することで、その後の
汎用性が格段にあがります。
5. 他の機能もメソッドとして追加してみる
さて、開発中に追加要件が発生することはよくあることです。
むしろ、当初の要件で完了まで進むことが少ないかもしれません。
追加の要件として、閉じるボタンをクリックしたら、ドアが閉じ、再度アニメーションが再開するように改修を追加してみましょう。
HTML
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>デモ</title> </head> <body> <div class="js-introduction"> <div class="js-introduction__left"></div> <div class="js-introduction__right"></div> </div> <!--追加 : 開始--> <a href="#" class="js-introduction-reset">リスタート</a> <!--追加 : ここまで--> </body> </html>
JavaScript
~~~~略~~~~ /** * 閉じるメソッド * @param string target left or right * @return this **/ Introduction.prototype.close = function (target) { if (typeof this.elDoors[target] !== "undefined") { // class属性の is-open を削除 this.elDoors[target].classList.remove("is-open"); } return this; } /** * リセットする * @return this **/ Introduction.prototype.reset = function () { this.close("left"); this.close("right"); this.el.classList.remove('is-loaded') return this; } addEventListener("DOMContentLoaded", function () { var introInstance = new Introduction(".js-introduction"); introInstance.start(); // 閉じるボタンをクリックしたときの処理を追加 document.querySelector(".js-introduction-reset").addEventListener('click', function(e){ e.preventDefault(); introInstance.reset(); introInstance.start(); }); });
クラスとしてメソッドを追加することで、このような改修についてもすでにクラス内で利用しているメンバ変数 (クラス内で this に紐付けられている変数の呼称) やメソッドを利用でき、コードとしてもシンプルに記述することができます。
まとめ
クラスの定義を一通り行いましたが、次の流れで実装することが可能です。
- コンストラクタの実装
- 設定値を検討 : メンバ変数の検討
- 機能の検討 : メソッドの定義
- リファクタリング : メンバ変数、メソッドを再検討
番外編 : Promiseを使ってみる
/** * ドアを開く * @param string target left or right * @return Promise */ Introduction.prototype.open = function (target,timeout) { return new Promise( (resolve, reject ) => { if (typeof this.elDoors[target] !== "undefined") { setTimeout(()=>{ this.elDoors[target].classList.add("is-open"); resolve(); }, timeout) } reject(); }) return this; } /** * アニメーションを開始 **/ Introduction.prototype.start = function () { var self = this; this.open('left', 1000).then(()=>{ this.open('right', 1000).then(()=>{ setTimeout(function () { this.el.classList.add('is-loaded') }, 1000); }); }) return this; } /** * await を利用した場合 **/ Introduction.prototype.start = async function () { var self = this; await this.open('left', 1000) await this.open('right', 1000) setTimeout(function () { this.el.classList.add('is-loaded') }, 1000); return this; }
参考
https://www.yunabe.jp/docs/javascript_class_in_google.html
Udemyを実際に体験した方の感想記事もぜひご覧ください♪