In my last post I was showing how you can build a JavaScript MVC framework around the jquery.tmpl.js plugin. To finish this up, every good MVC framework needs HTML helpers. The jquery.tmpl.js plugin already supports calling functions from the template, but has a hook that encodes all HTML that come out of these functions. What I was hoping to do was to have a nice little helper that would create button based on the text and href params, something like this:
...
{{each pages}}
<tr class="cr_${id} alt">
<td>
<a href="/${page_url}">${page_title}</a>
</td>
<td>
${page_url}
</td>
<td>
${page_status}
</td>
<td>
${page_date_modified}
</td>
<td>
${CMS.Helpers.button("edit", "#ajax/edit")}
</td>
</tr>
{{/each}}
...
Obviously, the helper needs to be defined as a global object, so let's create something like this:
window.CMS = {
Helpers: {
button: function (text, href) {
return "<a href=\"" + href + "\" class=\"button\">" + text + "</a>";
}
}
};
Now in order to make this work, I had to comment out the line where it HTML encodes all content returned by functions from the template:
From this:
....
encode: function (text) {
// Do HTML encoding replacing < > & and ' and " by corresponding entities.
return ("" + text).split("<").join("<").split(">").join(">").split('"').join(""").split("'").join("'");
}
....
To this:
....
encode: function (text) {
return text;
}
....
Update: so I figured out a proper way render HTML helpers using the "HTML property" in the markup and the "options" which let's you define private methods used within the template. It is not quite obvious it's possible at first, but it actually works out pretty sweet. So here is the magic:
(function ($) {
//HTML helpers
var CMS = {
Helpers: {
button: function (text, href) {
return "<a href=\"" + href + "\" class=\"button\">" + text + "</a>";
}
}
};
var sucessIndex = 0,
view,
model,
contentDiv = $("#content_div"),
controler = function (m, v) {
var tmplOptions = CMS,
html = $.tmpl(v, m, tmplOptions), //merge page model with view
content = $(html), //turn html into jQuery object
links = $("a", content); //query this object (before it even touches DOM)
links.click(function () {
alert("You clicked me!");
return false;
});
//put content on the page (in the DOM)
contentDiv.html(content);
};
//get view
$.ajax({
type: "Get",
url: "ajax/getView/test",
dataType: "html",
success: function (v) {
view = v;
//checks if model was loaded borefore running controler
if (sucessIndex > 0) {
controler(model, view);
}
else {
sucessIndex++;
}
}
});
//get model
$.ajax({
type: "Get",
url: "ajax/jPageModle",
dataType: "json",
success: function (m) {
model = m;
//checks if view was loaded borefore running controler
if (sucessIndex > 0) {
controler(model, view);
}
else {
sucessIndex++;
}
}
});
})(jQuery);
... and the template would now look something like this:
...
{{html $item.Helpers.button("edit", "#ajax/edit")}}
...
So another great benefit of this approach is that we can fully avoid using global variables. Also like the fact that the "options" object can go "deep" meaning, you can "import" a full namespace of functions (as I am doing by putting the "helpers" in the "Helpers" namespace, that can be referenced in the view. This is really powerful!
I love this new paradigm, and if the browser can handle it, I suppose the only thing left on the wish list is that Boris would together with is Scott Guthrie from the MVC3 and base ASP.NET MVC3 Razor and jquery.tmpl.js on the same syntax. That would really kick ass!
I have been looking on JavaScript MVC frameworks laterally and was able to ceate a demo build on the query.tmpl.js jQuery plugin developed by Boris Moore (also see John Resig the author of jQuery being involved in this project). My goal was to mimic full MVC paradigm where JSON would be the model, and view could be separated into an .HTML file, so that when editing, I would get highlighting, and editing it would be user friendly. In this demo, I am using ASP.NET MVC3 and VS 2010, but this paradigm could be used with any type of backend.
First the view: in a separated file, called test.html, I wrote this markup representing the page view (since this content is to be loaded via ajax, into an "ajax div" there is not header or footer, since these are already on the page):
<div class="hr">
<h1 class="icon_page_l">
${page_title} <span>Test</span>
</h1>
</div>
<table class="data_table width_full">
<tbody>
<tr>
<th>
Title
</th>
<th>
Url
</th>
<th>
Status
</th>
<th>
Date Modified
</th>
<th>
</th>
</tr>
{{each pages}}
<tr class="cr_${id} alt">
<td>
<a href="/${page_url}">${page_title}</a>
</td>
<td>
${page_url}
</td>
<td>
${page_status}
</td>
<td>
${page_date_modified}
</td>
<td>
<a class="ajax_action button right" href="#ajax/editPage/${id}/">Edit</a>
<a class="ajax_post_action button margin_right right"
href="#ajax/deletePage/${id}/">Delete</a>
</td>
</tr>
{{/each}}
</tbody>
</table>
This is what I have in the backed ASP.NET MVC, pretty much a mapped string method that will load my HTML file, and minify it (put all in one line) on the fly. Nothing too fancy, but it just allows me to have my "views" nicely formated:
public string getView(string p1, string p2, string p3)
{
string jsView = "~/Content/js/Views/" + p1 + ".html";
return CMS.Util.Ajax.minifyJs(CMS.Util.Ajax.LoadStaticFile(jsView));
}
Next, I create my model, which is ASP.NET MVC JSON result. The .NET MVC proved to be really powerful here, with few lines of code I can create wrapper object with two child objects each representing a DB table.
public JsonResult jPageModle(string p1, string p2, string p3)
{
//data context
var dataContext = new linqDataContext();
//query zones
var zones = from m in dataContext.zones
select m;
//query pages
var pages = from m in dataContext.pages
select m;
//create page model
var pageModel = new {
page_title = "Hello World",
pages = pages,
zones = zones
};
return Json(pageModel, JsonRequestBehavior.AllowGet);
}
A couple notes here, in ASP.NET MVC3 I believe, by default, JSON results will need to be POSTS, for some security reasons. Well I am kind of purist here, so I am setting it to get.
This is what comes out of the JSON result as our model:
{
"page_title": "Hello World",
"pages": [{
"id": 6,
"page_url": "index",
"page_html": "",
"page_author": "dpirek@gmail.com",
"page_settings": null,
"page_title": "účetnictví on-line",
"page_meta_description": "",
"page_meta_keywords": "",
"page_status": 1,
"page_date_modified": "\/Date(1287169574310)\/"
},
{
"id": 10,
"page_url": "about",
"page_html": "",
"page_author": "",
"page_settings": null,
"page_title": "About Page",
"page_meta_description": "",
"page_meta_keywords": "",
"page_status": 1,
"page_date_modified": "\/Date(1276185200060)\/"
},
{
"id": 31,
"page_url": "test",
"page_html": "test",
"page_author": "",
"page_settings": null,
"page_title": "ted a asf asf asf",
"page_meta_description": "",
"page_meta_keywords": "",
"page_status": 1,
"page_date_modified": "\/Date(1287422731090)\/"
}]
}
And now comes the controller: (before you run this, make sure you have the references to the latest jQuery and the jquery.tmpl.js plugin.
(function ($) {
var sucessIndex = 0,
view,
model,
contentDiv = $("#content_div"),
controler = function (m, v) {
var html = $.tmpl(v, m), //merge page model with view
content = $(html), //turn html into jQuery object
links = $("a", content); //query this object (before it even touches DOM)
links.click(function () {
alert("You clicked me!");
return false;
});
//put content on the page (in the DOM)
contentDiv.html(content);
};
//get view
$.ajax({
type: "Get",
url: "ajax/getView/test",
dataType: "html",
success: function (v) {
view = v;
//checks if model was loaded borefore running controler
if (sucessIndex > 0) {
controler(model, view);
}
else {
sucessIndex++;
}
}
});
//get model
$.ajax({
type: "Get",
url: "ajax/jPageModle",
dataType: "json",
success: function (m) {
model = m;
//checks if view was loaded borefore running controler
if (sucessIndex > 0) {
controler(model, view);
}
else {
sucessIndex++;
}
}
});
})(jQuery);
A few notes here:
One of the dilemmas was how to make two ajax calls (one for view, one for model) and make sure the controller runs only after both of them are loaded. I originally made the calls nested with was really ugly from performance point of view, so I created "sucessIndex" which basically a number which keeps track on successful ajax calls, and after each new successful ajax call this index is checked if it reached high enough, so that we have all we need and can run our controller.
Another nice feature that I am showing here is how you can bind element events before the content ever reaches the DOM: Creating jQuery object out of HTML string: $(html) which can then be queried inside, and added events. This pattern is commonly used inside of jQuery UI where a lot of HTML flies around, and is the most efficient way to create larger chunks of HTML code with events attached to it.
I have learned the most important lesson about SEO the hard way: if you site gets hacked through malicious comment script, google will first black list you, and then trop your page rank. I don't know if this was done as an intended attack to my site, or by some robot, but I can see the 80% drop in traffic, even after the site was cleared with google as a clean site, and since google does not care, I think this might be a nice way to destroy your competitor's search rankings. So now it looks security matters after all.