封装性、正確性、拡張性、再利用性
スライダー#
UI コンポーネントの HTML 構造を決定する#
<div class="slider">
<ul>
<li class="slider__item--selected">
<img src="https://p5.ssl.qhimg.com/t0119c74624763dd070.png"/>
</li>
<li class="slider__item">
<img src="https://p4.ssl.qhimg.com/t01adbe3351db853eb3.jpg"/>
</li>
<li class="slider__item">
<img src="https://p2.ssl.qhimg.com/t01645cd5ba0c3b60cb.jpg"/>
</li>
<li class="slider__item">
<img src="https://p4.ssl.qhimg.com/t01331ac159b58f5478.jpg"/>
</li>
</ul>
<a class="slider__next"></a>
<a class="slider__previous"></a>
<div class="slider__control">
<span class="slider__control-buttons--selected"></span>
<span class="slider__control-buttons"></span>
<span class="slider__control-buttons"></span>
<span class="slider__control-buttons"></span>
</div>
</div>
要素のスタイルを設定する#
.slider {
position: relative;
width: 790px;
height: 340px;
}
.slider ul {
list-style-type:none;
position: relative;
width: 100%;
height: 100%;
padding: 0;
margin: 0;
}
.slider__item,
.slider__item--selected {
position: absolute;
transition: opacity 1s;
opacity: 0;
text-align: center;
}
.slider__item--selected {
transition: opacity 1s;
opacity: 1;
}
.slider__next,
.slider__previous{
display: inline-block;
position: absolute;
top: 50%; /*スライダーコンポーネントの縦の中央に位置する*/
margin-top: -25px;
width: 30px;
height:50px;
text-align: center;
font-size: 24px;
line-height: 50px;
overflow: hidden;
border: none;
color: white;
background: rgba(0,0,0,0.2); /*半透明に設定*/
cursor: pointer; /*マウスがこの要素に移動したときに指の形を表示*/
opacity: 0; /*初期状態は透明*/
transition: opacity .5s; /*透明度変化のアニメーションを設定、時間は.5秒*/
}
.slider__previous {
left: 0; /*スライダー要素の最左端に位置する*/
}
.slider__next {
right: 0; /*スライダー要素の最右端に位置する*/
}
.slider:hover .slider__previous {
opacity: 1;
}
.slider:hover .slider__next {
opacity: 1;
}
.slider__previous:after {
content: '<';
}
.slider__next:after {
content: '>';
}
.slider__control{
position: relative;
display: table; /*テーブルレイアウト*/
background-color: rgba(255, 255, 255, 0.5);
padding: 5px;
border-radius: 12px;
bottom: 30px;
margin: auto;
}
.slider__control-buttons,
.slider__control-buttons--selected{
display: inline-block;
width: 15px;
height: 15px;
border-radius: 50%;/*円形に設定*/
margin: 0 5px;
background-color: white;
cursor: pointer;
}
.slider__control-buttons--selected {
background-color: red;
}
API を設計する#
class Slider {
constructor({container}) {
this.container = container;
this.items = Array.from(container.querySelectorAll('.slider__item, .slider__item--selected'));
}
/*
セレクタ`.slider__item--selected`を使用して選択された要素を取得
*/
getSelectedItem() {
const selected = this.container.querySelector('.slider__item--selected');
return selected;
}
/*
選択された要素がitems配列の中での位置を返す。
*/
getSelectedItemIndex() {
return this.items.indexOf(this.getSelectedItem());
}
slideTo(idx) {
const selected = this.getSelectedItem();
if(selected) { // 以前選択された画像を通常状態にマーク
selected.className = 'slider__item';
}
const item = this.items[idx];
if(item) { // 現在選択された画像を選択状態にマーク
item.className = 'slider__item--selected';
}
}
/*
次の画像を選択状態にマーク
*/
slideNext() {
const currentIdx = this.getSelectedItemIndex();
const nextIdx = (currentIdx + 1) % this.items.length;
this.slideTo(nextIdx);
}
/*
前の画像を選択状態にマーク
*/
slidePrevious() {
const currentIdx = this.getSelectedItemIndex();
const previousIdx = (this.items.length + currentIdx - 1) % this.items.length;
this.slideTo(previousIdx);
}
}
const container = document.querySelector('.slider');
const slider = new Slider({container});
setInterval(() => {
slider.slideNext();
}, 3000);
ユーザーコントロールを実装する#
コンストラクタを以下のように変更します:
constructor({container, cycle = 3000} = {}) {
this.container = container;
this.items = Array.from(container.querySelectorAll('.slider__item, .slider__item--selected'));
this.cycle = cycle;
const controller = this.container.querySelector('.slider__control');
const buttons = controller.querySelectorAll('.slider__control-buttons, .slider__control-buttons--selected');
controller.addEventListener('mouseover', (evt) => {
const idx = Array.from(buttons).indexOf(evt.target);
if(idx >= 0) {
this.slideTo(idx);
this.stop();
}
});
controller.addEventListener('mouseout', (evt) => {
this.start();
});
/*
スライドイベントを登録し、選択された画像と小さな円をselected状態に設定
*/
this.container.addEventListener('slide', (evt) => {
const idx = evt.detail.index;
const selected = controller.querySelector('.slider__control-buttons--selected');
if(selected) selected.className = 'slider__control-buttons';
buttons[idx].className = 'slider__control-buttons--selected';
});
const previous = this.container.querySelector('.slider__previous');
previous.addEventListener('click', (evt) => {
this.stop();
this.slidePrevious();
this.start();
evt.preventDefault();
});
const next = this.container.querySelector('.slider__next');
next.addEventListener('click', (evt) => {
this.stop();
this.slideNext();
this.start();
evt.preventDefault();
});
}
start() {
this.stop();
this._timer = setInterval(() => this.slideNext(), this.cycle);
}
stop() {
clearInterval(this._timer);
}
slideTo メソッドを修正し、カスタムイベントをトリガーするようにします:
slideTo(idx) {
const selected = this.getSelectedItem();
if (selected) {
selected.className = 'slider__item';
}
const item = this.items[idx];
if (item) {
item.className = 'slider__item--selected';
}
const detail = {index: idx};
const event = new CustomEvent('slide', {bubbles: true, detail});
this.container.dispatchEvent(event);
}
最後に呼び出しプロセスを次のように変更します:
const slider = new Slider({container});
slider.start();
コンポーネントのプラグイン化#
この画像スライダーコンポーネントにとって、プラグイン化はユーザーコントロールコンポーネントを Slider コンポーネントから切り離し、プラグインとして作成することができ、これにより Slider コンポーネントの拡張性が向上します。
class Slider {
constructor({container, cycle = 3000} = {}) {
this.container = container;
this.items = Array.from(container.querySelectorAll('.slider__item, .slider__item--selected'));
this.cycle = cycle;
}
registerPlugins(...plugins) {
plugins.forEach(plugin => plugin(this));
}
/*
セレクタ`.slider__item--selected`を使用して選択された要素を取得
*/
getSelectedItem() {
const selected = this.container.querySelector('.slider__item--selected');
return selected;
}
/*
選択された要素がitems配列の中での位置を返す。
*/
getSelectedItemIndex() {
return this.items.indexOf(this.getSelectedItem());
}
slideTo(idx) {
const selected = this.getSelectedItem();
if(selected) {
selected.className = 'slider__item';
}
const item = this.items[idx];
if(item) {
item.className = 'slider__item--selected';
}
const detail = {index: idx};
const event = new CustomEvent('slide', {bubbles: true, detail});
this.container.dispatchEvent(event);
}
/*
次の画像を選択状態にマーク
*/
slideNext() {
const currentIdx = this.getSelectedItemIndex();
const nextIdx = (currentIdx + 1) % this.items.length;
this.slideTo(nextIdx);
}
/*
前の画像を選択状態にマーク
*/
slidePrevious() {
const currentIdx = this.getSelectedItemIndex();
const previousIdx = (this.items.length + currentIdx - 1) % this.items.length;
this.slideTo(previousIdx);
}
start() {
this.stop();
this._timer = setInterval(() => this.slideNext(), this.cycle);
}
stop() {
clearInterval(this._timer);
}
}
/* 小さな円ボタンプラグイン */
function pluginController(slider) {
const controller = slider.container.querySelector('.slider__control');
if(controller) {
const buttons = controller.querySelectorAll('.slider__control-buttons, .slider__control-buttons--selected');
controller.addEventListener('mouseover', (evt) => {
const idx = Array.from(buttons).indexOf(evt.target);
if(idx >= 0) {
slider.slideTo(idx);
slider.stop();
}
});
controller.addEventListener('mouseout', (evt) => {
slider.start();
});
slider.container.addEventListener('slide', (evt) => {
const idx = evt.detail.index;
const selected = controller.querySelector('.slider__control-buttons--selected');
if(selected) selected.className = 'slider__control-buttons';
buttons[idx].className = 'slider__control-buttons--selected';
});
}
}
function pluginPrevious(slider) {
const previous = slider.container.querySelector('.slider__previous');
if(previous) {
previous.addEventListener('click', (evt) => {
slider.stop();
slider.slidePrevious();
slider.start();
evt.preventDefault();
});
}
}
function pluginNext(slider) {
const next = slider.container.querySelector('.slider__next');
if(next) {
next.addEventListener('click', (evt) => {
slider.stop();
slider.slideNext();
slider.start();
evt.preventDefault();
});
}
}
const container = document.querySelector('.slider');
const slider = new Slider({container});
slider.registerPlugins(pluginController, pluginPrevious, pluginNext);
slider.start();
もし、ある日、プロダクトマネージャーがこのコンポーネントに新しいユーザーコントロールを追加する必要がある場合、例えば「運試し」というボタンを追加し、そのボタンをクリックするとスライダーがランダムに画像を切り替えるようにする場合、次のようにするだけです:
HTML コードに「運試し」ボタンを追加します:
<button class="lucky">運試し</button>
次に、このプラグインを作成します:
function pluginLucky(slider) {
const luckyBtn = document.querySelector('.lucky');
if (luckyBtn) {
luckyBtn.addEventListener('click', evt => {
slider.stop();
slider.slideTo(Math.floor(Math.random() * slider.items.length));
slider.start();
evt.preventDefault();
});
}
}
最後に、それをスライダーに登録します:
slider.registerPlugins(pluginController, pluginPrevious, pluginNext, pluginLucky);
コンポーネントのテンプレート化#
class Slider {
constructor({container, images = [], cycle = 3000} = {}) {
this.container = container;
this.data = images;
this.container.innerHTML = this.render(this.data);
this.items = Array.from(this.container.querySelectorAll('.slider__item, .slider__item--selected'));
this.cycle = cycle;
this.slideTo(0);
}
render(images) {
const content = images.map(image => `
<li class="slider__item">
<img src="${image}"/>
</li>
`.trim());
return `<ul>${content.join('')}</ul>`;
}
registerPlugins(...plugins) {
plugins.forEach((plugin) => {
const pluginContainer = document.createElement('div');
pluginContainer.className = 'slider__plugin';
pluginContainer.innerHTML = plugin.render(this.data);
this.container.appendChild(pluginContainer);
plugin.initialize(this);
});
}
/*
セレクタ`.slider__item--selected`を使用して選択された要素を取得
*/
getSelectedItem() {
const selected = this.container.querySelector('.slider__item--selected');
return selected;
}
/*
選択された要素がitems配列の中での位置を返す。
*/
getSelectedItemIndex() {
return this.items.indexOf(this.getSelectedItem());
}
slideTo(idx) {
const selected = this.getSelectedItem();
if(selected) {
selected.className = 'slider__item';
}
const item = this.items[idx];
if(item) {
item.className = 'slider__item--selected';
}
const detail = {index: idx};
const event = new CustomEvent('slide', {bubbles: true, detail});
this.container.dispatchEvent(event);
}
/*
次の画像を選択状態にマーク
*/
slideNext() {
const currentIdx = this.getSelectedItemIndex();
const nextIdx = (currentIdx + 1) % this.items.length;
this.slideTo(nextIdx);
}
/*
前の画像を選択状態にマーク
*/
slidePrevious() {
const currentIdx = this.getSelectedItemIndex();
const previousIdx = (this.items.length + currentIdx - 1) % this.items.length;
this.slideTo(previousIdx);
}
start() {
this.stop();
this._timer = setInterval(() => this.slideNext(), this.cycle);
}
stop() {
clearInterval(this._timer);
}
}
const pluginController = { // 小さな円ボタンプラグイン
render(images) { // 画像の数が増えるにつれて、小さな円要素も増える必要がある
return `
<div class="slider__control">
${images.map((image, i) => `
<span class="slider__control-buttons${i === 0 ? '--selected' : ''}"></span>
`).join('')}
</div>
`.trim();
},
initialize(slider) {
const controller = slider.container.querySelector('.slider__control');
if(controller) {
const buttons = controller.querySelectorAll('.slider__control-buttons, .slider__control-buttons--selected');
controller.addEventListener('mouseover', (evt) => {
const idx = Array.from(buttons).indexOf(evt.target);
if(idx >= 0) {
slider.slideTo(idx);
slider.stop();
}
});
controller.addEventListener('mouseout', (evt) => {
slider.start();
});
slider.container.addEventListener('slide', (evt) => {
const idx = evt.detail.index;
const selected = controller.querySelector('.slider__control-buttons--selected');
if(selected) selected.className = 'slider__control-buttons';
buttons[idx].className = 'slider__control-buttons--selected';
});
}
},
};
const pluginPrevious = {
render() {
return '<a class="slider__previous"></a>';
},
initialize(slider) {
const previous = slider.container.querySelector('.slider__previous');
if(previous) {
previous.addEventListener('click', (evt) => {
slider.stop();
slider.slidePrevious();
slider.start();
evt.preventDefault();
});
}
},
};
const pluginNext = {
render() {
return '<a class="slider__next"></a>';
},
initialize(slider) {
const previous = slider.container.querySelector('.slider__next');
if(previous) {
previous.addEventListener('click', (evt) => {
slider.stop();
slider.slideNext();
slider.start();
evt.preventDefault();
});
}
},
};
const images = ['https://p5.ssl.qhimg.com/t0119c74624763dd070.png',
'https://p4.ssl.qhimg.com/t01adbe3351db853eb3.jpg',
'https://p2.ssl.qhimg.com/t01645cd5ba0c3b60cb.jpg',
'https://p4.ssl.qhimg.com/t01331ac159b58f5478.jpg'];
const container = document.querySelector('.slider');
const slider = new Slider({container, images});
slider.registerPlugins(pluginController, pluginPrevious, pluginNext);
slider.start();
テンプレート化された後、コードの拡張性がさらに向上しました。現在、スライド画像の数を増やしたり減らしたりする場合は、images 配列の要素数を変更するだけで済みます。プラグインが必要な場合や不要な場合は、registerPlugins () メソッドに渡す引数を変更するだけで済みます。
コンポーネントフレームワークを設計する#
コンポーネントの再利用性を高めるために、コンポーネントに統一された規範を設計する必要があります。この統一された規範を実現するために、一般的なコンポーネントメカニズムを設計し、このメカニズムを原則としてライブラリを構築します。この一般的なメカニズムは、実際にはコード設計と抽象化のための一般的な規範を提供し、この規範に従った基盤ライブラリは、実際には完全な UI コンポーネントフレームワークです。
class Component {
static name = 'component';
constructor({container, data, parent = null} = {}) {
this.data = data;
this.container = container;
this.container.innerHTML = this.render(this.data);
}
registerSubComponents(...Comps) {
const data = this.data;
const container = this.container;
this.children = this.children || [];
Comps.forEach((Comp) => {
const subContainer = document.createElement('div');
const sub = new Comp({container: subContainer, data, parent: this});
container.appendChild(subContainer);
this.children.push(sub);
});
}
render(data) {
/* 抽象 */
return '';
}
}
class Slider extends Component {
static name = 'slider';
constructor({container, images = [], cycle = 3000} = {}) {
super({container, data: images});
this.items = Array.from(this.container.querySelectorAll('.slider__item, .slider__item--selected'));
this.cycle = cycle;
this.slideTo(0);
}
render(images) {
const content = images.map(image => `
<li class="slider__item">
<img src="${image}"/>
</li>
`.trim());
return `<ul>${content.join('')}</ul>`;
}
getSelectedItem() {
const selected = this.container.querySelector('.slider__item--selected');
return selected;
}
getSelectedItemIndex() {
return this.items.indexOf(this.getSelectedItem());
}
slideTo(idx) {
const selected = this.getSelectedItem();
if(selected) {
selected.className = 'slider__item';
}
const item = this.items[idx];
if(item) {
item.className = 'slider__item--selected';
}
const detail = {index: idx};
const event = new CustomEvent('slide', {bubbles: true, detail});
this.container.dispatchEvent(event);
}
slideNext() {
const currentIdx = this.getSelectedItemIndex();
const nextIdx = (currentIdx + 1) % this.items.length;
this.slideTo(nextIdx);
}
slidePrevious() {
const currentIdx = this.getSelectedItemIndex();
const previousIdx = (this.items.length + currentIdx - 1) % this.items.length;
this.slideTo(previousIdx);
}
start() {
this.stop();
this._timer = setInterval(() => this.slideNext(), this.cycle);
}
stop() {
clearInterval(this._timer);
}
}
class SliderController extends Component {
static name = 'slider__control';
constructor({container, data, parent: slider}) {
super({container, data});
const buttons = container.querySelectorAll('.slider__control-buttons, .slider__control-buttons--selected');
container.addEventListener('mouseover', (evt) => {
const idx = Array.from(buttons).indexOf(evt.target);
if(idx >= 0) {
slider.slideTo(idx);
slider.stop();
}
});
container.addEventListener('mouseout', (evt) => {
slider.start();
});
slider.container.addEventListener('slide', (evt) => {
const idx = evt.detail.index;
const selected = container.querySelector('.slider__control-buttons--selected');
if(selected) selected.className = 'slider__control-buttons';
buttons[idx].className = 'slider__control-buttons--selected';
});
}
render(images) {
return `
<div class="slider__control">
${images.map((image, i) => `
<span class="slider__control-buttons${i === 0 ? '--selected' : ''}"></span>
`).join('')}
</div>
`.trim();
}
}
class SliderPrevious extends Component {
constructor({container, parent: slider}) {
super({container});
const previous = container.querySelector('.slider__previous');
previous.addEventListener('click', (evt) => {
slider.stop();
slider.slidePrevious();
slider.start();
evt.preventDefault();
});
}
render() {
return '<a class="slider__previous"></a>';
}
}
class SliderNext extends Component {
constructor({container, parent: slider}) {
super({container});
const previous = container.querySelector('.slider__next');
previous.addEventListener('click', (evt) => {
slider.stop();
slider.slideNext();
slider.start();
evt.preventDefault();
});
}
render() {
return '<a class="slider__next"></a>';
}
}
const images = ['https://p5.ssl.qhimg.com/t0119c74624763dd070.png',
'https://p4.ssl.qhimg.com/t01adbe3351db853eb3.jpg',
'https://p2.ssl.qhimg.com/t01645cd5ba0c3b60cb.jpg',
'https://p4.ssl.qhimg.com/t01331ac159b58f5478.jpg'];
const container = document.querySelector('.slider');
const slider = new Slider({container, images});
slider.registerSubComponents(SliderController, SliderPrevious, SliderNext);
slider.start();