Introduction to derby 0.6 components

    image
    I continue the series of ( one , two , three , four ) posts on the reactive full stack javascript derbyjs framework . This time we will talk about components (a certain analog of directives in the hangar) - an excellent way to hierarchically build interfaces, and partition applications into modules.

    General Component Information


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

    index.html

    Todos:



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

    That is, to match the template with a certain function that will be responsible for it. Nowhere is simpler (although the example is still completely useless). But what is this function? As you know, functions in javascript can define a class. The class methods are placed in the prototype, this is used here.

    Let's expand the example a little bit - bind input to a reactive variable and create an on-submit event handler. First, let's see how it would be if we did not have a component:


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

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

    How will it be if you make new-todo a component:

    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, _page and all other global collections are not visible here. And, on the contrary, the todo path is local here, it is not accessible in the global scope. Encapsulation is great. Secondly, the addNewTodo handler function is now also inside the NewTodo class without clogging the app with its details.

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

    If the components are designed to conceal the inner kitchen, what interface do they have? How are parameters passed to them and get results?

    Parameters are transferred in the same way as in a regular template through attributes and in the form of embedded html content (more on this later). Results are returned using events.

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

    index.html

    Todos:


    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 accepts 2 parameters: placeholder and inputClass and returns the “addtodo” event, we redirect the todos-list component to this event, where TodosList.prototype.add processes it. Note that when creating an instance of the todos-list component, we assigned it an alias list using the as keyword. That is why in the on-addtodo handler we were able to write list.add ().

    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. Responsibilities are strictly divided.

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

    Component interface


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

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

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


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

    Element attribute

    By default, the declaration and call of the template look something like this:
    (Not yet mod.)

  • {{@caption}}


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

  • {{@caption}}


  • Is it even possible

    In this case, 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 parameter content


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

  • {{@caption}}


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

    HomeHome
        Home
      
  • {{@content}}


  • This is very convenient, allows you to hide details and greatly simplify the top-level code.

    The attributes attributes and arrays are directly related to this.

    Attributes attribute

    You can imagine the problem when a block of html code transferred to a template inside a template does not have to be inserted into a specific place as a single block. Let's say there is some kind of widget that has header, footer and main content. His call could be something like this:
    <-- содержимое -->
    <-- содержимое -->
    <-- содержимое -->

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

    For this we need attributes:
    
       
       
         {{@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 (well, and, in fact, back in arrays) falls into content:

    Hello

    <-- содержимое -->
    <-- содержимое -->

    text {{@header}} {{@content}} {{@footer}}


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

    Arrays attribute


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


    The markup inside the dropdown will be quite complicated, which means that simply content will not work for us. Attributes will not work either, because there is a restriction - there can be only one option element. For our case, the use of the arrays template attribute would be ideal:

    
      
      {{each @options}}
        
  • {{this.content}}
  • {{}}


    As you probably noticed when declaring the template, 'arrays = "option / options" is set - here are two names:

    1. option - this will be the name of the html element inside dropdown when called
    2. options - this is 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 content will become the content field.

    Component software


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


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


    The component has predefined functions that will be called at some points in the life of the component - this 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 rendering the component. Its purpose is to initialize the internal model of the component, set default values, create the necessary links (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. It is necessary for registering event handlers, connecting client libraries to the component, subscribing to data changes, launching the reactive functions of the 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();
    };
    


    # event 'destroy'


    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, the following are available: model, app, dom (except init), all aliases to dom elements, and components created inside the component, parent-reference to the parent-component, well, of course, everything that we put into prototype of the component constructor function.

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

    Everything is clear with the app, this is an instance of a derby application, there is a lot to do through it, for example:

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


    Through dom, you can attach handlers to 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 the aliases for DOM elements defined in the template via 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])

    Aliases on dom elements inside the template are set using the as keyword:



    MainMenu.prototype.hide = function(){
      // Например так
      $(this.menu).hide();
    }
    


    Removing components from the application into a separate module



    Prior to this, we considered only components whose templates were already inside any html-files of the application. If you need (and usually need) to completely separate the component from the application, the following is done:

    A separate folder is created for the component, js, html, css files are placed in it (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, earlier, when the component template was already present in the html-files of the application, 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
    {{each @panes as #pane, #i}}
    {{#pane.content}}
    {{/each}}


    Pay special attention to the line:
    Tabs.prototype.view = __dirname;
    

    From here, derby will take the name of the component (it is also absent in the template itself, since 'index:' is used there). The algorithm is simple - the last segment of the path is taken. Let's say _dirname is now equal to '/ home / zag2art / work / project / src / components / tabs', which means that in other templates this component can be accessed via 'tabs', for example like this:
    
          Stuff'n
        
          More stuff
        

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

    It is very convenient to design components as separate npm modules, for example, www.npmjs.org/package/d-d3-barchart

    Only registered users can participate in the survey. Please come in.

    Well, is it worth it to continue the series?

    • 89.8% yes 89
    • 10.1% no 10

    Also popular now: