📜 ⬆️ ⬇️

Introduction to the components of derby 0.6

image
I continue the series ( one , two , three , four ) posts on reactive fullstek javascript framework derbyjs . This time it will be about components (some analogue of directives in angular) - a great way to hierarchically build the interface, and split the application into modules.

General Component Information


Components in the Derby 0.6 are called derby-patterns, rendered in a separate scope. Let's figure it out. Suppose we have such a view file (I chose the same Todo-list for the demonstration - the to-do list from TodoMVC):

index.html
<Body:> <h1>Todos:</h1> <view name="new-todo"></view> <!--    --> <new-todo:> <form> <input type="text"> <button type="submit">Add Todo</button> </form> 


And Body: and new-todo: here are the templates, how to make the new-todo component? To do this, you need to register it in the derby application:
 app.component('new-todo', function(){}); 

That is, to associate a pattern with a certain function that will be responsible for it. There is simply no place (although the example is still completely useless). But what is this feature? As you know, functions in javascript can define a class. Class methods are placed in a prototype, this is used here.
')
Let's slightly expand the example - let's tie the input to the reactive variable and create an on-submit event handler. First, let's see how it would be if we didn’t have a component:
 <new-todo:> <form on-submit="addNewTodo()"> <input type="text" value="{{_page.new-todo}}"> <button type="submit">Add Todo</button> </form> 


 app.proto.addNewTodo = function(){ //... } 

What are the disadvantages here:
1. The global scope is obstructed (_page)
2. The addNewTodo function is added to app.proto - in a large application there will be noodles here.

How will it be if you make a new-todo component:
 <new-todo:> <form on-submit="addNewTodo()"> <input type="text" value="{{todo}}"> <button type="submit">Add Todo</button> </form> 

 app.component('new-todo', NewTodo); function NewTodo(){} NewTodo.prototype.addNewTodo = function(todo){ //     "scoped" //     ,   var todo = this.model.get('todo'); //... } 

So what has changed? Firstly, inside the new-todo template: now it has its own scope, the _page and all other global collections are not visible here. And, on the contrary, the todo path here is local, in the global scope it is not available. Inkapsulation is great. Secondly, the addNewTodo handler function is now also inside the NewTodo class without clogging up the app with its details.

So, derby-components are ui-elements, the purpose of which is in hiding the internal details of the work of a certain visual block. It is worth noting here, and it is important that the components do not involve loading data. Data must be loaded at the level of the controller that processes the url.

If the components are designed to conceal the internal kitchen, what kind of interface do they have? How are the parameters transmitted and the results obtained?

Parameters are passed the same way as in a regular template through attributes and in the form of nested html-content (more on this later). Results are returned by events.

A small demonstration on our example. Let's transfer to our component new-todo class and placeholder for the input field, and we will receive the entered value through the event:

index.html
 <Body:> <h1>Todos:</h1> <view name="new-todo" plaeholder="Input new Todo" inputClass="big" on-addtodo="list.add()"> </view> <view name="todos-list" as="list"></view> <new-todo:> <form on-submit="addNewTodo()"> <input type="text" value="{{todo}}" placeholder="{{@plaeholder}}" class="{{@inputClass}}"> <button type="submit">Add Todo</button> </form> <todos-list:> <!--    --> 

 app.component('new-todo', NewTodo); app.component('todos-list:', TodosList); function NewTodo(){} NewTodo.prototype.addNewTodo = function(todo){ var todo = this.model.get('todo'); //  ,     // (   ) this.emit('addtodo', todo); } function TodosList(){}; TodosList.prototype.add = function(todo){ //        //  .  ,   //       //    } 

Let's discuss all this and see what we have achieved.

Our new-todo component now takes 2 parameters: placeholder and inputClass and returns the addtodo event, we redirect this event to the todos-list component, where it is processed by TodosList.prototype.add. Notice that by creating an instance of the todos-list component we assigned an alias of list to it using the as keyword. That is why we were able to write list.add () in the on-addtodo handler.

Thus, new-todo is completely isolated and does not work with the external model, on the other hand, the todos-list component is fully responsible for the todos list. Duties are strictly separated.

Now it is worthwhile to dwell on the parameters passed to the component.

Interface component


It should be noted that the transfer of parameters to the components was inherited from the templates, so most of the functionality is similar (unless otherwise stated, I will give examples on the templates).

Note that templates (as well as components) in html derby files are similar to functions, they have a declaration, where the template itself is described. And there is also (possibly multiple) calling this template from other templates.

# Syntax of the template declaration (component) and what is content

 <name: ([element="element"] [attributes="attributes"] [arrays="arrays"])> 

The attributes element, attributes and array are optional. What do they mean? Consider the examples:

Element attribute

By default, the declaration and invocation of the template look something like this:
(Not yet arr)

 <!--   --> <nav-link:> <!--  $render.url   url  --> <li class="{{if $render.url === @href}}active{{/}}"> <a href="{{@href}}">{{@caption}}</a> </li> <!--     ,   Body: --> <view name="nav-link" href="/" caption="Home"></view> 


Doing so is not always convenient. Sometimes I would like to call a template not through a view tag with the appropriate name, but transparently, using the template name as the tag name. This is what the element attribute is for.

 <!--  ,       nav-link --> <nav-link: element="nav-link"> <li class="{{if $render.url === @href}}active{{/}}"> <a href="{{@href}}">{{@caption}}</a> </li> <!--  nav-link   ,   Body: --> <nav-link href="/" caption="Home"></nav-link> 


And you can even
 <nav-link href="/" caption="Home"/> 

In this version, we do not use the closing part of the tag, since we do not have the contents of the tag. And what is it?

Implicit content parameter


When calling a template, we use the view tag, or a tag named with the element attribute like this:

 <!--  --> <view name="nav-link" href="/" caption="Home"></view> <!--   --> <nav-link name="nav-link" href="/" caption="Home"></nav-link> <!--   --> <nav-link: element="nav-link"> <li class="{{if $render.url === @href}}active{{/}}"> <a href="{{@href}}">{{@caption}}</a> </li> 


It turns out that when you call, between the opening and closing parts of the tag, you can place some content, for example, text or some nested html. It will be passed inside the template by the implicit content parameter. In our example, let's replace caption using content :

 <!--  --> <view name="nav-link" href="/">Home</view> <!--   --> <nav-link name="nav-link" href="/">Home</nav-link> <!--    --> <nav-link name="nav-link" href="/"> <span class="img image-home"> Home </span> </nav-link> <!--   --> <nav-link: element="nav-link"> <li class="{{if $render.url === @href}}active{{/}}"> <a href="{{@href}}">{{@content}}</a> </li> 


It is very convenient, allows you to hide the details and greatly simplify the code of the top level.

The attributes attributes and arrays are directly related to this.

Attributes attribute

You can imagine the task when the html-code block transmitted to the template inside the template should not be inserted as a single unit in a specific place. Suppose there is some widget that has a header, footer, and main content. His call could be something like this:
 <widget> <header><--  --></header> <footer><--  --></footer> <body><--  --></body> </widget> 

And inside the widget template there will be some kind of complicated markup, where we should be able to insert all these 3 blocks separately, in the form of header , footer and body

For this you need attributes:
 <widget: attributes="header footer body"> <!--   --> <!--   --> {{@header}} <!--   --> <!--   --> {{@body}} <!--   --> {{@footer}} <!--   --> 

By the way, instead of body, it would be quite possible to use content, because everything that is not listed in attributes (and, in fact, still in arrays) falls into content:

 <Body:> <widget> <h1>Hello<h1> <header><--  --></header> <footer><--  --></footer> <p>text</text> </widget> <widget: attributes="header footer"> <!--   --> <!--   --> {{@header}} <!--   --> <!--   --> {{@content}} <!--    h1  p --> <!--   --> {{@footer}} <!--   --> 

There is one restriction here, everything that we have listed in attributes should be found in the internal block (inserted into the template) only once. And what if we need more? If we want, for example, to make our own implementation of the drop-down list and the list items can be a lot?

Arrays attribute


We make our drop-down list, we want the resulting template to take arguments like this:

 <dropdown> <option></option> <option class="bold"></option> <option></option> </dropdown> 

The markup inside the dropdown will be quite complicated, it means that just content will not suit us. Also, attributes will not work, because there is a restriction - there can be only one option element. For our case, using the arrays template attribute would be ideal:

 <dropdown: arrays="option/options"> <!--   --> {{each @options}} <li class="{{this.class}}"> {{this.content}} </li> {{}} <!--   --> 


As you probably noticed, the template declaration is given by 'arrays = "option / options"' - here there are two names:

1. option - this is what the html element will be called inside the dropdown when called
2. options - this will be the name of the array with the elements inside the template, the elements themselves inside this array will be represented by objects, where all the attributes of the option will become fields of the object, and its internal contents will become the content field.

Software component


As we already said, a template becomes a component if a constructor function is registered for it.

 <new-todo:> <form on-submit="addNewTodo()"> <input type="text" value="{{todo}}"> <button type="submit">Add Todo</button> </form> 

 app.component('new-todo', NewTodo); function NewTodo(){} NewTodo.prototype.addNewTodo = function(todo){ var todo = this.model.get('todo'); //... } 


A component has predefined functions that will be called at some point in the life of the component - it is create and init, there is also a 'destroy' event. It is also quite useful.

# init

The init function is called both on the client and on the server, before the component is rendered. Its purpose is to initialize the internal model of the component, set default values, create the necessary references (ref).

 //   https://github.com/codeparty/d-d3-barchart/blob/master/index.js function BarChart() {} BarChart.prototype.init = function() { var model = this.model; model.setNull("data", []); model.setNull("width", 200); model.setNull("height", 100); // ... }; 


# create

Called only on the client after rendering the component. Required for registering event handlers, connecting to a component of client libraries, subscriptions to changing data, launching reactive functions of a component, etc.
 BarChart.prototype.create = function() { var model = this.model; var that = this; // changes in values inside the array model.on("all", "data**", function() { //console.log("event data:", arguments); that.transform() that.draw() }); that.draw(); }; 


# 'destroy' event


Called when the component is destroyed, needed for final actions: disabling things like setInterval, disabling client libraries, etc.
 MyComponent.prototype.create = function(){ var intervalId = setIterval myFunc, 1000 this.on('destroy', function(){ clearInterval(intervalId); }); } 


What is available in this in component handlers?



In all component handlers in this are available: model, app, dom (except init), all aliases to dom elements, and components created inside the component, parent link to the parent component, and of course, everything we ourselves put in prototype component constructor function.

Model here with reduced scope. That is, through this.model, the component will only see the model of the component itself; if you need to access the global derby scope, use this.model.root, or this.app.model.

C app everything is clear, it is an instance of a derby application, through it there is a lot that can be done, for example:

 MyComponent.prototype.back = function(){ this.app.history.back(); } 


Through dom, you can hang handlers on DOM events (on, once, removeListener functions are available), for example:
 //  https://github.com/codeparty/d-bootstrap/blob/master/dropdown/index.js Dropdown.prototype.create = function(model, dom) { // Close on click outside of the dropdown var dropdown = this; dom.on('click', function(e) { if (dropdown.toggleButton.contains(e.target)) return; if (dropdown.menu.contains(e.target)) return; model.set('open', false); }); }; 


To fully understand this example, you need to keep in mind that this.toggleButton and this.menu are aliases for DOM elements, specified in the template as, as:

Look here: github.com/codeparty/d-bootstrap/blob/master/dropdown/index.html#L4-L11

All dom: on, once, removeListeners functions can take four parameters: type, [target], listener, [useCapture]. Target - the element on which the handler is hung (from which it is removed), if target is not specified, it is equal to document. The remaining 3 parameters are similar to the corresponding parameters of the usual addEventListener (type, listener [, useCapture])

The aliases on the dom-elements inside the template are set using the a keyword as:

 <main-menu:> <div as="menu"> <!-- ... --> </div> 


 MainMenu.prototype.hide = function(){ //   $(this.menu).hide(); } 


Removal of components from the application in a separate module



Before that, we considered only components whose templates were already inside any html files of the application. If it is necessary (and usually necessary) to completely separate the component from the application, the following is done:

A separate folder is created for the component, js, html are put into it, css files (there is a small feature with style files), the component is registered in the application using the app.component function into which only one parameter is passed - the constructor function. Something like this:

app.component (require ('../ components / dropdown'));

Note that earlier, when the component template was already present in the application's html files, the registration was different:

app.component ('dropdown', Dropdown);

Let's look at some example:

tabs / index.js
 module.exports = Tabs; function Tabs() {} Tabs.prototype.view = __dirname; Tabs.prototype.init = function(model) { model.setNull('selectedIndex', 0); }; Tabs.prototype.select = function(index) { this.model.set('selectedIndex', index); }; 

tabs / index.html
 <index: arrays="pane/panes" element="tabs"> <ul class="nav nav-tabs"> {{each @panes as #pane, #i}} <li class="{{if selectedIndex === #i}}active{{/if}}"> <a on-click="select(#i)">{{#pane.title}}</a> </li> {{/each}} </ul> <div class="tab-content"> {{each @panes as #pane, #i}} <div class="tab-pane{{if selectedIndex === #i}} active{{/if}}"> {{#pane.content}} </div> {{/each}} </div> 


Special attention should be paid to the line:
 Tabs.prototype.view = __dirname; 

From here derby will take the name of the component (it is also missing from the template itself, since it uses 'index:'). The algorithm is simple - the last segment of the path is taken. Suppose _dirname we have now is '/ home / zag2art / work / project / src / components / tabs', which means that in other templates this component can be accessed via 'tabs', for example:
 <Body:> <tabs selected-index="{{widgets.data.currentTab}}"> <pane title="One"> Stuff'n </pane> <pane title="Two"> More stuff </pane> </tabs> 

The very same connection of this component to the application will be as follows:
 app.component(require('../components/tabs')); 

It is very convenient to design components in the form of hotel npm modules, for example, www.npmjs.org/package/d-d3-barchart

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


All Articles