Working with views in Griffin.Yo

Griffin.Yo is a SPA library written in typescript. My goal is to create a simple library which is easy to get started with, but powerful enough to support building complex web applications. This post will show you how you can work with views in Griffin.Yo.

Views are simple HTML pages. They do not contain or facilitate a template language. All bindings are instead made with the help of different attributes. The bindings are simple. You just map your JSON object property names to the elements.

A simple example:

<div id="YourView">
    <div data-name="FirstName"></div>
</div>

To render the view you use the ViewRender class.

var renderer = new ViewRenderer("YourView");
renderer.render({ FirstName: "jonas" });

//or in the view model:
context.Render({ FirstName: "jonas" });

Customizing rendering

If you want to adjust something you need to use directives when calling the render method. For instance if you want to populate a link you might want to change both the URL and the link text.

View:

<div id="YourView">
    <a href="" data-name="UserId"></a>
</div>

Code:

var dto = { UserId: 1, UserName: "Jonas" };
var directive = { 
    UserId: {
        href: function(value) {
            return "#/user/" + value;
         },
         text: function(value, parentObject) {
             return parentObject.UserName;
         }
    }
};

var renderer = new ViewRenderer("YourView");
renderer.render(dto, directive);

The directive object is an mirror of the data structure, but property for each HTML attribute which should be set. The only exceptions is for “text” and “html” which corresponds to htmlElement.innerText and htmlElement.innerHTML. Thus you are free to add any amount of functions to a directive node (one per HTML attribute).

To illustrate that let’s go wild:

<div id="YourView">
    <a href="" data-name="UserId"></a>
</div>

Code:

var dto = { UserId: 1, UserName: "Jonas" };
var directive = { 
    UserId: {
        href: function(value) {
            return "#/user/" + value;
         },
         class: function() {
            return 'active';
         },
         "data-pioneer": function(value) {
             if (value < 10) {
                 return "true";
             } else {
                 return "false";
             }
         }
         text: function(value, parentObject) {
             return parentObject.UserName;
         }
    }
};

The generated HTML would look like:

<div id="YourView">
    <a href="#/user/1" data-name="UserId" data-pioneer="true" class="active">Jonas</a>
</div>

Aggregation

You can also use directives to expose an aggregate result of other fields.

Say that you got a view containing the field “FullName”:

<div id="YourView">
    <h1 data-name="FullName"></h1>
</div>

.. but the data returned from the server only got “FirstName” and “LastName”:

{
    FirstName: "Jonas",
    LastName: "Gauffin"
}

To get around that problem you can create a simple directive:

var directive: {
    FullName: function(value, parentObject) {
        return parentObject.FirstName + " " + parentObject.LastName;
    }
}

var renderer = new ViewRenderer("YourView");
renderer.render(dto, directive);

Which will result in the HTML element being populated.

View mappings

There are currently a few different attributes that you can use in your views.

data-name

This is the basic one which just maps a property to a specific attribute in your view. You can also use the “id” and “name” attributes instead of “data-name”.

Complex objects are of course supported.

<div id="YourView">
    <div data-name="User">
        <span data-name="FirstName"></span>
        <span data-name="LastName"></span>
    </div>
    <div data-name="Address">
        <span data-name="PostalCode"></span>
        <span data-name="City"></span>
    </div>
</div>

.. which would correspond to the JSON object ..

{
    User: {
        FirstName: "Jonas",
        LastName: "Gauffin",
    },
    Address: {
        PostalCode: 12345,
        City: "Falun"
    }
}

data-collection

This attribute is used to process collections/lists in the JSON object.

<div id="YourView">
    <table>
        <tr data-collection="Users">
            <td data-name="FirstName"></td>
        </tr>
    <table>
</div>

The attribute must always be set on the parent element to the element that is going to be repeated.

For a regular list you add it on the ul or ol element:

<div id="YourView">
    <ol data-collection="Users">
        <li data-name="FirstName"></li>
    </ol>
</div>

The parent must only contain one child element, you can for instance not do this:

<div id="YourView">
    <div data-collection="Messages">
        <div data-name="subject"></div>
        <div data-name="body"></div>
    </div>
</div>

.. instead you need to put them in a container:

<div id="YourView">
    <div data-collection="Messages">
        <div>
            <div data-name="subject"></div>
            <div data-name="body"></div>
        </div>
    </div>
</div>

Empty collections

Rendering an empty list or table is not very aesthetic, therefore you can add an element below your collection with the attribute “data-unless”. When used with an array it simply evaluates if the array is empty or not.

<div id="YourView">
    <div data-collection="Messages">
        <div>
            <div data-name="subject"></div>
            <div data-name="body"></div>
        </div>
    </div>
    <div data-unless="Messages">
        No messages have been written yet. Why don't you take the initiative for once???
    </div>
</div>

data-if

Finally there are the data-if attribute. It evaluates the value as an expression in the context { vm: yourViewModel, ctx: IRouteExecutionContext}.

You could for instance use this in you view:

<div data-if="ctx.routeData['applicationState'] == 'open'">
    <!-- info -->
</div>

.. or ..

<div data-if="vm.yourFunction()">
    <!-- info -->
</div>

Global directives

Sometimes you want to adjust all values of a certain kind. For instance date formatting, other localization rules or something similar. To do that you can register global value directives.

var dateAdjuster = {
    process: function(context) {
        //I have a convention where date fields end with "Utc" such as "CreatedAtUtc"
        if (context.propertyName.indexOf("Utc", this.length - "Utc".length) !== -1) {
            context.value = new Date(context.value).toLocalString();
        }
    }
}

Griffin.Yo.ViewRenderer.registerGlobal(dateAdjuster);

Once done, all dates in all views will be formatted according to the user browser locale setting. The information in the context can be found here.

Route parsing in links

If you are using the Spa features of Griffin.Yo you get another feature for free. All href attributes for links located in the views will be parsed by the library.

So if you have mapped a route link this:

var spa = new Griffin.Yo.Spa("yourAppName");
spa.mapRoute("application/:applicationId/bug/:bugId/comment/:commentId");

.. you can use those values in all links in your view:

<a href="#/application/:applicationId">Return to the application</a> |
<a href="#/application/:applicationId/bug/:bugId">Bug summary</a> |
<a href="#/application/:applicationId/bug/:bugId/comment/:commentId/delete">Delete comment</a>

<!-- rest of the view -->

.. the generated view will contain the actual values instead of the routeData specifiers.

Navigation menus

Sometimes you want to update the main menu with links that are valid only in the current context. To do that you simply just add the “data-navigation” attribute to a container node. Let’s assume that your main layout looks like:

<!DOCTYPE html>
<html lang="en">
  <head>
    <!-- settings -->
  </head>
  <body>
    <div class="container">
      <nav class="navbar navbar-default">
        <div class="container-fluid">
          <div id="navbar" class="navbar-collapse collapse">
            <ul class="nav navbar-nav">
              <li class="active"><a href="#">Home</a></li>
              <li><a href="#">About</a></li>
              <li><a href="#">Contact</a></li>
              <li class="dropdown">
                <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Dropdown <span class="caret"></span></a>
                <ul class="dropdown-menu">
                  <li><a href="#">Action</a></li>
                  <li><a href="#">Another action</a></li>
                </ul>
              </li>
            </ul>
			
            <!-- context menu will be inserted in here -->
            <ul class="nav navbar-nav" id="main-navigation">                    
            </ul>

          </div>
        </div>
      </nav>

      <div id="YoView">
      </div>

    </div> <!-- /container -->

	<!-- scripts... -->
  </body>
</html>

Notice the ul with the “main-navigation” attribute. To populate with navigation items from your view, add an element somewhere in your view:

<!-- your view elements here  -->
<div data-navigation="main-navigation">
<li><a href="#/applications">application list</a></li>
<li><a href="#/application/:applicationId">back to application</a></li>
</div>

The container will be removed from the view, and the child elements will be inserted into the main menu.

Summary

Code is available at github.

Leave a comment if you have feedback or suggestions.

  • Jeremy Child

    This is such a great clean SPA framework. Thanks for spending the time to make this!

    • Comments like that is the reason to why I do this. Thank you!