📜 ⬆️ ⬇️

The implementation of the pattern decorator on JS

The essence of the pattern is that there is a class with actual functionality (component) and optional wrapper classes that complement the basic functionality (decorators). And the trick is that decorators can be any number, they can be combined in any order and (since they require only an interface from a component) - they can work with different components.

Of course, you can realize something similar even due to the fact that functions in JS are first-level objects, but I would like to share a very close implementation of GOST GoF .

UPD: reference to the working example , thanks Barttos .
')
Before habrakat: encapsulation is present in the script, inheritance (in fact) is done via call, jQuery is missing - if your ideology does not allow to accept such restrictions, please do not write about it in the comments and, better yet, do not read this article. Constructive criticism and questions are welcome.

We will implement a simple blocklist. Not new, but we will implement it in such a way that we can flip through divs without animation or with it (components) and we can choose whether we have buttons for switching “pages” :) or page number (decorators), or both. The most interesting thing, when using all this “wealth”, it will be without a difference to us whether it is scrolling or how many and which UI elements are involved.

HTML and CSS for listalka is:
  <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>
		 / *
		 * at the end of the topic
		 * /
		 </ script>
	 </ head>
	 <body>
		 <div id = "container">
			 <div id = "scroll" style = "left: 0px;">
			 <div style = "background: #ffc;"> page 1 </ div>
			 <div style = "background: #fcf;"> page 2 </ div>
			 <div style = "background: #cff;"> page 3 </ div>
			 <div style = "background: #fcc;"> page 4 </ div>
			 <div style = "background: #ccf;"> page 5 </ div>
			 <div style = "background: #cfc;"> page 6 </ div>
			 <div style = "background: #ccc;"> page 7 </ div>
			 </ div>
		 </ div>
		 <script>
			 / *
			 * use
			 * /
		 </ script>
	 </ body>
 </ html> 


How to screw UI on top

The component is a separate part, ready to rewind pages when calling .nextPage and .prevPage. To wind up something from above we need:
  1. create a decorator;
  2. pass the component to the decorator;
  3. make decorators the same methods as the component;
  4. work with decorator methods, and he will already do his functionality and call the same methods on the component.
The key to all this is the same naming of methods, that is, the interface.

Members

All components must have the same interface, and all decorators must have the same interface, but additionally extended to be able to accept the component.

The very interface is called Component (in the listing is Scroll ), and its implementations is ConcreteComponent (in the listing: SimpleScroll and AnimScroll ). The decorator interface, Decorator (and in the Decorator listing), is also based on the Component interface. And the implementations of Decorator, ConcreteDecorator (in listing: Decorator_SwitchPage and Decorator_PageNum ) already belong to the Component indirectly.

Scroll and Decorator
I recommend to see Scroll and Decorator listing code at the bottom of the article. As you can see, Decorator rewrites (overloads) all methods of Scroll:Both Scroll and Decorator take on a very significant part of the dirty work, because they are more basic classes than interfaces.

SimpleScroll and AnimScroll
Thanks to Scroll, both classes can work with the local variables container and scroll. These are nodes with position: relative and absolute respectively. The methods hasNextPage, hasPrevPage, findPages and getCurPage are the same, but I didn’t take them out to Scroll so that it would be at least a bit like the interface. It is quite possible to make these methods in the intermediate class.

But nextPage and prevPage are different, but decorators simply call methods and can work with both classes.

Decorator_SwitchPage and Decorator_PageNum
SwitchPage adds forward and back buttons to the container.
PageNum adds an indicator of the current page, and their total number.

After connecting the Decorator, we need to keep references to its methods - the local variable methods serves as a crutch. It is used in overloaded methods to run the corresponding method on the nested component. Good point to evaluate the difference in the number of methods in Decorator and its implementations;)

Using

When used, the component will be created - SimpleScroll or AnimScroll. Then decorators: PageNum and SwitchPage. The component is transferred to the first decorator, the first decorator to the second decorator. We will work with the extreme (topmost) decorator, and he will send a method call down the chain.

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();


Without decorators
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 (interface)
 // because of the scope, it will also set local variables and setters / getters to them - not academic, but not scary
 // if there is a desire to use only prototyping, then it is necessary to create local variables "in place",
 // and leave all functions that use them in Scroll empty
 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 () {};  // manual initialization after setting the container and scroll
	 // this will simplify the task somewhat, since  we will not need to rewrite the setters

	 this.nextPage = function () {};  // fast forward
	 this.prevPage = function () {};  // rewind back

	 this.hasNextPage = function (depth) {};  // presence of the next page, or the next depth pages;  default depth = 1
	 this.hasPrevPage = function (depth) {};  // presence of the previous page, or the following depth pages;  default depth = 1
	 this.findPages = function () {};  // return method  number of pages
	 this.getCurPage = function () {};  // return method  current page number
 }

 // ConcreteComponent (Component implementation)
 // easiest scrolling
 function SimpleScroll () {
	 var dublicate = this;  // permanent link to the object being instantiated (for working with any this)
	 Scroll.call (this);  // aggregate interface
	 // Scroll already knows how to work with container and scroll, but nothing more -
	 // now we need to implement (overload) all empty methods
	 var curPage = 0;  // current page (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 ||  one;
		 return curPage + depth <dublicate.findPages ();
	 };
	 this.hasPrevPage = function (depth) {
		 var depth = depth ||  one;
		 return curPage - depth> = 0;
	 };
	 this.findPages = function () {
		 return this.getScroll (). getElementsByTagName ("div"). length;
	 };
	 this.getCurPage = function () {
		 return curPage;
	 };
 }

 // ConcreteComponent (Component implementation)
 // another scrolling, with animation
 function AnimScroll () {
	 var dublicate = this;  // permanent link to the object being instantiated (for working with any this)
	 Scroll.call (this);  // aggregate interface
	 var curPage = 0; 
	 var curOffset = 0;  // current page offset in pixels

	 this.init = function () {};

	 this.nextPage = function () {
		 if (dublicate.hasNextPage () &&! curOffset) {
			 curPage ++;  // add immediately to decorators work
			 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 ||  one;
		 return curPage + depth <dublicate.findPages ();
	 };
	 this.hasPrevPage = function (depth) {
		 var depth = depth ||  one;
		 return curPage - depth> = 0;
	 };
	 this.findPages = function () {
		 return this.getScroll (). getElementsByTagName ("div"). length;
	 };
	 this.getCurPage = function () {
		 return curPage;
	 };
 }


 // Decorator (interface)
 // can encapsulate (save to a local component variable) a component or another decorator
 // and just passes method calls to the component (except setComponent and 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 (implementation of Decorator)
 // buttons to switch pages
 function Decorator_SwitchPage () {
	 var dublicate = this;  // permanent link to the object being instantiated (for working with any this)
	 // connect the Decorator and write down references to the methods that are defined in Decorator's "interface" and which will be overridden here
	 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 (implementation of Decorator)
 // current page indicator
 function Decorator_PageNum () {
	 var dublicate = this;  // permanent link to the object being instantiated (for working with any 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 ();
	 }
 } 


Gingerbread
When there are many components or decorators, you can write a convenient function:
  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;
	 } 


Demo
Add to the layout:
  <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 ();"> Recreate component with decorators </ button>
 <br />
 <button onclick = "workWith.prevPage ();"> workWith.prevPage () </ button>
 <button onclick = "workWith.nextPage ();"> workWith.nextPage () </ button>
 <br /> 


And such a 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 ();  // reset previous settings (if any)
	
	 // create component - basis for further work
	 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) {
		 // create the first decorator
		 decorator1 = new Decorator_PageNum ();
		 decorator1.setComponent (component);  // wrap the component in the decorator
		 workWith = decorator1;
	 }
	
	 if (document.getElementById ("switchPage"). checked) {
		 // create another decorator
		 decorator2 = new Decorator_SwitchPage ();
		 if (decorator1) {
			 // wraps the first decorator in the second
			 decorator2.setComponent (decorator1); 
		 } else {
			 // wrap the component in the decorator
			 decorator2.setComponent (component); 
		 }
		 workWith = decorator2;
	 }
	
	 workWith.init ();
 }

 create (); 

Source: https://habr.com/ru/post/65219/


All Articles