Суть паттерна в том, что есть класс с фактической функциональностью (компонент) и опциональными классами-обертками, которые дополняют основной функционал (декораторы). А фишка в том, что декораторов может быть сколько угодно, совмещаться они могут в произвольном порядке и (поскольку требуют от компонента только интерфейса) — могут работать с разными компонентами.
Безусловно, реализовать что-то похожее можно даже за счет только лишь того, что функции в JS являются объектами первого уровня, но мне бы хотелось поделиться реализацией весьма близкой кГОСТу GoF'у.
UPD: ссылка на рабочий пример, спасибо Barttos.
Перед хабракатом: в скрипте присутствует инкапсуляция, наследование (по сути) осуществляется через call, jQuery отсутствует — если Ваша идеология не позволяет принять такие ограничения, пожалуйста, не пишите об этом в комментах и, еще лучше, не читайте эту статью. Конструктивная критика и вопросы приветствуются.
Реализовывать будем простую листалку блоков. Не ново, но реализуем мы её так, что сможем перелистывать дивы без анимации или с ней (компоненты) и сможем выбирать, будут ли у нас кнопки переключения «страниц» :) или номер страницы (декораторы), или и то, и другое. Самое интересное, при использовании всего этого «богатства», нам будет без разницы ни как оно листается, ни сколько и каких UI-элементов задействовано.
HTML и CSS у листалки такой:
Самый-самый интерфейс называется Component (в листинге — Scroll), а его реализации — ConcreteComponent (в листинге: SimpleScroll и AnimScroll). Интерфейс декораторов, Decorator (и в листинге Decorator), тоже основан на интерфейсе Component. А уже реализации Decorator'а, ConcreteDecorator (в листинге: Decorator_SwitchPage и Decorator_PageNum), относятся к Component'у косвенно.
А вот nextPage и prevPage различаются, но декораторы просто вызывают методы и могут работать с обеими классами.
PageNum добавляет индикатор текущей страницы, и их общего количества.
После подключения Decorator'а нам надо сохранить ссылки на его методы — в качестве костыля выступает локальная переменная methods. Она используется в перегруженных методах, чтобы запустить соответствующий метод у вложенного компонента. Хороший момент, чтобы оценить разницу в количестве методов у Decorator'а и его реализаций ;)
И вот такой script:
Безусловно, реализовать что-то похожее можно даже за счет только лишь того, что функции в JS являются объектами первого уровня, но мне бы хотелось поделиться реализацией весьма близкой к
UPD: ссылка на рабочий пример, спасибо Barttos.
Перед хабракатом: в скрипте присутствует инкапсуляция, наследование (по сути) осуществляется через call, jQuery отсутствует — если Ваша идеология не позволяет принять такие ограничения, пожалуйста, не пишите об этом в комментах и, еще лучше, не читайте эту статью. Конструктивная критика и вопросы приветствуются.
Реализовывать будем простую листалку блоков. Не ново, но реализуем мы её так, что сможем перелистывать дивы без анимации или с ней (компоненты) и сможем выбирать, будут ли у нас кнопки переключения «страниц» :) или номер страницы (декораторы), или и то, и другое. Самое интересное, при использовании всего этого «богатства», нам будет без разницы ни как оно листается, ни сколько и каких UI-элементов задействовано.
HTML и CSS у листалки такой:
<html>
<head>
<title> </title>
<style>
#container { padding-top: 53px; padding-bottom: 3px; border: 1px solid gray; }
#container, #scroll div { width: 100px; }
#scroll, #scroll div { height: 50px; }
#scroll div { float: left; }
#container { position: relative; overflow: hidden; }
#scroll { position: absolute; top: 0px; width: 1000px; border-bottom: 1px solid gray; }
</style>
<script>
/*
* в конце топика
*/
</script>
</head>
<body>
<div id="container">
<div id="scroll" style="left: 0px;">
<div style="background: #ffc;">страница 1</div>
<div style="background: #fcf;">страница 2</div>
<div style="background: #cff;">страница 3</div>
<div style="background: #fcc;">страница 4</div>
<div style="background: #ccf;">страница 5</div>
<div style="background: #cfc;">страница 6</div>
<div style="background: #ccc;">страница 7</div>
</div>
</div>
<script>
/*
* использование
*/
</script>
</body>
</html>Как накрутить UI сверху
Компонент является самостоятельной частью, готовой перематывать страницы при вызове .nextPage и .prevPage. Чтобы накрутить что-нибудь сверху нам надо:- создать декоратор;
- передать декоратору компонент;
- сделать у декораторов теже методы, что у компонента;
- работать с методами декоратора, а он уже будет делать свою функциональность и вызывать теже методы у компонента.
Участники
Все компоненты должны иметь одинаковый интерфейс, и все декораторы должны иметь тот же самый интерфейс, но дополнительно расширенный, чтобы уметь принимать компонент.Самый-самый интерфейс называется Component (в листинге — Scroll), а его реализации — ConcreteComponent (в листинге: SimpleScroll и AnimScroll). Интерфейс декораторов, Decorator (и в листинге Decorator), тоже основан на интерфейсе Component. А уже реализации Decorator'а, ConcreteDecorator (в листинге: Decorator_SwitchPage и Decorator_PageNum), относятся к Component'у косвенно.
Scroll и Decorator
Рекомендую посмотреть Scroll и Decorator, листинг кода внизу статьи. Как видим, Decorator переписывает (перегружает) все методы Scroll'а:- комментировать каждый из них повторно не требуется :)
- Decorator запускает тот же метод у вложенного (через setComponent) компонента.
SimpleScroll и AnimScroll
Благодаря Scroll'у, оба класса умеют работать с локальными переменными container и scroll. Это ноды с position: relative и absolute соответственно. Методы hasNextPage, hasPrevPage, findPages и getCurPage совпадают, но я не стал выносить их в Scroll, чтобы тот хоть немного напоминал интерфейс. Вполне можно вынести эти методы в промежуточный класс.А вот nextPage и prevPage различаются, но декораторы просто вызывают методы и могут работать с обеими классами.
Decorator_SwitchPage и Decorator_PageNum
SwitchPage добавляет в container кнопки «вперед» и «назад».PageNum добавляет индикатор текущей страницы, и их общего количества.
После подключения Decorator'а нам надо сохранить ссылки на его методы — в качестве костыля выступает локальная переменная methods. Она используется в перегруженных методах, чтобы запустить соответствующий метод у вложенного компонента. Хороший момент, чтобы оценить разницу в количестве методов у Decorator'а и его реализаций ;)
Использование
При использовании будет создаваться компонент — SimpleScroll или AnimScroll. Затем декораторы: PageNum и SwitchPage. В первый декоратор передается компонент, во второй декоратор — первый декоратор. Работать мы будем с крайним (самым верхним) декоратором, а он будет отправлять вызов методов вниз по цепочке.SimpleScroll + PageNum + SwitchPage
// создаем компонент - основу для дальнейшей работы
component = new SimpleScroll();
component.setContainer(document.getElementById("container"));
component.setScroll(document.getElementById("scroll"));
// создаем первый декоратор
decorator1 = new Decorator_PageNum();
decorator1.setComponent(component); // заворачиваем компонент в декоратор
// создаем еще один декоратор
decorator2 = new Decorator_SwitchPage();
decorator2.setComponent(decorator1); // заворачиваем первый декоратор во второй
decorator2.init();
decorator2.nextPage(); Без декораторов
component = new SimpleScroll();
component.setContainer(document.getElementById("container"));
component.setScroll(document.getElementById("scroll"));
component.init();
component.nextPage(); AnimScroll + SwitchPage
component = new AnimScroll();
component.setContainer(document.getElementById("container"));
component.setScroll(document.getElementById("scroll"));
decorator1 = new Decorator_SwitchPage(); // или new Decorator_PageNum();
decorator1.setComponent(component);
decorator1.init();
decorator1.nextPage(); JavaScript
// Component (интерфейс)
// из-за scope, в нем так же будут заданы локальные переменные и сеттеры/геттеры к ним - не академично, но и не страшно
// если есть желание использовать только прототипирование, то надо создавать локальные переменные "на месте",
// а все функции, которые их используют в Scroll оставить пустыми
function Scroll() {
var container, scroll;
this.setContainer = function(val) {
container = val;
};
this.setScroll = function(val) {
scroll = val;
};
this.getContainer = function() {
return container;
};
this.getScroll = function() {
return scroll;
};
this.init = function() { }; // ручная инициализация после задания container и scroll
// это несколько упростит задачу, т.к. нам не надо будет переписывать сеттеры
this.nextPage = function() { }; // перематываем вперед
this.prevPage = function() { }; // перематываем назад
this.hasNextPage = function(depth) { }; // наличие следующей страницы, или следующих depth страниц; по-умолчанию depth = 1
this.hasPrevPage = function(depth) { }; // наличие предыдущей страницы, или следующих depth страниц; по-умолчанию depth = 1
this.findPages = function() { }; // метод возвр. кол-во страниц
this.getCurPage = function() { }; // метод возвр. номер текущей страницы
}
// ConcreteComponent (реализация Component'а)
// самый простой скроллинг
function SimpleScroll() {
var dublicate = this; // постоянная ссылка на инстанцирующийся объект (для работы с любым this)
Scroll.call(this); // агрегируем интерфейс
// Scroll уже умеет работать с container и scroll, но ничего более -
// теперь нам надо реализовать (перегрузить) все пустые методы
var curPage = 0; // текущая страница (0-4)
this.init = function() { };
this.nextPage = function() {
if (dublicate.hasNextPage()) {
this.getScroll().style.left = ++curPage * -100 +"px";
}
};
this.prevPage = function() {
if (dublicate.hasPrevPage()) {
this.getScroll().style.left = --curPage * -100 +"px";
}
};
this.hasNextPage = function(depth) {
var depth = depth || 1;
return curPage + depth < dublicate.findPages();
};
this.hasPrevPage = function(depth) {
var depth = depth || 1;
return curPage - depth >= 0;
};
this.findPages = function() {
return this.getScroll().getElementsByTagName("div").length;
};
this.getCurPage = function() {
return curPage;
};
}
// ConcreteComponent (реализация Component'а)
// другой скроллинг, с анимацией
function AnimScroll() {
var dublicate = this; // постоянная ссылка на инстанцирующийся объект (для работы с любым this)
Scroll.call(this); // агрегируем интерфейс
var curPage = 0;
var curOffset = 0; // текущее смещение страницы в пикселях
this.init = function() { };
this.nextPage = function() {
if (dublicate.hasNextPage() && !curOffset) {
curPage++; // прибавим сразу, чтобы декораторы работали
curOffset = 0;
nextPageIterate();
}
};
function nextPageIterate() {
curOffset -= 10;
dublicate.getScroll().style.left = curOffset + (curPage-1)* -100 +"px";
if (curOffset>-100) {
window.setTimeout(arguments.callee, 20);
} else {
curOffset = 0;
}
}
this.prevPage = function() {
if (dublicate.hasPrevPage() && !curOffset) {
curPage--;
curOffset=0;
prevPageIterate();
}
};
function prevPageIterate() {
curOffset += 10;
dublicate.getScroll().style.left = curOffset + (curPage+1)* -100 +"px";
if (curOffset<100) {
window.setTimeout(arguments.callee, 20);
} else {
curOffset = 0;
}
}
this.hasNextPage = function(depth) {
var depth = depth || 1;
return curPage + depth < dublicate.findPages();
};
this.hasPrevPage = function(depth) {
var depth = depth || 1;
return curPage - depth >= 0;
};
this.findPages = function() {
return this.getScroll().getElementsByTagName("div").length;
};
this.getCurPage = function() {
return curPage;
};
}
// Decorator (интерфейс)
// умеет инкапсулировать (сохранять в локальную переменную component) компонент или другой декоратор
// и просто передает вызовы методов в component (кроме setComponent и getComponent)
function Decorator() {
var component;
this.setComponent = function(val) {
component = val;
};
this.getComponent = function() {
return component;
};
this.setContainer = function(val) {
return component.setContainer(val);
};
this.setScroll = function(val) {
return component.setScroll(val);
};
this.getContainer = function() {
return component.getContainer();
};
this.getScroll = function() {
return component.getScroll();
};
this.init = function() {
return component.init();
};
this.nextPage = function() {
return component.nextPage();
};
this.prevPage = function() {
return component.prevPage();
};
this.hasNextPage = function(depth) {
return component.hasNextPage(depth);
};
this.hasPrevPage = function(depth) {
return component.hasPrevPage(depth);
};
this.findPages = function() {
return component.findPages();
};
this.getCurPage = function() {
return component.getCurPage();
};
}
Decorator.prototype = new Scroll();
Decorator.prototype.constructor = Decorator;
// ConcreteDecorator (реализация Decorator'а)
// кнопки для переключения страниц
function Decorator_SwitchPage() {
var dublicate = this; // постоянная ссылка на инстанцирующийся объект (для работы с любым this)
// подключаем Decorator и записываем ссылки на методы, которые определенны в "интерфейсе" Decorator'а, и которые будут переопределяться здесь
Decorator.call(this);
var methods = {
nextPage: this.nextPage,
prevPage: this.prevPage,
init: this.init
};
var buttonNext, buttonPrev;
this.init = function() {
dublicate.getContainer().appendChild( buttonPrev = createButton("<", dublicate.prevPage) );
dublicate.getContainer().appendChild( buttonNext = createButton(">", dublicate.nextPage) );
buttonNext.disabled = !dublicate.hasNextPage();
buttonPrev.disabled = !dublicate.hasPrevPage();
return methods.init();
};
this.nextPage = function() {
buttonNext.disabled = !dublicate.hasNextPage(2);
buttonPrev.disabled = !dublicate.hasPrevPage(-1);
return methods.nextPage();
};
this.prevPage = function() {
buttonNext.disabled = !dublicate.hasNextPage(-1);
buttonPrev.disabled = !dublicate.hasPrevPage(2);
return methods.prevPage();
};
function createButton(text, onclick) {
var ret = document.createElement("button");
ret.appendChild( document.createTextNode( text ) );
ret.onclick = onclick;
return ret;
}
}
// ConcreteDecorator (реализация Decorator'а)
// индикатор текущей страницы
function Decorator_PageNum() {
var dublicate = this; // постоянная ссылка на инстанцирующийся объект (для работы с любым this)
Decorator.call(this);
var methods = {
nextPage: this.nextPage,
prevPage: this.prevPage,
init: this.init
};
var text;
this.init = function() {
dublicate.getContainer().appendChild( text = document.createTextNode( "..." ) );
var ret = methods.init();
chText();
return ret;
};
this.nextPage = function() {
var ret = methods.nextPage();
chText();
return ret;
};
this.prevPage = function() {
var ret = methods.prevPage();
chText();
return ret;
};
function chText() {
text.nodeValue = " "+ (dublicate.getCurPage()+1) +" / "+ dublicate.findPages();
}
}Пряники
Когда компонентов или декораторов будет много, можно написать удобную функцию:function cr() {
var ret, last;
for (var i=0, l=arguments.length; i<l; i++) {
ret = new arguments[i]();
if (!i) {
ret.setContainer(document.getElementById("container"));
ret.setScroll(document.getElementById("scroll"));
} else (
ret.setComponent(last);
}
last = ret;
}
return ret;
}Демка
К верстке добавляем:<table cellpadding="5"><tr> <th>workWith</th> <td>=</td> <td> <div> <input type="radio" name="component" value="simple" id="component-simple" checked="checked" /> <label for="component-simple">SimpleScroll</label> </div> <input type="radio" name="component" value="simple" id="component-anim" /> <label for="component-anim">AnimScroll</label> </td> <td>+</td> <td> <input type="checkbox" id="pageNum" checked="checked" /> <label for="pageNum">pageNum</label> </td> <td>+</td> <td> <input type="checkbox" id="switchPage" checked="checked" /> <label for="switchPage">switchPage</label> </td> </tr></table> <button onclick="create();">Пересоздать компонент с декораторами</button> <br /> <button onclick="workWith.prevPage();">workWith.prevPage()</button> <button onclick="workWith.nextPage();">workWith.nextPage()</button> <br />
И вот такой script:
var component;
var decorator1;
var decorator2;
var workWith;
function reset() {
component = decorator1 = decorator2 = workWith = null;
var node = document.getElementById("scroll");
node.style.left = "0px";
while(node.nextSibling) {
node.parentNode.removeChild(node.nextSibling);
}
}
function create() {
reset(); // сбрасываем предыдущие настройки (если были)
// создаем компонент - основу для дальнейшей работы
if (document.getElementById("component-simple").checked) {
component = new SimpleScroll();
} else {
component = new AnimScroll();
}
component.setContainer(document.getElementById("container"));
component.setScroll(document.getElementById("scroll"));
workWith = component;
if (document.getElementById("pageNum").checked) {
// создаем первый декоратор
decorator1 = new Decorator_PageNum();
decorator1.setComponent(component); // заворачиваем компонент в декоратор
workWith = decorator1;
}
if (document.getElementById("switchPage").checked) {
// создаем еще один декоратор
decorator2 = new Decorator_SwitchPage();
if (decorator1) {
// заворачиваем первый декоратор во второй
decorator2.setComponent(decorator1);
} else {
// заворачиваем компонент в декоратор
decorator2.setComponent(component);
}
workWith = decorator2;
}
workWith.init();
}
create();