• ¶

    tectonic.js

  • npm install tectonic.js
  • ¶

    Introduction

  • ¶

    Tectonic is a functional rendering engine for DOM nodes, heavily inspired by Beebole’s PURE rendering engine. It ascribes to PURE’s unobtusive philosophy, whereby HTML code is completely free of any application logic or new syntax and JavaScript code is uninhibited by presentational concerns. This is achieved by both PURE and Tectonic by the use of a directive object that marries the DOM referenced by CSS selectors to properties in your application’s data. Where Tectonic departs from PURE is in the use of functions, known as renderers, to directly manipulate DOM nodes. This permits Tectonic to provide two-way data flow via a parse method, which makes use of an inverse function that is attached to a renderer.

  • ¶
    Fork me on GitHub. GitHub Forks

    git clone https://github.com/tecknack/tectonic.git
  • ¶

    Run the test suite and gauge its coverage. Build status Coverage via Codecov

    npm test
  • ¶

    To truly understand Tectonic read the annotated source code.

    more tectonic.js
  • ¶

    Perhaps the best way to explain Tectonic is by example.

  • ¶

    Examples

    Basics

    Without any plugins, all functionality is accessible from the Tectonic object.

  • Hi there!
  • Example 1
    ¶

    The simplest usage is autoRender. Here we have a HTML snippet; the class name has been carefully chosen to match a property in our data.

    new Tectonic(document.getElementById('example-1'))
    .autoRender(
      {
        'hello': 'Hello, World!'
      }
    );
  • ¶

    This results in the value from our data subsituted in the chosen DOM node.

    <span class="hello">Hello, World!</span>
  • Hi there!
  • Example 2
    ¶

    That's all very well and good but autoRender is not very flexible (not to mention inefficient). Here we have the same HTML snippet; but instead of our data needing to match the DOM, we pass a directive with the data to render in order to do the equivalent matching.

    new Tectonic(document.getElementById('example-2'))
    .render(
      {
        'message': 'Hello, World!'
      },
      {
        '.hello': 'message'
      }
    );
  • ¶

    The result is the same.

    <span class="hello">Hello, World!</span>
  • Hi there!
  • Example 3
    ¶

    Directives can also be precompiled. Again here's the same example but first we compile our directive.

    var t = new Tectonic(document.getElementById('example-3'));
    var template = t.compile(
      {
        '.hello': 'message'
      }
    );
    t.render(
      {
        'message': 'Hello, World!'
      },
      template
    );
  • ¶

    No surprises here.

    <span class="hello">Hello, World!</span>
  • Hi there!
  • Example 4
    ¶

    The precompiled template can also be used independently.

    var t = new Tectonic(document.getElementById('example-4'));
    var template = t.compile(
      {
        '.hello': 'message'
      }
    );
    template(
      {
        'message': 'Hello, World!'
      }
    );
  • ¶

    But this time it returns a new node, detached from the DOM! It's up to you to do what you want with it.

    <span class="hello">Hi there!</span>
  • Hi there!
  • Example 5
    ¶

    We've seen how to put data into the DOM for the simplest use case, but Tectonic can also get data out of the DOM too. Note that this is entirely independent from render, we are not using any tricks like storing data in the DOM during rendering.

    new Tectonic(document.getElementById('example-5'))
    .parse(
      {
        '.hello': 'message'
      }
    );
  • ¶

    The return value from parse is a plain old object.

    {
      "message": "Hi there!"
    }
  • Hi there!
  • Example 6
    ¶

    We can use parse with precompiled templates too.

    var t = new Tectonic(document.getElementById('example-6'));
    var template = t.compile(
      {
        '.hello': 'message'
      }
    );
    t.parse(template);
  • ¶

    The return value is the same.

    {
      "message": "Hi there!"
    }
  • Hi there!
  • Example 7
    ¶

    The trick here is that compile exports an inverse function on its returned function.

    Here we also use get to return the original DOM node that was given to Tectonic.

    var t = new Tectonic(document.getElementById('example-7'));
    var template = t.compile(
      {
        '.hello': 'message'
      }
    );
    template.inverse(t.get());
  • ¶

    Again, the return value is the same.

    {
      "message": "Hi there!"
    }
  • ¶

    Selectors

    As you've already seen a directive is a plain old object that tells Tectonic where to find the data (the object's values) and where to put it in the DOM (the object's key). The following examples explores the latter, that is the directive's selectors.

  • Example 8
    ¶

    As you've already guessed, the selector of a directive can be any valid selector accepted by querySelector.

    new Tectonic(document.getElementById('example-8'))
    .render(
      {
        'element': 'Element matched by name.',
        'class': 'Element matched by class name.',
        'attribute': 'Element matched by attribute.',
        'pseudo-class': 'Element matched by pseudo-class'
      },
      {
        'p': 'element',
        '.msg': 'class',
        '[data-msg]': 'attribute',
        ':last-child': 'pseudo-class'
      }
    );
  • ¶

    But don't be fooled the selectors are not CSS selectors.

    <p>Element matched by name.</p>
    <p class="msg">Element matched by class name.</p>
    <p data-msg="example">Element matched by attribute.</p>
    <p>Element matched by pseudo-class</p>
  • Let's get solarized!

  • Example 9
    ¶

    Tectonic allows you to update attributes in the DOM by using a special @attribute syntax.

    new Tectonic(document.getElementById('example-9'))
    .render(
      {
        'theme': 'solarized-dark'
      },
      {
        'p@class': 'theme'
      }
    );
  • ¶

    The attribute value is replaced.

    <p class="solarized-dark">Let's get solarized!</p>
  • Let's get

  • Example 10
    ¶

    Replacing values isn't always what you want, so Tectonic interprets the pseudo-classes :before and :after differently.

    new Tectonic(document.getElementById('example-10'))
    .render(
      {
        'theme': 'solarized-dark',
        'name': 'Solarized (Dark)'
      },
      {
        'p@class:before': 'theme',
        'p:after': 'name'
      }
    );
  • ¶

    This time the original attribute value is retain and added to.

    <p class="solarized-dark background">Let's get Solarized (Dark)</p>
  • I'm solarized

    I'm not

  • Example 11
    ¶

    There's an extra pseudo-class, :toggle.

    new Tectonic(document.getElementById('example-11'))
    .render(
      {
        'theme': 'solarized-dark',
        'message': "No you're not, I am"
      },
      {
        'p@class:toggle': 'theme',
        ':last-child': 'message'
      }
    );
  • ¶

    If the class was already there, it gets removed, otherwise it gets added.

    <p class="">I'm solarized</p>
    <p class="solarized-dark">No you're not, I am</p>
  • Example 12
    ¶

    Input elements and dropdown boxes are treated specially. Since all input elements should be empty, Tectonic updates the value property instead of its contents. Remember to use @checked for checkbox and radio input fields. Similarly, @selected should be used with option elements.

    new Tectonic(document.getElementById('example-12'))
    .render(
      {
        'latest': '0.1.2',
        'ready': 1,
        'isMaster': 1
      },
      {
        '[name="version"]': 'latest',
        '[name="published"]@checked': 'ready',
        '[value="master"]@selected': 'isMaster'
      }
    );
  • ¶

    Notice that the input element doesn't receive a value attribute, this is because only the value property of the DOM object is set. To set the value attribute of the element use a @value selector.

    <input type="text" name="version">
    <input type="checkbox" name="published" checked="1">
    <select name="branch">
      <option value="gh-pages">github.io</option>
      <option value="master" selected="1">master</option>
    </select>
  • Let's get solarized!

  • Example 13
    ¶

    If you want your selector to match the top level element (that is to say the element being rendered) then use . or an empty string.

    Oh, and you can use arrays as data too.

    new Tectonic(document.getElementById('example-13'))
    .render(
      [ 'a', 'b', 'c', 'd', 'e', 'f' ],
      {
        '.': '0',
        '': '1',
        '.:after': '2',
        ':after': '3',
        '.@data-e': '4',
        '@data-f': '5'
      }
    );
  • ¶

    None of the examples actually show the top level container, so you'll just have to open the console and try it for yourself.

    bcd
  • ¶

    Data

    We've explored the keys of a directive object, so let's now turn our attention to how Tectonic finds the data (the directive object's values).

  • {
      : 
    }
  • Example 14
    ¶

    Thus far we have always used the name of the property in the data object to specify the data, but we can also use a constant string or a combination of both.

    The first value of this directive contains the string 'message', Tectonic see the single quotes and interprets the value as a string literal. The second value of the directive uses the same technique but this time Tectonic sees the double quotes as the string delimiters and interpolates the value property of the data in the middle.

    new Tectonic(document.getElementById('example-14'))
    .render(
      {
        'value': 'Hello, World!'
      },
      {
        '.hljs-attr': "'message'",
        '.hljs-string': '"\'" value "\'"'
      }
    );
  • ¶

    The result is message inside the hljs-attr element and 'Hello, World!' inside the hljs-string element.

    <pre><code class="json hljs">{
      <span class="hljs-attr">message</span>: <span class="hljs-string">'Hello, World!'</span>
    }</code></pre>
  • ,
  • Example 15
    ¶

    Nested objects can be accessed with either a dot sepatated string or an array.

    new Tectonic(document.getElementById('example-15'))
    .render(
      {
        nested: {
         greeting: 'Hello',
         '.': 'World'
        }
      },
      {
        '.string': 'nested.greeting',
        '.array': ['nested', '.']
      }
    );
  • ¶

    Note this won't work for objects that have an empty string as a key.

    <span class="string">Hello</span>,
    <span class="array">World</span>
  • ¶

    Loops

  • Example 16
    ¶

    If you provide an object then Tectonic will create a loop.

    Your object must have one key that contains a left pointing arrow, <-, on the righthand side of the arrow provide the key of the property in your data that Tectonic should loop over and on the lefthand side specify a name that you can use to refer to items in the loop from within a nested directive. The value of this special key is the nested directive.

    new Tectonic(document.getElementById('example-16'))
    .render(
      {
        libs: [
          { name: "PURE", url: "https://beebole.com/pure" },
          { name: "Tectonic.js", url: "http://tecknack.github.io/tectonic" },
          { name: "jQuery", url: "http://jquery.com" }
        ]
      },
      {
        'li': {
          'lib<-libs': {
            'a': 'lib.name',
            'a@href': 'lib.url'
          }
        }
      }
    );
  • ¶

    The element selected by the outer directive is repeated as necessary and the nested directive is applied to each, once per item in the array. We have told Tectonic that that item will be known as lib from within the nested directive.

    <ul>
      <li><a target="_blank" href="https://beebole.com/pure">PURE</a></li>
      <li><a target="_blank" href="http://tecknack.github.io/tectonic">Tectonic.js</a></li>
      <li><a target="_blank" href="http://jquery.com">jQuery</a></li>
    </ul>
  • Example 17
    ¶

    Next to the <- key, a filter function can be provided. For each item in the array, this function is called with the item, the item's index and the array itself and if it returns a truthy value the item will included in the loop, otherwise it will be skipped

    new Tectonic(document.getElementById('example-17'))
    .render(
      {
        libs: [
          { name: "PURE", url: "https://beebole.com/pure" },
          { name: "Tectonic.js", url: "http://tecknack.github.io/tectonic" },
          { name: "jQuery", url: "http://jquery.com" }
        ]
      },
      {
        'li': {
          'lib<-libs': {
            'a': 'lib.name',
            'a@href': 'lib.url'
          },
          filter: function(lib) {
            return lib.url.indexOf('http:') >= 0;
          }
        }
      }
    );
  • ¶

    The library with https URL is not rendered.

    <ul>
      <li><a target="_blank" href="http://tecknack.github.io/tectonic">Tectonic.js</a></li>
      <li><a target="_blank" href="http://jquery.com">jQuery</a></li>
    </ul>
  • Example 18
    ¶

    Next to the <- key, a sort function can be provided. This function is passed directly to Array.prototype.sort.

    new Tectonic(document.getElementById('example-18'))
    .render(
      {
        libs: [
          { name: "PURE", url: "https://beebole.com/pure" },
          { name: "Tectonic.js", url: "http://tecknack.github.io/tectonic" },
          { name: "jQuery", url: "http://jquery.com" }
        ]
      },
      {
        'li': {
          'lib<-libs': {
            'a': 'lib.name',
            'a@href': 'lib.url'
          },
          sort: function(libA, libB) {
            var nameA = libA.name.toUpperCase();
            var nameB = libB.name.toUpperCase();
            return nameA < nameB ? -1 : nameA > nameB ? 1 : 0;
          }
        }
      }
    );
  • ¶

    The jQuery library is rendered first.

    <ul>
      <li><a target="_blank" href="http://jquery.com">jQuery</a></li>
      <li><a target="_blank" href="https://beebole.com/pure">PURE</a></li>
      <li><a target="_blank" href="http://tecknack.github.io/tectonic">Tectonic.js</a></li>
    </ul>
  • Example 19
    ¶

    The minimal looping specifier is <- and means that the data object is expected to be an array and the nested directive can access the properties of the items directly.

    new Tectonic(document.getElementById('example-19'))
    .render(
      [
        { name: "PURE", url: "https://beebole.com/pure" },
        { name: "Tectonic.js", url: "http://tecknack.github.io/tectonic" },
        { name: "jQuery", url: "http://jquery.com" }
      ],
      {
        'li': {
          '<-': {
            'a': 'name',
            'a@href': 'url'
          }
        }
      }
    );
  • ¶

    The result is the same as example 16.

    <ul>
      <li><a target="_blank" href="https://beebole.com/pure">PURE</a></li>
      <li><a target="_blank" href="http://tecknack.github.io/tectonic">Tectonic.js</a></li>
      <li><a target="_blank" href="http://jquery.com">jQuery</a></li>
    </ul>
  • ¶

    Formatting Functions

  • Example 20
    ¶

    Let's see what happens if we provide a function.

    The function is passed the data object and the target element, which in this case is #example-20.

    new Tectonic(document.getElementById('example-20'))
    .render(
      1.23,
      {
        'li': function(x, target) {
          return x.toFixed(1);
        }
      }
    );
  • ¶

    The returned value from the function is subsituted into the DOM.

    <ul>
      <li>1.2</li>
    </ul>
  • Example 21
    ¶

    Functions are best without side-effects, but we all need a little global state at times.

    By default formatting functions are bound to the Tectonic instance, but you can set the context to a different object. Then this will be bound to your provided context object from inside your formatting function. The same is also true of parsing functions.

    Typically, a plugin will define the context. For example, a Backbone plugin may set the context to be the view that owns the target element being rendered.

    new Tectonic(document.getElementById('example-21'))
    .context({
      baseUrl: 'http://tecknack.github.io/tectonic'
    })
    .render(
      {
        links: [
          {
            name: 'Test suite',
            href: '/spec/index.html'
          },
          {
            name: 'Code coverage',
            href: '/coverage/Chrome%2043.0.2357%20(Linux%200.0.0)/tectonic/index.html'
          },
          {
            name: 'Source code',
            href: '/docs/tectonic.html'
          }
        ]
      },
      {
        'li': {
          '<-links': {
            'a': 'name',
            'a@href': function(link) {
              return this.baseUrl + link;
            }
          }
        }
      }
    );
  • ¶

    <ul>
      <li><a href="http://tecknack.github.io/tectonic/spec/index.html">Test suite</a></li>
      <li><a href="http://tecknack.github.io/tectonic/coverage/Chrome%2043.0.2357%20(Linux%200.0.0)/tectonic/index.html">Code coverage</a></li>
      <li><a href="http://tecknack.github.io/tectonic/docs/tectonic.html">Source code</a></li>
    </ul>
  • Example 22
    ¶

    The function in the previous example will only be called once, regardless of the number of elements that match the selector. But if this is the case, we can still control the output by returning a function.

    This function will be called once per matching element, and that element will be passed to the function as the second argument (the first argument is the data object), the third argument will be the index of the element within the set of matched elements and the fourth argument is an array of all the matched elements.

    new Tectonic(document.getElementById('example-22'))
    .render(
      1.23,
      {
        'li': function() {
          var i = 0;
          return function(x, li) {
            return x.toFixed(i++);
          };
        }
      }
    );
  • ¶

    The closure has been used to vary the precision, but the third argument could have also been used.

    <ul>
      <li>1</li>
      <li>1.2</li>
      <li>1.23</li>
      <li>1.230</li>
    </ul>
  • Example 23
    ¶

    Providing a function in a looping directive is arguably more useful.

    During a loop the function is given an item from the data array as the first argument, an element that corresponds to that item as the second, the index of the item in the array as the third, the fourth is the set of all matchings elements (the length of this array is the same as the length of the data array) and lastly the data array is provided.

    new Tectonic(document.getElementById('example-23'))
    .render(
      [0.68, 0.92, 0.71, 0.84],
      {
        'li': {
          '<-': {
            '': function(x, element, i, elements, values) {
              return x.toFixed(i);
            }
          }
        }
      }
    );
  • ¶

    The function has been called once per item in the array.

    <ul>
      <li>1</li>
      <li>0.9</li>
      <li>0.71</li>
      <li>0.840</li>
    </ul>
    • Example 24
      ¶

      We can even use a precompiled renderer as a function. Notice that we must use template.apply to avoid the "chicken or the egg" problem.

      Note that the ul has been used as the target of the renderer, this is to ensure we don't end up with many #example-24 elements.

      var t = new Tectonic($('#example-24 ul'))
      var template = t.compile({
        'li': {
          '<-children': {
            'a': 'name',
            '.children': function() {
              return template.apply(void 0, arguments);
            }
          }
        }
      });
      t.render(
        { "children": [
            { "name": "Europe",
              "children": [
                { "name": "Belgium",
                  "children": [
                    { "name": "Brussels",
                      "children": null
                    },
                    { "name": "Namur" },
                    { "name": "Antwerpen" }
                  ]
                },
                { "name": "Germany" },
                { "name": "UK"}
              ]
            },
            { "name": "America",
              "children": [
                { "name": "US",
                  "children": [
                    { "name": "Alabama" },
                    { "name": "Georgia"}
                  ]
                },
                { "name": "Canada" },
                { "name": "Argentina" }
              ]
            },
            { "name": "Asia" },
            { "name": "Africa" },
            { "name": "Antarctica" }
          ]
        },
        template
      );
    • ¶

      The render has added comments where li elements would have gone if there was more data. This is in case you want to execute the renderer again when you get more data.

      <ul class="children">
        <li>
          <a class="name">Europe</a>
          <ul class="children">
            <li>
              <a class="name">Belgium</a>
              <ul class="children">
                <li>
                  <a class="name">Brussels</a>
                  <ul class="children">
                    <!--li-->
                  </ul>
                </li>
                <li>
                  <a class="name">Namur</a>
                  <ul class="children">
                    <!--li-->
                  </ul>
                </li>
                <li>
                  <a class="name">Antwerpen</a>
                  <ul class="children">
                    <!--li-->
                  </ul>
                </li>
              </ul>
            </li>
            <li>
              <a class="name">Germany</a>
              <ul class="children">
                <!--li-->
              </ul>
            </li>
            <li>
              <a class="name">UK</a>
              <ul class="children">
                <!--li-->
              </ul>
            </li>
          </ul>
        </li>
        <li>
          <a class="name">America</a>
          <ul class="children">
            <li>
              <a class="name">US</a>
              <ul class="children">
                <li>
                  <a class="name">Alabama</a>
                  <ul class="children">
                    <!--li-->
                  </ul>
                </li>
                <li>
                  <a class="name">Georgia</a>
                  <ul class="children">
                    <!--li-->
                  </ul>
                </li>
              </ul>
            </li>
            <li>
              <a class="name">Canada</a>
              <ul class="children">
                <!--li-->
              </ul>
            </li>
            <li>
              <a class="name">Argentina</a>
              <ul class="children">
                <!--li-->
              </ul>
            </li>
          </ul>
        </li>
        <li>
          <a class="name">Asia</a>
          <ul class="children">
            <!--li-->
          </ul>
        </li>
        <li>
          <a class="name">Africa</a>
          <ul class="children">
            <!--li-->
          </ul>
        </li>
        <li>
          <a class="name">Antarctica</a>
          <ul class="children">
            <!--li-->
          </ul>
        </li>
      </ul>
    • ¶

      Copyright © 2015-2016 Corin Lawson.