Problem: How do we dynamically generate HTML for the views?

Setup

You will need copies of the following files:

Motivation

In a previous lesson we developed a View class, the final class should look like this:


interface ViewOptions
{
    el?: any;
}

abstract class View implements IDestroyable
{
    // Attributes

    // HTML tags are stored as jQuery object
    private _parentEl: JQuery = null;       // Parent tag to which view is attached and rendered into.
    private _el: JQuery = null;             // Tag containing contents of view that is attached to parent.

    // Constructor
    constructor(parentEl: any, options?: ViewOptions)
    {
        this.ParentEl = parentEl;
        if (options && options.el)
        {
            // Tag to contain view contents has been provided
            this.El = options.el;
        }
        else
        {
            // By default view contents are rendered into a <div> tag
            this.El = $(document.createElement('div'));
        }
        this.Attach();
    }

    // Properties
    private set ParentEl(el: any)
    {
        this._parentEl = this.GetJQueryObject(el);
    }

    private set El(el: any)
    {
        this._el = this.GetJQueryObject(el);
    }

    get Parent(): JQuery
    {
        return this._parentEl;
    }

    protected get View(): JQuery
    {
        // The container tag into which the view contents should be placed
        return this._el;
    }

    // Methods
    protected GetJQueryObject(el: any): JQuery
    {
        // If input is a JS HTML object or a text selection e.g. "#someId"
        // then we let jQuery convert it to a jQuery object using $(tag).
        // If was a jQuery object already we just use it.
        return el instanceof $ ? el : $(el);
    }

    protected $(selector: any): JQuery
    {
        return this._el.find(selector);
    }

    Attach(): void
    {
        if (this._parentEl.find(this._el).length == 0) // Make sure it isn't already attached
        {
            this._el.appendTo(this._parentEl);  // Add the view to the parent element  
        }
    }

    Detach(): void
    {
        this._el.detach(); // Remove the view from the parent but you can reattach it
    }

    Move(newParentEl: any): JQuery
    {
        var oldParent = this._parentEl;
        this.Detach();
        this.ParentEl = newParentEl;
        this.Attach();
        return oldParent;
    }

    Render(): void
    {		  
        this.UnbindEvents();
        this.RenderView(this._el);
        this.BindEvents();
    }

    protected abstract BindEvents(): void;
    protected abstract UnbindEvents(): void;

    Destroy(): void
    {
        this.UnbindEvents();

        // Detach from parent and destroy all jQuery objects
        // associated with view. If detached will just destroy
        // contents of view.
        this._el.remove();
        this._el = null;
        this._parentEl = null;
    }

    protected abstract RenderView(el: JQuery): void
}  

However, using this View class we had to generate HTML content for the view when it was rendered by calling RenderView(). To do this we were forced to create the HTML as string so that we could rewrite the contents of the view. It looked something like this:

    RenderView(el: JQuery): void
    {
        var html: string;
        html = "<input class='close-button' type='button' value='X' />";
        html += "<p>Hello " + this._name + "</p>";
        html += "<select class='region-name'>";
        html += "<option value='region1'>Region One</option>";
        html += "<option value='region2'>Region Two</option>";
        html += "<option value='region3'>Region Three</option>";
        html += "</select>";
        html += "<input class='move-button' type='button' value='Move' />";
        this.View.html(html);
    }

Obviously, this approach is fairly limiting and we would like a better solution. The best answer is to use templates. A template is basically a formal way of specifying HTML tags and placeholders where content should be inserted. We will use a template compiler called Handlebars.js. A simple template looks like this:

    <script id="hello-template" type="text/x-handlebars-template">
        <form>
            <p>Hello {{name}}</p>
            <input type="submit" class="goodbye-button" value="Click Me" />
        </form>
    </script>

Notice the use of {{name}} as a placeholder where a value should be displayed. The template itself is contained in a <script> tag with the type set to"text/x-handlebars-template".

To compile this template to HTML we include handlebars.js in our page and provide the values to be inserted within an object literal.

var templateText: string;
var template: HandlebarsTemplateDelegate;
var html: string;

templateText = $("#hello-template").html();		// Read the template from the webpage
template = Handlebars.compile(templateText);	// Compile the template so we can use it

// Supply the values to be used, and generate the HTML
// You can reuse the compiled template multiple times
html = template({								
	name: "Fred"
});

html = template({								
	name: "Jane"
});

We need to create a class that encapsulates these three steps:

  1. Load the template text from the webpage
  2. Compile and store the template
  3. Supply the values and use the template to generate the HTML for the view

Usage

Let's take an example of a HTML page that contains a handlebars template:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title></title>
</head>
<body>
    <div id="display-contents-here"></div>

    <script id="hello-template" type="text/x-handlebars-template">
        <form>
            <p>Hello {{name}}<p>
            <input type="submit" class="goodbye-button" value="Click Me" />
        </form>
    </script>

    <script src="Scripts/jquery-2.2.0.js"></script>
    <script src="Scripts/handlebars.js"></script>
    <script src="BasicView.js"></script>
    <script src="SinglePageApp/EventTemplateParser.js"></script>
    <script src="BasicTemplateView.js"></script>
    <script src="BasicSayHelloTemplateView.js"></script>
    <script src="ExampleBasicTemplateViewPage.js"></script>
</body>
</html>
In this page, we have a <div> tag "display-contents-here" where the view will be displayed. The template below it produces the following HTML, assuming the name provided is "Fred":
        <form>
            <p>Hello Fred<p>
            <input type="submit" class="goodbye-button" value="Click Me" />
        </form>

The button in the view will simply open an alert box saying "Goodbye" but will demonstrate how "click" events can be registered in the view when using a template.

We will create a SayHelloTemplateView class, that allows us to use this template to say "Hello" to a named user. The class will look like this:

class SayHelloTemplateView extends TemplateView
{
    // Attributes
    private _name: string;

    // Constructor
    constructor(parentEl: any, name: string, options?: BasicTemplateViewOptions)
    {
        super(parentEl, options);
        this._name = name;
    }

    // Properties

    // Methods
    protected RenderView(el: JQuery): void
    {
        if (this.HasTemplate()) el.html(this.Template({
            name: this._name
        }));
        else el.html("Error no template provided");
    }

    // Manually setup events
    BindEvents(): void
    {
        this.$(".goodbye-button").on("click", $.proxy(this.ButtonClicked, this));
    }

    UnbindEvents(): void
    {
        this.$(".goodbye-button").off()
    }

    ButtonClicked(e: JQueryEventObject): void
    {
        window.alert("Goodbye from " + this._name);
        e.preventDefault();
    }
}

The name of the user is provided in the constructor and stored in the attribute _name. When RenderView() is called, we check that the template exists and then use it to generate the HTML and we provide the required name for display.

The BindEvents() and UnbindEvents() methods work as before, and these are called by the Render() method..

The code for the TemplateView class must be completed by you.

The Page class that displays the SayHelloTemplateView will look something like this:

class ExampleTemplateViewPage
{
    // Attributes

    // Constructor

    // Properties

    // Methods
    PageLoad(): void
    {
        var view: SayHelloTemplateView = new SayHelloTemplateView("#display-contents-here", "Fred");

        view.LoadTemplate("#hello-template");
        view.Render();
    }
} 

// jQuery page ready
$(function ()
{
    var page: ExampleTemplateViewPage = new ExampleTemplateViewPage();

    page.PageLoad();
});
Once the view is create we call LoadTemplate() to load the template from the webpage. The view is rendered which also causes the events to be bound.

Details

You must complete the abstract TemplateView class shown below. This class inherits from the View class and parent constructor is passed the location where the view should be displayed (parentEl) and any options. An additional option is provided by the TemplateView to allow the location of the template to be specified in the constructor but you can just call LoadTemplate() later yourself as we did above.

The property Template allows the child class to create the view HTML.

LoadTemplate() reads the template text from the <script> tag on the webpage and is passed the location id (tmplEl) where this can be found. As can be seen below, it checks to see if it exists and throws an error if it doesn't. You have to provide the code to compile the template and store it the atrribute _template, so that whenever the view is rendered the new values can be supplied and the HTML generated.

The Render() method in the View class is overriden to check that a template existss.

interface TemplateViewOptions extends ViewOptions
{
    tmplEl?: any;
}

abstract class TemplateView extends View
{
    // Attributes
    private _template: HandlebarsTemplateDelegate;

    // Constructor
    constructor(parentEl: any, options?: TemplateViewOptions)
    {
        super(parentEl, options);
        if (options && options.tmplEl)
        {
            this.LoadTemplate(options.tmplEl);
        }
    }

    // Properties
    protected get Template(): HandlebarsTemplateDelegate
    {
        return this._template;
    }

    // Methods
    LoadTemplate(tmplEl: any): boolean
    {
        // Load the template from a HTML tag
        // on the web page.
        var $tmplEl: JQuery = this.GetJQueryObject(tmplEl);

        // Check it exists on page.
        if ($tmplEl.length == 0) return false;

        // Compile the template and store it
        this._template = <strong>// Your code goes here</strong>

        this.OnTemplateLoad();
        return true;
    }

    // Child can override this to be informed when new template is loaded!!
    OnTemplateLoad(): void { }

    HasTemplate(): boolean
    {
        return this._template !== undefined;
    }

    Render(): void
    {
        if (this._template === undefined) throw new Error("No template supplied before rendering ModelView");

        // Render the view
        super.Render();
    }
}

Hints

  1. We are using Handlebars.js