Sunday, November 8, 2015

Handlebars Essentials 2

This continues exploring Handlebars library.
Here you will see how to split markup into parts, with the goal to move templates and partials into their own units, and thus allow granular code management.
It will demonstrate how to build an MVC style framework of managing page elements, with pages being simple containers for subviews (hierarchies of views), and client-side logic in controllers being responsible for controlling a single view by feeding data from a model data source.

There is one deficiency here that will be tackled in the next post. It is about optimizing templates and speeding up page load by pre-compilation.


1. Index.html - this is a bare-bones host page for templated elements.
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Bunnies</title>
    <link rel="stylesheet" href="css/styles.css">
    <script src="lib/jquery/jquery-2.1.4.min.js"></script>
    <script src="lib/handlebars/handlebars-v4.0.4.js"></script>

    <script src="js/initModel.js"></script>
</head>

<body>
    <div id="main">
    </div>
    <script src="js/app.js"></script>
</body>
</html>


2. InitModel.js - loads and initializes a model object. It is a simple skeletal code to load from a local storage, initialize, and define basic operations on items.
(function() {

  var model = window.model = {};
  model.items = [];
  model.lang = 0;
  model.languages = [
    {
      langId: "lang0",
      siteTitle: "App Prototype (with Handlebars)",
      page1Name: "Home",
      page2Name: "Favorites",
      page3Name: "Contact us",
      lang1Name: "Eng",
      lang2Name: "C++",
      noItemsFound: "No items found",
      one: "Call one",
      two: "Call two",
      good: "Good for you",
      bad: "Bad for you"
    },
    {
      langId: "lang1",
      siteTitle: "template<T> void App (std::shared_ptr<Handlebars>);",
      page1Name: "home();",
      page2Name: "favorites();",
      page3Name: "info();",
      lang1Name: "std::shared_ptr<Eng>();",
      lang2Name: "this",
      noItemsFound: "NULL",
      one: "one();",
      two: "two();",
      good: "result = true;",
      bad: "result = false;"
    },
  ];

  // MARK: - System support functions.

  function parseUrl() {
    var queryParams = window.location.search.slice(1).split('&');
    var paramsObj = {};
    queryParams.forEach(function(val) {
      if (val) {
        var pair = val.split("=");
        paramsObj[pair[0]] = pair[1];
      }
    });
    return paramsObj;
  };

  // MARK: - Init functions.

  window.model.initLanguage = function() {
    var wasUnarchived = this.unarchiveLanguage();
    console.log(this.lang);

    var lang = parseUrl().lang;
    if (lang == undefined) {
      lang = this.lang;
    }
    console.log(lang);
    if (this.lang == lang) {
      return;
    }

    this.lang = lang;
    this.archiveLanguage();
  }

  function Item(id, image, name, isGood, moreInfo) {
    this.id = id;
    this.image = image;
    this.name = name;
    this.isGood = isGood;
    this.moreInfo = moreInfo;
    return this;
  }

  window.model.initItems = function() {
    if (this.unarchiveItems()) {
      return;
    }
    this.items = [
      new Item("bunny0", "bubby_bunny.jpg", "Bubby Bunny", false),
      new Item("bunny1", "cloudy_bunny.jpg", "Cloudy Bunny", false),
      new Item("bunny2", "meditating_cat.jpg", "Meditating Cat", true)
    ];
    this.archiveItems();
  }

  // MARK: - Item operations.

  window.model.chooseItem = function(itemId) {
    var item = this.findItem(itemId);
    if (item) {
      item.moreInfo = true;
      this.archiveItems();
    }
  }

  window.model.clearItem = function(itemId) {
    var item = this.findItem(itemId);
    if (item) {
      item.moreInfo = false;
      this.archiveItems();
    }
  }

  window.model.findItem = function(itemId) {
    var item;
    this.items.forEach(function(val) {
      if (val.id === itemId) {
        item = val;
      }
    });
    return item;
  }

  // MARK: - Archiver.

  window.model.archive = function() {
    this.archiveLanguage();
    this.archiveItems();
  }

  window.model.unarchive = function() {
    var result = this.unarchiveLanguage()
              && this.unarchiveItems();
    return result;
  }

  window.model.archiveLanguage = function() {
    window.localStorage.setItem("model.language", JSON.stringify(this.lang));
  }

  window.model.archiveItems = function() {
    window.localStorage.setItem("model.items", JSON.stringify(this.items));
  }

  window.model.unarchiveLanguage = function() {
    var data = window.localStorage.getItem("model.language");
    if (!data) {
      return false;
    }
    //console.log(data);
    this.lang = JSON.parse(data);
    return true;
  }

  window.model.unarchiveItems = function() {
    var data = window.localStorage.getItem("model.items");
    if (!data) {
      return false;
    }
    var json = JSON.parse(data);

    json.forEach(function(val) {
      model.items.push(new Item(val.id,
        val.image, val.name, val.isGood, val.moreInfo));
    });
    return true;
  }

  model.initLanguage();
  model.initItems();

})();


3. App.js - loads templates and binds controls. Note that it parallelizes loading for the three templates, and each loaded template is then compiled in the same async context because there is no need to involve a window yet. Then the loader awaits on all three to finish, and finally, after the document is ready, renders templates into markup by supplying a model. In this scenario the single app.js handles all template logic, and if needed the more involved code can be moved to separate units, that can be named to match their templates.
(function() {

  var indexTemplate = null;
  var itemsTemplate = null;
  var itemTemplate = null;

  loadTemplatesAndRender();

  function loadTemplatesAndRender() {
    var t1 = $.get("templates/index.hbs", function (data) {
      var compiled = Handlebars.compile(data);
      indexTemplate = compiled;
    }, "html");
    var t2 = $.get("templates/items.hbs", function (data) {
      var compiled = Handlebars.compile(data);
      itemsTemplate = compiled;
    }, "html");
    // Note, the item template is a partial,
    // compiling just in case it’s used as a standalone template.
    var t3 = $.get("templates/item.hbs", function (data) {
      Handlebars.registerPartial("item", data);
      var compiled = Handlebars.compile(data);
      //console.log(compiled);
      itemTemplate = compiled;
    }, "html");
    // Wait for templates to finish loading.
    $.when(t1, t2, t3).done(function() {
      // Also wait for $(document).ready().
      $(function() {
        //alert("ALL DONE");
        renderPage();
        renderItems();
      })
    })
  }

  function renderPage() {
    var compiled = indexTemplate;
    // language is set by the initModel.
    var rendered = compiled(model.languages[model.lang]);
    $("#main").html(rendered);
  }

  function renderItems() {
    var compiled = itemsTemplate;
    // items and a language are set by the initModel.
    var rendered = compiled({
      items: model.items,
      language: model.languages[model.lang]
    });
    $("#theItems").html(rendered);
    attachButtons();
  }

  function attachButtons() {
    $(".button-one").click(function() {
      var id = $(this).closest(".item-card").data("item-id");
      model.chooseItem(id);
      renderItems();
    });

    $(".button-two").click(function() {
      var id = $(this).closest(".item-card").data("item-id");
      model.clearItem(id);
      renderItems();
    });
  }

})();


4. Index.hbs - the top-level template. The binding tags will be replaced by texts from the model.languages[lang] dictionary. And “theItems” element will host renderings of the deeper levels of templates.
<div class="header">
  <div class="title">
      <h1>{{siteTitle}}</h1>
  </div>
  <div>
    <nav class="navigation">
      <a href="?option=page1">{{page1Name}}</a>
      <a href="?option=page2">{{page2Name}}</a>
      <a href="?option=page3">{{page3Name}}</a>
    </nav>
  </div>
</div>
<div id="languageSwitch">
  <nav class="navigation">
    <a href="?lang=0">{{lang1Name}}</a>
    <a href="?lang=1">{{lang2Name}}</a>
  </nav>
</div>
<div class="content">
  <div id="theItems">
  </div>
  <footer class="footer">
  </footer>
</div>


5. Items.hbs - a template-iterator to render all items from the model.items array. It is using a partial to render individual items. In this case the partial is hard-coded and accepts a language object from the root template. For a conditional template selection a helper function can be used (as described in prior post).
{{#each items}}
  {{> item language=@root.language}}
{{else}}
  {{language.noItemsFound}}
{{/each}}


6. Item.hbs - a final template to render an individual item’s card. It has a simple markup to present each item’s name, picture, additional info, and two buttons. The placeholders refer to properties of an item. Handlebars will assign the source item as a context to this template.
<div class="item-card" data-item-id="{{id}}">
  <div class="card-title" style="background: url('images/{{image}}') center 15% no-repeat #46B6AC;">
      <h2>{{name}}</h2>
  </div>
  <div class="card-border">
    <button class="button-one">
      {{language.one}}
    </button>
    <button class="button-two">
      {{language.two}}
    </button>
  </div>
  {{#if moreInfo}}
    {{#if isGood}}
      <span class="result-good">{{language.good}}</span>
    {{else}}
      <span class="result-bad">{{language.bad}}</span>
    {{/if}}
  {{/if}}
</div>



No comments:

Post a Comment