Quantcast
Channel: Magnus Hansson SharePoint Blog
Viewing all articles
Browse latest Browse all 15

JavaScript Template Manager - it's all on the client side

$
0
0

The Problem

Many developers especially when writing JavaScript have a thing for writing HTML markup inside of their JavaScript code. The downside of this approach is clearly that the markup is getting mixed with the coding language and when changing the markup you may have to look inside of hundreds and hundreds of rows of JavaScript just to change the markup for one piece of HTML.

We don't want to have the design definition inside of the JavaScript. The design should be defined where it is, then you will get a much better overview. This is not the case if you put the markup inside of the JavaScript, C# or whatever programming language that you are using.

In ASP.NET we use server side controls such as the asp:repeater as a template with data holders with the html clearly defined and it is very clear where the data is:

<asp:Repeater runat="server" ID="RSSFeedList">  
                <HeaderTemplate>
                      <ul class="dfwp-list">  
                </HeaderTemplate>
                <ItemTemplate>

                     <li class="item <%# itemClassName((RssFeed)Container.DataItem) %>">
                        <div class="link-item">
                            <a target="_blank" href='http://www.magtherag.com/"<%' # ((RssFeed)Container.DataItem).LinkUrl %>" title="<%# ((RssFeed)Container.DataItem).Title %>">
                                <%# ((RssFeed)Container.DataItem).Title %>
                            </a>
                            <div class="description published-date"><%# ((RssFeed)Container.DataItem).PublishedDate%></div>
                        </div>
                    </li>

                </ItemTemplate>
                <FooterTemplate>
                     </ul>
                </FooterTemplate>
            </asp:Repeater>

I know now there are these good frameworks for templating the JavaScript code (JSRender, Mustache.js and so on) but before these projects came out of the beta stage I needed to have this kind of functionality right away. I did not want put the html structure inside of the js code as it was bound to change many times before release and I want to make it more obvious where the logic was and where the design definition was. We were more than one developer in this project and the clearer the separation got between the logic and the design the better so that the right person can quickly find the code needed for his/her tasks.

The solution

rss-feed-internal

In this particular case I was building a RSS feed manager that took a couple of rss feed
urls, fetched them through an ajax call and then parsed the result on the page. I did not want the HTML markup for this to live inside of the RSS feed manager's JavaScript but on the html page itself. As I was building a User Control anyways to initialize the RSS feed I chose to put it there.

I wanted the markup to be something like this:

<ul class="dfwp-list">  
                <li class="item">
                    <div class="link-item">
                        <a target="_blank" href='http://www.magtherag.com/"' http://www.whateverlink.com&quot; title="Whatever Title">Whatever title
                        </a>
                        <div class="description published-date">2013-02-10</div>
                    </div>
                </li>  
            </ul>

The idea is that we should have a list item that should work as a repeater but instead of using backend code we are working with the client side in the same manner as when working with the classic ASP.NET controls using data placeholders.
Then the markup should look something like this:

<ul class="dfwp-list">  
                <li class="item" style="display:none">
                    <div class="link-item">
                        <a target="_blank" href='http://www.magtherag.com/"' {link}" title="{title}">
                            {title}
                        </a>
                        <div class="description published-date">{pubDate}</div>
                    </div>
                </li>  
            </ul>

My RSS Feed JavaScript code needed to fetch the data from the ajax call and then create objects in an array to pass these to a function inside of the Template Manager. There the template manager will take the html snippet from the page, parse through the <li> in this case, copy the html and replace the values inside of each curly brackets "{}" with the data corresponding to that value which will be the property of the current object in the array. Notice the "display:none" in the <li> which is there because this <li> will not be visible on the page as it just is a markup definition holder. 

In my RSSFeed class I first needed to instansiate the template manager and added it as a property for later access.

// Rendering template manager  
RssFeedManager.Template = new Project.TemplateManager(objListTemplate);

When I had fetched the rss feed items I put them in a variable "feedItems" where I store each object in the array.

 // add a new rss item to the array  
 RssFeedManager.addRssItem = function (title, link, pubDate, description, additionalCSS) {

        var item = new Object();
        item.title = title;
        item.link = link;
        item.pubDate = new Date(pubDate);
        item.pubViewDate = new Date(pubDate).toLocaleString();
        item.description = description;

        // additional css ( used in the rendering template manager ) 
        item.additionalCSS = additionalCSS;

        // add the item to the feed array
        feedItems[feedItems.length] = item;

    }

And when the user presses "give me the next 20 items" I call my addItemsToPage function which has 2 parameters, where in the array the parsing will start and where in the array the parsing will end. The Template Manager also has a function "addItemsToPage" which parameters follows: feedItems (array of objects), sDataValues (props to insert, both name of property and data type), startIndex, endIndex.
This is how it looks like:

RssFeedManager.addItemsToPage = function (startIndex, endIndex) {

        return RssFeedManager.Template.addItemsToPage(feedItems, "title:string,link:string,pubDate:date", startIndex, endIndex);

    }

The Template Manager

For those that are interested, here is the whole code for the Template Manager. Feel free to use it as you wish. The project variable should be replaced with whatever you like ( name of project, customer etc)

/// == Dependencies  
/// <reference path="/_layouts/Project/js/plugins/jquery.min.js" />

window.Project = window.Project || {};

Project.TemplateManager = function (objListTemplate) {  
    /// <summary>
    /// Rendering template manager. Adds data to a html element definition. 
    /// Example: <ul id="steria-rss-template">
    ///            <li style="display:none;">
    ///               <div>
    ///                        <a target="_blank" href='http://www.magtherag.com/"' {link}" title="{title}">
    ///                            {title}
    ///                        </a>
    ///                        <div>{pubDate}</div>
    ///                    </div>
    ///                </li>  
    ///            </ul>
    /// The template will create a new element from the template html element. Remove the template html element and put the new
    /// element in it's place
    /// </summary>    
    /// <param name="objListTemplate" type="jQuery">The jquery html element that serves as a template</param>    
    /// <returns type="jQuery" />

    var $ = jQuery;
    var Template = this;

    // private variables
    var newContainer = null;

    // the first item in the template element will work as a row template
    var rowTemplate = $(objListTemplate).children(':first-child');

    // returns the new container
    this.getNewContainer = function () {

        if (newContainer == null)
            setNewContainer();

        return newContainer;
    }

    // set the new container
    setNewContainer();

    // replaces all strings in a html element with a value
    function replaceAll(el, sToSearchFor, sReplaceWith) {
        el.html(el.html().replace(eval(sToSearchFor), sReplaceWith));
    }

    // remove all items in the container
    this.removeAllItems = function () {
        $(newContainer).empty();
    }

    // append new item
    this.appendItem = function (item) {

        if (newContainer == null)
            setNewContainer();

        $(newContainer).append(item);
        $(item).show();
    }

    // sets the new container and hide the template html element
    function setNewContainer() {
        if (newContainer == null) {

            var newObj = $(objListTemplate).clone();
            newObj.empty();
            $(objListTemplate).parent().append(newObj);
            $(objListTemplate).hide();

            newContainer = newObj;
        }
    }

    // Add items to the page from an array of items from a startIndex and an endIndex
    // and returns the final position in the array
    // feedItems: Array, sDataValues: string, startIndex: int, endIndex: int
    // examples of sDataValues: "title:string,link:string,pubDate:date"
    this.addItemsToPage = function (feedItems, sDataValues, startIndex, endIndex) {

        var index = 0;

        if (endIndex > feedItems.length) endIndex = feedItems.length;

        // loop throught and render the items
        for (var i = startIndex; i < endIndex; i++) {

            // fetch the new node 
            var newNode = this.getNewElement(feedItems[i], sDataValues);

            // add the new node 
            this.appendItem(newNode);

            index = i;
        }

        return index;
    }

    // fill a new row element with data and returns it
    this.getNewElement = function (obj, strVal) {

        var newEl = rowTemplate.clone();
        var valArr = strVal.split(',');
        var prop;

        for (prop in obj) {
            var currentProp = prop;

            if (currentProp == "additionalCSS") {
                $(newEl).addClass(obj[prop]);
            }
            else {
                for (var i = 0; i < valArr.length; i++) {
                    if (valArr[i].split(':')[0] == currentProp) {
                        var val = obj[prop];
                        if (valArr[i].split(':')[1] == "date") {
                            val = new Date(obj[prop]).toLocaleString();
                        }
                        replaceAll(newEl, "/{" + valArr[i].split(':')[0] + "}/g", val);
                    }
                }
            }

        }

        return newEl;
    }
}

Conclusion

By using the Template Manager class I could easily replace the markup with something completely different without changing any JavaScript file at all. The JavaScript is just like the good old backend code that focuses about gathering data and peforming other stuff that is for pure logical reasons. As JavaScript evolves and more developers need to work in this language (HTML 5 and SharePoint 2013 are just 2 examples) we are in need of making everything more transparent in order to work with code the proper way and not treat JavaScript as it has been treated earlier. Build class libraries, build reusable code, build stuff that will last and do not focus on building just small snippets that easily get too many and uncontrollable.


Viewing all articles
Browse latest Browse all 15

Trending Articles