You will need copies of the following files:
In a previous lesson we developed a TemplateView class, the final class should look like this:
/// <reference path="./Scripts/typings/handlebars/handlebars.d.ts"/>
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);
if ($tmplEl.length == 0) return false;
// Compile the template and store it
this._template = Handlebars.compile($tmplEl.html());
this.OnTemplateLoad();
return true;
}
// Child should override this!!
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();
}
}
We need to extend this so that it works with our AjaxifiedModel class and allows the view to display the contents of the model. When we create the Model class we stored its data in a JavaScript object and provided the methods SetData() and GetData() to change the values. This was deliberate because we can pass this object to the template to create the HTML. There aree two ways we could achieve this, first we could provide a property and return this._data. But this is not a very object-oriented approach. In the second, approach we pass the template to the Model and it returns the HTML for us while keeping its data to itself. This is the approach we will take, and we will add a method ToHtnl() to the Model class.
enum ModelUrlType { Create = 0, Read = 1, Update = 2, Delete = 3 };
enum ModelStatus { Created, Read, Updated, Deleted, Dropped, AjaxError };
abstract class Model
{
// Attributes
private _data: Object;
private _primaryKeyName: string = "id";
private _primaryKeySet: boolean = false;
private _urls: string[] = [];
private static _ajaxSetup: boolean = true;
// Constructor
constructor(dataFields: any, primaryKeyName: string, urls: string[])
{
this._data = dataFields;
this._primaryKeyName = primaryKeyName;
this._urls = urls;
Model.AjaxSetup();
}
// Properties
get PrimaryKeyName(): string
{
return this._primaryKeyName;
}
get PrimaryKey(): number
{
return this.GetData(this._primaryKeyName);
}
set PrimaryKey(value: number)
{
this.SetData(this._primaryKeyName, value);
}
protected get PrimaryKeyNameValue(): Object
{
var priKey = {};
priKey[this.PrimaryKeyName] = this.PrimaryKey;
return priKey;
}
// Methods
static AjaxSetup(): void
{
if (this._ajaxSetup)
{
$.ajaxSetup({ cache: false }); // May need to turn off browser page cache
this._ajaxSetup = false;
}
}
protected GetData(propName: string): any
{
if (this._data[propName] !== undefined) // Does field exist?
{
// If reading the primary key value and it hasn't been set, then return undefined.
return (this._primaryKeySet || propName != this._primaryKeyName) ? this._data[propName] : undefined;
}
return undefined;
}
protected SetData(propName: string, value: any): boolean
{
if (this._data[propName] !== undefined || propName == this._primaryKeyName)
{
if (value !== undefined)
{
this._data[propName] = value;
if (!this._primaryKeySet && propName == this._primaryKeyName) this._primaryKeySet = true; // Note the primary key has been provided
return true;
}
}
return false;
}
protected Url(urlType: ModelUrlType, args?: Object): string
{
if (this._urls[urlType] !== undefined)
{
return this.UrlAppendPostfix(this._urls[urlType], args);
}
else
{
throw new Error("No postfix supplied for url type " + urlType);
}
}
protected UrlAppendPostfix(postfix: string, args?: Object): string
{
var url: IUrl = Url.Create();
url.Append(postfix);
if (args)
{
url.AppendArgs(args);
}
return url.Url;
}
ToHTMLPairs(skipList?: string[]): string
{
return HTMLPairsConvertor.ConvertObjToPairsa(this._data, skipList);
}
ToHtml(template: HandlebarsTemplateDelegate): string ← Pass template to the Model and return the new HTML text
{ ← Usage in ModelView
return template(this._data); ← Pass model's object to template to insert the values in the new HTML
}
abstract Create(): boolean;
abstract Read(): boolean;
abstract Update(): boolean;
abstract Delete(): boolean;
}
The problem is we want to combine the AjaxifiedModel and TemplateViews we can create, to allow us to display a model in a view. Often, this involves a form that allows the user to make changes to the model. Effectively, we have to bind the view to the model so any changes made by the user can be made to the model and we also need to bind the model to the view so that any changes made to the model can be seen by the user.
We'll start by creating our own AjaxifiedModel class called "AjaxifiedCarModel"representing a car with an associated name and description. We also need the server side pages ModelCreateCar, ModelUpdateCar, etc.
class class AjaxifiedCarModel extends AjaxifiedModel
{
// Attributes
private _view: IAjaxifiedCallback;
// Constructor
constructor()
{
var urls: string[] = [];
urls[odelUrlType.Create] = "./AjaxServerSide/ModelCreateCar";
urls[ModelUrlType.Read] = "./AjaxServerSide/ModelReadCar";
urls[ModelUrlType.Update] = "./AjaxServerSide/ModelUpdateCar";
urls[ModelUrlType.Delete] = "./AjaxServerSide/ModelDeleteCar";
// Specify what the model fields are and their
// types in an object passed to the parent
// constructor
super({
"id": 0, // Data fields of model
"name": "",
"description": ""
},
"id", // Primary key
urls); // URLs for create, read, etc.
}
// Properties
get Name(): string { return this.GetData("name"); }
set Name(name: string) { this.SetData("name", name); }
get Description(): number { return this.GetData("description"); }
set Description(description: number) { this.SetData("description", description); }
// Methods
BindToView(view: IAjaxifiedCallback): void ← Example Usage
{
this._view = view; // Link the model to the view so we can call OnAjaxSuccess() on view
}
protected OnSuccess(modelStatus: ModelStatus): void
{
if (this._view)
{
this._view.OnAjaxSuccess(modelStatus); // Tell the view the model has changed
}
}
}
Let's create the HTML page that contains a couple of handlebars templates:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title></title>
</head>
<body>
<div id="car-form"></div>
<script id="create-car-template" type="text/x-handlebars-template">
<form>
Name:<input type="text" class="car-name" /><br />
Description:<input type="text" class="car-description" /><br />
<input type="submit" class="submit-button" value="Create car" />
</form>
</script>
<script id="update-car-template" type="text/x-handlebars-template">
<form>
Name:<input type="text" class="car-name" value="{{name}}" /><br /> ← "name" is field in Model
Description:vinput type="text" class="car-description" value="{{description}}" /><br /> ← "description" is field in Model
vinput type="submit" class="submit-button" value="Update car" />
v/form>
</script>
<script src="Scripts/jquery-2.2.0.js">
<script src="Scripts/handlebars.js">
<script src="HTMLPairsConvertor.js">
<script src="URL.js">
<script src="Model.js">
<script src="AjaxifiedModel.js">
<script src="View.js">
<script src="TemplateView.js">
<script src="ModelView.js">
<script src="AjaxifiedCarModel.js">
<script src="AjaxifiedCarModelView.js">
vscript src="ExampleModelViewPage.js">
</body>
</html>
The code for the page will create the Model and ModelView. When creating the view, the model must be passed into the constructor to bind the view to the model, allowing changes made by the user to be made to the model. Likewsie, the model must be bound to the view using BindToView() so that the model can inform the view when it changes. It does this by calling OnAjaxSuccess(). The first template loaded is "#create-car-template" because we are assuming they will be craeating a new model when the page loads.
class ExampleBasicModelViewPage
{
// Attributes
// Constructor
// Properties
// Methods
PageLoad(): void
{
var model: AjaxifiedCarModel = new AjaxifiedCarModel();
var view: AjaxifiedCarModelView = new AjaxifiedCarModelView("#car-form", model); ← Pass model to view so the view can change it when the user makes changes
model.BindToView(view); ← Pass the view to the model so it can inform the view when it has been changed, usually by AJAX read
view.LoadTemplate("#create-car-template");
view.Render();
}
}
// jQuery page ready
$(function ()
{
var page: ExampleBasicModelViewPage = new ExampleBasicModelViewPage();
page.PageLoad();
});
The ModelView class is actually quite straightforward becuase the combination of the View and TemplateView classes provides all the functionality we required.
abstract class ModelView extends TemplateView
{
// Attributes
private _model: Model;
// Constructor
constructor(parentEl: any, model: Model, options?: TemplateViewOptions)
{
super(parentEl, options);
this._model = model; // Store the Model so we can make changes to it.
}
// Properties
get Model(): Model ← Example Usage
{
return this._model;
}
// Methods
protected RenderView(el: JQuery): void
{
if (this.HasTemplate())
{
// Render the model in the view
// using the template
el.html(this._model.ToHtml(this.Template));
}
else
{
el.html("Error no template provided");
}
}
}
We have created an AjaxifiedCarModel, so to display it we create an AjaxifiedCarModelView class that inherrits from ModelView and allows us to use the templates in the webpage. Having two templates allows us to provide forms to create and update a car model. The class will look like this:
class AjaxifiedCarModelView extends ModelView
{
// Attributes
private _created: boolean = false;
// Constructor
constructor(parentEl: any, model: AjaxifiedCarModel, options?: TemplateViewOptions)
{
super(parentEl, model, options);
}
// Properties
// Methods
// Manually setup events
BindEvents(): void
{
if (this._created) this.$(".submit-button").on("click", $.proxy(this.Update, this));
else this.$(".submit-button").on("click", $.proxy(this.Create, this));
}
UnbindEvents(): void
{
this.$(".submit-button").off();
}
Create(e: JQueryEventObject): void
{
var model: AjaxifiedCarModel = this.Model;
model.Name = this.$(".car-name").val();
model.Description = this.$(".car-description").val();
this.Model.Create(); // AJAX to do insert
e.preventDefault();
}
Update(e: JQueryEventObject): void
{
var model: AjaxifiedCarModel = this.Model;
model.Name = this.$(".car-name").val();
model.Description = this.$(".car-description").val();
this.Model.Update(); // AJAX to do update
e.preventDefault();
}
OnAjaxSuccess(modelStatus: ModelStatus): void
{
switch (modelStatus)
{
case ModelStatus.Created:
window.alert("Created successfully");
this._created = true;
this.LoadTemplate("#update-car-template");
this.Render();
break;
case ModelStatus.Updated:
window.alert("Updated successfully");
this.Render();
break;
case ModelStatus.Read:
case ModelStatus.Deleted:
this.Render();
break;
}
}
}
Notice how the Create() and Update() methods read the text boxes to update the model then use it to perform the AJAX operation. When we get a response from the AJAX transfer, the OnAjaxSuccess() method is called. Usually we just Render() the view again to display any changes to the model, but in this example when we perform a create we also switch the view by changing the template to "update-car-template" allowing the user to edit any further changes. I've added Read and Delete for completeness, but we aren't actually using this functionality in this example.
Try creating your own AjaxifiedModel and ModelView classes.