📜 ⬆️ ⬇️

Front-end template engine

I covered my last article with the description of the “bicycle” (loader and templating engine within the framework of the “lightweight” framework). By the will of fate, for a couple of projects I was forced to select the template engine and make it a standalone version, while enriching it with a number of new features. It is about the front-end template and will be discussed.

But in order to save your time, I will first mark those to whom this article may be interesting (for there will be a lot of letters):



')
The project is called Flex.Patterns, but for simplicity, I'll just call it patterns. Below are a few examples that you can easily reproduce yourself. Unlike Flex, described in the last article, patterns does not require any settings and dances with a tambourine - picked up and use. Patterns are generally quite simple, which was my main goal.

For example, the pattern in patterns is just an HTML page and nothing else. No specific syntax like that used in EJS and many other template engines.

<ul> <% for(var i=0; i<supplies.length; i++) {%> <li><%= supplies[i] %></li> <% } %> </ul> 


The entire syntax patterns is limited to three definitions:



Creating a template



Well, let's take an example. Create a popup to authorize the user. We need four templates:



Below is a popup template.

 <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title>Flex.Template</title> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <link rel="stylesheet" type="text/css" href="pattern.css" /> </head> <body> <div data-style="Popup" id="{{id}}"> <div data-style="Popup.Container"> <div data-style="Popup.Title"> <p data-style="Popup.Title">{{title}}</p> </div> <div data-style="Popup.Content">{{content}}</div> <div data-style="Popup.Bottom"> <p data-style="Popup.Bottom">{{bottom}}</p> </div> </div> </div> </body> </html> 


As you have already noticed, this is the most ordinary HTML file. In HEAD, you can include CSS and JS files that will be automatically connected with the template and cached.

Caching is an important part of patterns. Both the templates themselves (HTML) and resources (CSS and JS) are stored in localStorage, which means that when you reuse the template, all data will be taken not from the server, but from the client, which most favorably affects the rendering speed. In addition, patterns itself monitors the relevance of the cache: every time, patterns request HEADERs for all patterns and their resources; and if something has changed, patterns will automatically update the cache to keep the entire system up to date. But, back to our login window.

The markup template (I will continue to give only the content of the BODY tag, to save space)

 <div data-type="Pattern.Login"> <p>Login</p> {{login}} <p>Password</p> {{password}} <div data-type="Pattern.Controls">{{controls}}</div> </div> 


The field for entering text (in our case it will be the login and password)

 <p>{{::value}}</p> <div data-type="TextInput.Wrapper"> <div data-type="TextInput.Container"> <input data-type="TextInput" type="{{type}}" value="{{::value}}" name="TestInput"/> </div> </div> 


Notice that we linked INPUT.value and P.innerHTML through a variable named value using the above {{:: value}} tag. Thus, if some text is entered in INPUT, it will be displayed in the related paragraph. In addition, the created value variable will be placed in the model.

Well, the last template required for the login window is the button.

  <a data-type="Buttons.Flat" id="{{id}}">{{title}}</a> 


Before you go further, it is worth a reservation. The fact that patterns uses full-fledged HTML files as templates allows you to open them separately from the page where they are used, and this makes it possible to quickly debug styles and logic, if any.

Attaching a template



A template can be attached to a page (that is, drawn) in two ways:



Which one to use depends solely on the task. For example, if the template should be drawn immediately after the page loads, then it is better to attach it through the markup. If we are talking about something like our authorization test window, then a call through JavaScript is more appropriate. Let's look at both methods.

JavaScript rendering



The get method - _patterns.get () is responsible for rendering the template, which returns an instance of the template class, which you can mount (attach to the specified node) via the - render method. Take a look at the example below and everything will become clear.

 var id = flex.unique(); _patterns.get({ url : '/patterns/popup/pattern.html', node : document.body, hooks : { id : id, title : 'Test dialog window', content : _patterns.get({ url : '/patterns/patterns/login/pattern.html', hooks : { login : _patterns.get({ url : '/patterns/controls/textinput/pattern.html', hooks : { type: 'text', } }), password: _patterns.get({ url : '/patterns/controls/textinput/pattern.html', hooks : { type: 'password', } }), controls: _patterns.get({ url : '/patterns/buttons/flat/pattern.html', hooks : [{ title: 'login', id: 'login_button' }, { title: 'cancel', id: 'cancel_button' }] }), } }) } }).render(); 


The most important parameter is the url, where we specify the place where to get the template. An equally important parameter is hooks. Remember in the templates we specified the places for the content through the label - {{name}}. In the hooks parameter, we define the content for each such tag.

A full description of all the parameters that the _patterns.get () method accepts can be found here . And the result of this example can be found here .

But we go further.

HMTL markup rendering



In a situation where we need the generated template immediately after loading the page, we can, using the PATTERN tag, place the template directly into the markup.

  <pattern src="/patterns/popup/pattern.html" style="display:none;"> <id>0</id> <title>Test dialog window</title> <content src="/patterns/patterns/login/pattern.html"> <login src="/patterns/controls/textinput/pattern.html"> <type>text</type> </login> <password src="/patterns/controls/textinput/pattern.html"> <type>password</type> </password> <controls src="/patterns/buttons/flat/pattern.html"> <id>login_button</id><title>login</title> <id>cancel_button</id><title>cancel</title> </controls> </content> </pattern> 


In this case, we use tags of the same name to define hooks. That is, the following two designs are identical in their meaning.

 <pattern src="/patterns/popup/pattern.html" style="display:none;"> <id>0</id> <title>Test dialog window</title> ... </pattern> 


 _patterns.get({ url : '/patterns/popup/pattern.html', hooks : { id : 0, title : 'Test dialog window', ... } }).render(); 


Please note that we use the PATTERN tag only for the root node, and then we only add the SCR property to indicate that the nested template will be used as the hook content.

That is, the following markup means that patterns must find a pattern at the address specified in the SRC and apply it with a hook type in the value “text”.

 <login src="/patterns/controls/textinput/pattern.html"> <type>text</type> </login> 


Here you can look at a working example. Open the sources of page to make sure that there are no JavaScript calls and the PATTERN tag with the data needed for rendering is in the original page markup.

Pattern Repeat



Very often we need to repeat the pattern many times. The most striking example of this is the table. To create it we need two templates.

Table template

 <table data-type="Demo.Table"> <tr> <th>{{titles.column_0}}</th> <th>{{titles.column_1}}</th> <th>{{titles.column_2}}</th> <th>{{titles.column_3}}</th> </tr> {{rows}} </table> 


And the row pattern in the table.

 <tr> <td>{{column_0}}</td> <td>{{column_1}}</td> <td>{{column_2}}</td> <td>{{column_3}}</td> </tr> 


Having these two templates and data, we can draw our table.

 var data_source = []; for (var i = 0; i < 100; i += 1) { data_source.push({ column_0: (Math.random() * 1000).toFixed(4), column_1: (Math.random() * 1000).toFixed(4), column_2: (Math.random() * 1000).toFixed(4), column_3: (Math.random() * 1000).toFixed(4), }); } _patterns.get({ url: '/patterns/table/container/pattern.html', node: document.body, hooks: { titles: { column_0: 'Column #0', column_1: 'Column #1', column_2: 'Column #2', column_3: 'Column #3', }, rows: _patterns.get({ url: '/patterns/table/row/pattern.html', hooks: data_source, }) } }).render(); 


Here you can find a working example.

So, to repeat a pattern several times, we just need to pass the hook value as an array of data. And, as you can see, to repeat the pattern when defining it in HTML, we repeat the hook values ​​as many times as we need, as was previously demonstrated with the buttons to the authorization window.

 <controls src="/patterns/buttons/flat/pattern.html"> <id>login_button</id><title>login</title> <id>cancel_button</id><title>cancel</title> </controls> 


Also note that the names of the hooks in the headers are defined through the {{titles.column_0}} point, which allows us to use a more meaningful definition of their values ​​in the rendering function. So, all headers are defined in the titles object.

Controllers and callback functions



In fact, in patterns, the controller and the callback function are the same. The only difference is in the storage location.

As you might guess, the callback function is determined at the time of rendering the template.

 _patterns.get({ url : 'some_url', callbacks: { //Callback-function definition success: function (results) { var instance = this, dom = results.dom, model = results.model, binds = results.binds, map = results.map, resources = results.resources; ... } }, }).render(); 


But to create a controller, you need to create a JS file with the following content

 _controller(function (results) { var instance = this, dom = results.dom, model = results.model, binds = results.binds, map = results.map, resources = results.resources; ... }); 


Then you just need to attach it to your template.

 <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title>Flex.Template</title> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <link rel="stylesheet" type="text/css" href="pattern.css" /> <!-- Attach controller of template --> <script type="text/javascript" src="conroller.js"></script> </head> <body> <div data-style="Popup"> <div data-style="Popup.Container"> <div data-style="Popup.Title"> <p data-style="Popup.Title">{{title}}</p> </div> <div data-style="Popup.Content">{{content}}</div> <div data-style="Popup.Bottom"> <p data-style="Popup.Bottom">{{bottom}}</p> </div> </div> </div> </body> </html> 


And that's all, the controller is ready. Now everything that is defined inside your controller will be launched every time after the template is drawn.

However, the most interesting object is the results object, which is passed to both the controller and the callback function.

Model and connection



The two important objects you get are model and binds.

 var model = results.model; var binds = results.binds; 


To demonstrate what is what, let's change the pattern for the table row as follows:

 <tr> <td style="background:{{::background_0}};">{{column_0}}{{::column_0}}</td> <td style="background:{{::background_1}};">{{column_1}}{{::column_1}}</td> <td style="background:{{::background_2}};">{{column_2}}{{::column_2}}</td> <td style="background:{{::background_3}};">{{column_3}}{{::column_3}}</td> </tr> 


As you can see, we added a couple of links. First, we associated the background property of each cell with the background_n variable. We did the same for the values ​​of the cells themselves, associating them with the column_n variable.

Now in the controller (or callback functions) we can access the associated properties of the nodes.

 _patterns.get({ ... callbacks : { success: function (results) { (function (model) { var fun = function () { var r = Math.round(19 * Math.random()), c = Math.round(3 * Math.random()); model.__rows__[r]['column_' + c] = (Math.random() * 1000).toFixed(4); model.__rows__[r]['background_' + c] = 'rgb(' + Math.round(255 * Math.random()) + ', ' + Math.round(255 * Math.random()) + ', ' + Math.round(255 * Math.random()) + ')'; setTimeout(fun, Math.ceil(50 * Math.random())); }; fun(); }(results.model)); } } }).render(); 


Look at the fly off the table here .

So the model object contains references to the associated values. Notice the __rows__ property. Through this construction __hook__, nesting levels of hooks are indicated. Since the data is not contained in the root template (the template table), but is nested in hook rows, it is possible to access it through model .__ rows__. The double underscore is introduced as a preventive measure against name conflicts.

If you remember, we linked INPUT.value with P.innerHTML in the authorization window template. In the callback function, we also get a reference to value.

 _patterns.get({ url : '/patterns/popup/pattern.html', node : document.body, hooks : { id : id, title : 'Test dialog window', content : _patterns.get({ url : '/patterns/patterns/login/pattern.html', hooks : { login : _patterns.get({ url : '/patterns/controls/textinput/pattern.html', hooks : { type: 'text', } }), password: _patterns.get({ url : '/patterns/controls/textinput/pattern.html', hooks : { type: 'password', } }), controls: _patterns.get({ url : '/patterns/buttons/flat/pattern.html', hooks : [{ title: 'login', id: 'login_button' }, { title: 'cancel', id: 'cancel_button' }] }), } }) }, callbacks: { success: function (results) { var instance = this, model = results.model; model.__content__.__login__.value = 'this new login'; } }, }).render(); 


With model figured out, but what is binds? And binds in their structure is the same as model, with one exception - “at the end” is not the value, but the methods.

 success: function (results) { var instance = this, dom = results.dom, binds = results.binds, id = null; //Add handle id = binds.__content__.__login__.value.addHandle(function (name, value) { var obj = this; }); //Remove handle binds.__content__.__login__.value.removeHandle(id); } 


And there are only two of them (methods):



As you may have guessed, the first attaches an event handler, and the second removes it. Thus, you can “hang” your function to any property of the model, which will work whenever this property changes.

DOM and map



Two more interesting objects are DOM and map.

 var dom = results.dom; var map = results.map; 


Let's change the button template for our login window a bit to demonstrate the capabilities of the dom object.

  <a data-type="Buttons.Flat" id="{{id}}" {{$button}}>{{title}}</a> 


So we added a {{$ button}} link to the button node. Thus, we have marked the node in relation to which patterns will create a collection of methods for working with this node.

 success: function (results) { var instance = this, dom = results.dom; dom.listed.__content__.__controls__[0].button.on('click', function () { alert('You cannot login. It\'s just test. Login is "' + model.__content__.__login__.value + '", and password is "' + model.__content__.__password__.value + '"'); }); dom.listed.__content__.__controls__[1].button.on('click', function () { alert('Do not close me, please.'); }); dom.grouped.__content__.__controls__.button.on('click', function () { alert('This is common handle for both buttons'); }); } 


As you can see, we got the opportunity to attach event handlers to form buttons. A complete list of all the methods out of the box can be found here . There is also a description of how to add your own methods.

Here I will only draw your attention to the fact that the dom object has two properties:



The first property contains grouped methods. That is, since we have two buttons on the form, when addressing, for example, the on method (attaching events), we will attach the event to two buttons at once. If we need access to each individual button, then we need to use the listed property.

In turn, the map object gives us the ability to quickly search for nodes, as it limits the search to the context of the template or its parts.

 success: function (results) { var instance = this, map = results.map, nodes = null; //Will find all P in whole popup nodes = map.__context.select('p'); //Will find all P inside popup in content area nodes = map.content.__context.select('p'); //Will find all P in textbox-control of login nodes = map.content.login.__context.select('p'); } 


That is, map.content.login .__ context.select ('p') will search for all paragraphs only within the part of the template that relates to the text field template defined for specifying the login.

You can use the map object to quickly search for nodes and get links to them.

Data exchange



Finally, the last object passed to the callback function is resources. Everything is simple - it is a data exchange mechanism. So, when drawing a template, you can define the resources property.

 _patterns.get({ url : '/patterns/popup/pattern.html', node : document.body, hooks : { id : id, title : 'Test dialog window', content : _patterns.get({ url : '/patterns/login/pattern.html', hooks : { login : _patterns.get({ url : '/patterns/controls/textinput/pattern.html', hooks : { type: 'text', } }), password: _patterns.get({ url : '/patterns/controls/textinput/pattern.html', hooks : { type: 'password', } }), controls: _patterns.get({ url : '/patterns/buttons/flat/pattern.html', hooks : [{ title: 'login', id: 'login_button' }, { title: 'cancel', id: 'cancel_button' }] }), }, }) }, resources: { field1 : 'one', field2 : 'two' }, callbacks: { success: function (results) { var instance = this, resources = results.resources; window.console.log(resources.field1); window.console.log(resources.field2); //Result in console: //one //two } }, }).render(); 


This is what will be passed to the callback function, as demonstrated in the example. Thus, you get the opportunity to exchange data between the moments: before drawing and after.

Conditions or changing patterns



In fact, this is the most interesting part of the patterns (from my point of view, of course), because the approaches proposed here may puzzle you a bit. But first things first.

So, a good template cannot be absolutely static and should somehow change depending on the data with which it will be drawn. An overwhelming number of template engines for this purpose use syntax mixing, inserting logic directly into the markup. This is done, for example, by the EJS mentioned at the beginning of the article.

 <ul> <% for(var i=0; i<supplies.length; i++) {%> <li><%= supplies[i] %></li> <% } %> </ul> 


To demonstrate how such problems are solved by patterns, let’s go back to our example authorization window and refine the text field template so that the user is displayed with a valid character in case of using it for a password.

 <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title>Flex.Template</title> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <link rel="stylesheet" type="text/css" href="pattern.css" /> <!--Attach JS file with condition-handle--> <script type="text/javascript" src="conditions.js"></script> </head> <body> <p>{{::value}}</p> <div data-type="TextInput.Wrapper"> <div data-type="TextInput.Container"> <input data-type="TextInput" type="{{type}}" value="{{::value}}" name="TestInput" {{$input}}/> </div> <!--type=password--> <div data-type="TextInput.Info.Icon"></div> <div data-type="TextInput.Info.Popup"> <p>You can use in password only letters, number and _</p> </div> <!--type--> </div> </body> </html> 


So, as you can see, we have added some new markup, namely:

 <!--[condition_name]=[condition_value]--> <div data-type="TextInput.Info.Icon"></div> <div data-type="TextInput.Info.Popup"> <p>You can use in password only letters, number and _</p> </div> <!--[condition_name]--> 


In this way, we can define conditions by framing the desired part of the markup in HTML comments.

Also, you could not overlook the attached JS file - conditions.js. Here is its content:

 _conditions({ type: function (data) { return data.type; } }); 


As you can see, there is defined a function (type) corresponding to the name of the condition in the markup.

So what happens after the updated authorization window template is rendered? The logic of action patterns will be fairly simple: by finding conditions in a text field template, patterns will try to find the type function (by the name of the condition). Having found this function, patterns will give it hook values ​​(the function argument is data). If this function returns the password value specified in the condition, then an additional part of the markup will be included in the template.

Here is a working example of our updated authorization window.

In addition, we can define the conditions not only in a separate file attached to the template, but also in time to draw it.

 _patterns.get({ url : '/patterns/popup/pattern.html', node : document.body, hooks : { id : id, title : 'Test dialog window', content : _patterns.get({ url : '/patterns/login/pattern.html', hooks : { login : _patterns.get({ url : '/patterns/controls/textinput/pattern.html', hooks : { type: 'text', }, conditions : { type: function (data) { return data.type; } }, }), password: _patterns.get({ url : '/patterns/controls/textinput/pattern.html', hooks : { type: 'password', }, conditions : { type: function (data) { return data.type; } }, }), controls: _patterns.get({ url : '/patterns/buttons/flat/pattern.html', hooks : [{ title: 'login', id: 'login_button' }, { title: 'cancel', id: 'cancel_button' }] }), }, }) }, }).render(); 


You probably now would like to say: the conditions in the template are implemented through the ass is somehow strange. Do not hurry. There are two serious motives.



To better understand the “second”, let's change the row pattern for our table.

 <tr> <td>{{column_0}}{{::column_0}}</td> <td>{{column_1}}{{::column_1}}</td> <td>{{column_2}}{{::column_2}}</td> <td> <div> <p>{{column_3}}{{::column_3}}</p> <!--value_sets=0--> <!--sub_value_sets=0--> <p>This value is less than 111</p> <!--sub_value_sets--> <!--sub_value_sets=0.5--> <p>This value is more than 111 and less than 222</p> <!--sub_value_sets--> <!--sub_value_sets=1--> <p>This value is more than 222 and less than 333</p> <!--sub_value_sets--> <!--value_sets--> <!--value_sets=0.5--> <p>This value is more than 333 and less than 666</p> <!--value_sets--> <!--value_sets=1--> <p>This value is more than 666 and less than 1000</p> <!--value_sets--> </div> </td> </tr> 


, ? -, .

 var conditions = { value_sets: function (data) { if (data.column_3 <= 333 ) { return '0'; } if (data.column_3 > 333 && data.column_3 <= 666 ) { return '0.5'; } if (data.column_3 > 666 ) { return '1'; } }, sub_value_sets: function (data) { if (data.column_3 <= 111 ) { return '0'; } if (data.column_3 > 111 && data.column_3 <= 222 ) { return '0.5'; } if (data.column_3 > 222 ) { return '1'; } }, }; conditions.value_sets. tracking = ['column_3']; conditions.sub_value_sets. tracking = ['column_0']; _conditions(conditions); 


: , , .

tracking patterns . , .

, .

 var data_source = []; for (var i = 0; i < 100; i += 1) { data_source.push({ column_0: (Math.random() * 1000).toFixed(4), column_1: (Math.random() * 1000).toFixed(4), column_2: (Math.random() * 1000).toFixed(4), column_3: (Math.random() * 1000).toFixed(4), }); } _patterns.get({ url : '/patterns/table/container/pattern.html', node : document.body, hooks : { titles : { column_0: 'Column #0', column_1: 'Column #1', column_2: 'Column #2', column_3: 'Column #3', }, rows : _patterns.get({ url: '/patterns/table/row_con/pattern.html', hooks: data_source, }) }, callbacks : { success: function (results) { (function (model) { var fun = function () { var r = Math.round(99 * Math.random()), c = Math.round(3 * Math.random()); model.__rows__[r]['column_' + c] = (Math.random() * 1000).toFixed(4); setTimeout(fun, Math.ceil(50 * Math.random())); }; fun(); }(results.model)); } } }).render(); 


, , 50 . . , , ( ). .



, , , , .

patterns :



, , .

patterns.

github'.

.

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


All Articles