You will need copies of the following files:
PLEASE NOTE: You must add a reference to the .d.ts file at the top of the typescript file
⁄⁄⁄ <path="typings/backbone.marionette/index.d.ts" ⁄>
We may want a view class to detect when changes occur in Model attribute values so that they can be displayed, or user interaction with the DOM e.g. clicking buttons. Both these interactions are monitored using Events. We can also create a view class that exposes its own events (triggers) allowing the view to make the code using it aware of changes to the view itself.
As usual, lets start by creating a simple Model class..
namespace ExampleEventHandlingNS
{
export class SampleModel extends Backbone.Model
{
constructor()
{
super({
value1: "A random Value",
value2: "Another Random Value"
});
}
set Value1(value: string) { this.set('value1', value); }
get Value1(): string { return this.get('value1'); }
set Value2(value: string) { this.set('value2', value); }
get Value2(): string { return this.get('value2'); }
}
}
We now define the ItemView class, to display this Model. Handling the events requires a bit of explanation, so we will begin by adding callbacks for events generated by the Model. There are basically two types of Model events we are interested in here, those that tell us when a change has occurred in the value of a specific attribute, and an event that just tells us that a change has occured to one or more attribute values. The events are "change:attributeName" and "change" respectively, and they are specified in a object-literal in the option modelEvents which can be passed in the constructor to the parent ItemView class like this:
super({
......
modelEvents: {
"change:value1": callback,
"change": callback
}
});
Unfortunately, you cannot refer to this unitl the parent super() constructor has finished executing, which effectively means the callback methods must be declared as "static" i.e. belonging to the class. What's more, being declared as a static method also means that the callback may not reference this and consequently has no knowledge of the view instance that on which the event is being processed!!
To avoid this problem, we add the modelEvents after the call to the parent constructor and we can then use
this.callbackto set the event callback, and declare the callback using the Typescript callback syntax to preserve the view context i.e. this=the view object. The result looks like this:
constructor(model: SampleModel, el: any)
{
super({
........
});
this.modelEvents = {
"change:value1": this.Value2Changed,
"change": this.DataChanged
};
this.delegateEvents(); // MUST ADD THIS!!
}
private Value1Changed = (model, value): void =>
{
window.alert("Value1 changed to '" + value + "'");
}
private DataChanged = (model, values): void =>
{
window.alert("Data changed to '" + model.get('value1') + " and " + model.get('value2') + "'");
}
Note the call to delegateEvents(), without this the events added after the parent constructor is called are not registered.
We can also handle DOM events in the View class, using the events option which works in the same way as the modelEvents option. This can be quite easy if you can rely on the "id" attributes on the HTML page being unique, which they should be. In which case, use the CSS hash ('#') selector as usual. The Marionette syntax for handling a button 'click' event is:
"click #someButton1"
Sometimes, the same View (and hence template) will be rendered multiple times. Each element on a HTML page should have a unique "id", so adding id values into the template is not a good idea. For example, this will happen when you render a CollectionView which contains the same ItemView for each Model in the Collection. In this case, instead of using id values the approach we have to use an alternative approach to selecting the HTML elements to which we want to add event handlers. A common example, is displaying buttons in each ItemView to allow the user to edit or delete the associated Model. To select each button, we use the input tag, type and value attributes of the HTML tag, where the input type is 'button' and the value is the text that appears in the button. Marionette uses the following syntax.
[attribute1="X"][attribute2="Y"] is an AND operation, while [attiibute1="X"],[attribute2='Y'] is an OR operation (note the comma).
"click input[type='button'][value='Edit']"
"click input[type='button'][value='Delete']"
For example, to handle the click event on a HTML button with the id="someButton1" to handle it in the View would look like this:
constructor(model: SampleModel, el: any)
{
super({
........
});
this.modelEvents = {
........
};
// DOM events
// Note: If you set up a DOM event and a trigger for
// the same HTML control, only the trigger works.
this.events = <any>{
"click #someButton1": this.SomeButtonClicked
}
this.delegateEvents(); // MUST ADD THIS!!
}
private SomeButtonClicked = (): void =>
{
window.alert("Button clicked");
}
The method we have been describing is Marionettes approach to handling events and hides much of the Backbone event handling from us.Alternatively, you can just use the Backbone approach which is to use a function called listenTo().
listenTo() tells an object to listen to a particular event on another object. There is an alternative, which is to use on(), but the advantage of using listenTo() is that it allows the object to keep track of the events. These can then be removed all at once later on i.e when you call destroy() on the view. Marionette calls stopListening() when we destroy() a view, which cleans up all the event bindings. One exception to this, is on() SHOULD be used when handling DOM events.
constructor(model: SampleModel, el: any)
{
super({
.......
});
this.listenTo(this.model, 'change:value1', this.Value1Changed);
// Or for events in collections, same idea!!
// this.listenTo(this.collection, "add", this.modelAdded);
// Example: Re-render the view when model changes.
// Same idea as previous line but this time any value in the model changing
// will call the callback function, which in this case is just the render() function.
this.listenTo(this.model, 'change', this.render);
// DONT DO THIS!!
// this.model.on('change', this.render, this);
}
A view can also generate events itself for users of the view to handle. To this Marionette, uses triggers which again work like the event options we have been discussing. For example, if the user clicks a button we want to let the user of the view know by sending them an event like this:
constructor(model: SampleModel, el: any)
{
super({
.......
});
// Triggers, exposes events to code using the View
this.triggers = {
"click #someButton2": "trigger:ButtonClickEvent"
}
this.delegateEvents(); // MUST ADD THIS!!
}
A complete view class handling all these types of events and triggers, looks something like this::
namespace ExampleEventHandlingNS
{
export class SampleView extends Backbone.Marionette.ItemView<SampleModel>
{
constructor(model: SampleModel, el: any)
{
super({
model: model,
el: el,
template: '#sample-template'
});
this.modelEvents = {
"change:value1": this.Value1Changed,
"change": this.DataChanged
};
// DOM events
this.events = <any>{
"click #someButton1": this.SomeButtonClicked
}
this.triggers = {
"click #someButton2": "trigger:ButtonClickEvent"
}
this.delegateEvents(); // MUST ADD THIS!!
}
private Value1Changed = (model, value): void =>
{
window.alert("Value1 changed to '" + value + "'");
}
private DataChanged = (model, values): void =>
{
window.alert("Data changed to '" + model.get('value1') + " and " + model.get('value2') + "'");
}
private SomeButtonClicked = (): void =>
{
window.alert("Button clicked");
}
}
}
You use this ItemView like this:
class ExampleEventsPage
{
PageLoad(): void
{
let sampleModel: ExampleEventHandlingNS.SampleModel = new ExampleEventHandlingNS.SampleModel();
let sampleView: ExampleEventHandlingNS.SampleView = new ExampleEventHandlingNS.SampleView(sampleModel, '#container');
sampleView.render();
sampleModel.Value1 = "Changed Random Value One";
sampleModel.Value2 = "Changed Random Value Two";
// Add an event handler to the ButtonClickEvent exposed by the view
// Don't use listenTo for DOM objects.
sampleView.on("trigger:ButtonClickEvent", this.ButtonClicked);
}
ButtonClicked = (args: any): void =>
{
window.alert("External event handler, read value2 '" + args.model.Value2 + "'");
}
}
$(function ()
{
let page: ExampleEventsPage = new ExampleEventsPage();
page.PageLoad();
});
A similar approach can be use when dealing with CollectionView classes, a collectionEvents option allows us to detect changes to a Collection of Models. The most useful is the "sync" event which is signalled when the collection is written back to the webservice. But this is jumping ahead a bit, so instead the "add", "remove" and "update" events will signal the view when changes are made to the Collection. Often, we just re-render the view when we see changes being made to the Collection, by just calling the render() function which is part of Backbone.js.
export class UserCollectionView extends Backbone.Marionette.CollectionView<SomeModel, SomeItemView>
{
constructor(collection: UserCollection, el: any)
{
super({
collection: collection,
el: el,
........
});
this.collectionEvents = {
"add": this.render, <--- Just re-render the view
"remove": this.render,
"update": this.render
}
this.delegateEvents();
....
}
}Warning: Using an "add" event in this way is not normally recommended, because it will be generated for every Model added to the Collection. Imagine you fetch the contents of a database table, every record will generate an event an re-render the View!!! When using a Collection to download from a REST webservice, the best approach is to use the "sync" event instead, which is generated when the collection has been successfully synced with the server.
Continuing the previous exercise, we create a page layout view that looked like this.
| List of books |
|
||
| Add Book | |||
Previously we were not able to automatically update the views as we made changes to the Model details and Collection contents., but now we can. Add the appropriate event handling required to re-render the view(s) as changes are made.