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" });
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 a 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: <a href="https://github.com/jgauffin/griffin.yo/blob/master/src/lib/Routing/IRouteExecutionContext.ts">IRouteExecutionContext</a>}
.
You could for instance use this in your view:
<div data-if="ctx.routeData['applicationState'] == 'open'">
</div>
.. or ..
<div data-if="vm.yourFunction()">
</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) {
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>
<!--
.. 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>
<!--
</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>
<!--
<ul class="nav navbar-nav" id="main-navigation">
</ul>
</div>
</div>
</nav>
<div id="YoView">
</div>
</div> <!--
<!--
</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:
<!--
<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.