Problem: How do we edit data and send the results back to the RESY Webservice?

Setup

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" ⁄>
    

Motivation

We have explored how to load data from a REST webservice, but we now want to edit the data in the Model and Collection classes then send the changes back to the service.

Usage

Lets begin by creating the Model and Collection classes that we need.

namespace ExampleRestHandlingNS
{
    export class UserModel extends Backbone.Model
    {
        constructor(name?: string, email?: string, username?: string, password?: string)
        {
            super({
                usrId: undefined,	// See later notes for doing an ADD i.e. REST POST
                name: name,         // Set to undefined if not provided
                email: email,
                username: username,
                password: password,
            });

            this.idAttribute = "usrId";        // The name of the id field used by the REST service

            // The urlRoot is not always required, see notes below.
            this.urlRoot = "http://localhost:1747/UserService.svc/users";
        }

        get UsrId(): string { return this.get('usrId'); }

        get Name(): string { return this.get('name'); }
        set Name(value: string) { this.set('name', value); }
        get Email(): string { return this.get('email'); }
        set Email(value: string) { this.set('email', value); }
        get Username(): string { return this.get('username'); }
        set Username(value: string) { this.set('username', value); }
        get Password(): string { return this.get('password'); }
        set Password(value: string) { this.set('password', value); }

        SetValues(values: { name: string, email: string, username: string, password: string }, options?: any): void
        {
            // usrId will be set here with other values read from REST service
            this.set(values);
        }
    }
    
	export class UserCollection extends Backbone.Collection<UserModel>
    {
        constructor()
        {
            super(undefined, {
                model: UserCollection.CreateModel
            });
            this.url = "http://localhost:1747/UserService.svc/users";
        }

        private static CreateModel(attrs?: any, options?: any): UserModel
        {
            let model: UserModel = new UserModel();
            //model.set(attrs, options);
            // Set the usrId and other UserModel values read from the REST service
            model.SetValues(attrs, options);
            return model;
        }
    }
}

In both the Model and Collection classes we have added this.urlRoot or this.url=""some url" to specify the REST URL that will be used. Actually, we don't always need to set the url in both the Model and Collection classes, because the Collection will handle REST for us. However, sometimes you may want to add or edit a single model rather than display a collection, in which case add the url to the Model class.

Note: If the model id is undefined Backbone will perform a POST operation becasue we must be creating a new database entry i.e. there is no primary key value. If the model has and id value, it must have been read from the database so we are editing the model and Backbone will perform a PUT operation.. This is done automatically by Backbone.

Say we want to add or edit a Model we can create a ItemView class that displays the textboxes, etc required to edit the model data. The template looks like this:

<script id="edit-user-template" type="text/html">
	Edit User <%= usrId %>
	<br />Name:
	<input id="editnametextbox" value="<%= name%>" type="text" />
	<br />Email:
	<input id="editemailtextbox" value="<%= email%>" type="text" />
	<br />Username:
	<input id="editusernametextbox" value="<%= username%>" type="text" />
	<br />Password:
	<input id="editpasswordtextbox" value="<%= password%>" type="password" />
	<br />
	<input type='button' value='Update' />
	<input type='button' value='Cancel' />
	<br />
</script>

The ItemView class handles the Update and Cancel button click events, changing the Model attribute values and doing a save() call causes a REST POST or PUT operation. The code looks like this:

namespace ExampleRestHandlingNS
{
	export class EditUserForm extends Backbone.Marionette.ItemView<UserModel>
	{
		constructor(userModel: UserModel, el: any)
		{
            super({
                model: userModel,
                el: el,
                template: "#edit-user-template"
            });

            this.modelEvents = {
                "sync": this.render        // Call this.render() method on the model "sync" event
            }

            this.events = <any>{
                "click input[type = 'button'][value = 'Update']": this.UpdateButtonClick,
                "click input[type = 'button'][value = 'Cancel']": this.CancelButtonClick
            };

            this.delegateEvents();
        }

        private UpdateButtonClick = (): void =>
        {
            // Change model attribute value and save() the model
            // to cause REST POST or PUT operation to be performend.
            // Model id should already have been set if we are doing an edit.
            this.model.Name = $("#editnametextbox").val();
            this.model.Email = $("#editemailtextbox").val();
            this.model.Username = $("#editusernametextbox").val();
            this.model.Password = $("#editpasswordtextbox").val();
            this.model.save();
        }

        private CancelButtonClick = (): void =>
        {
            this.destroy();
            this.model.clear();
        }
    }
}

A very basic way to use this View looks like this:

class ExampleRESTEditPage
{
    PageLoad(): void
    {
		let userModel: ExampleRestHandlingNS.UserModel = new ExampleRestHandlingNS.UserModel();
		let editForm: ExampleRestHandlingNS.EditUserForm = new ExampleRestHandlingNS.EditUserForm(userModel, '#editform');
        
 		ADD
        // By default the UserModel values are all undefined, including
        // the usrId field (see UserModel constructor) meaning any changes
        // made by the user will be sent as a REST POST
        editForm.render();
        
 		EDIT
 		// If you are doing an edit, load the  Model with the current data from the database
		// Typically, you wouldn't do this way because you would want the user to pick
		// the primary key or search value for the data displayed in the view.
		userModel.fetch({
			url: "http://localhost:1747/UserService.svc/users/" + "fred",  // Look for user "fred", <-- assumes he's in the database already, should read a search text box
			success: function (model, response, options)
			{
				// REST service has returned the new values and the model has
				// been updated including the usrId field. Meaning any changes
				// made by the user will be sent as a REST PUT               
				editForm.render();   // Wait for response before rendering view.
			},
			error: function (model, response, options)
			{
				window.alert('error!');
			}
		});
	}
}

We will get a bit fancy here and create a CompositeView so that we can display a collection and a form for adding a new entry to the database. When the Add button is clicked, we use the Collection to create a new Model and this sends the model to the REST service to be added to the database.

namespace ExampleRestHandlingNS
{
    export class UserCompositeView extends Backbone.Marionette.CompositeView<UserModel, UserView>
    {
        constructor(users: UserCollection, el: any)
        {
            super({
                collection: users,
                el: el,
                tagName: "div",                             // Add a <div> into which view is rendered
                template: "#compositeview-template",        // For displaying users list and add new user form
                childView: UserView,                        // Render model from the collection using this type of View
                childViewContainer: "#displayedusers"      // Specify where in the "CompositeView" template is displayed
            });

            this.events = <any>{
                // Could use "#addbutton" but I wanted to demo using jQuery selector 
                // to choose first <input type="button"> tag in view.
                // Only events for the DOM elements in the CompositeViews template
                // are caught here!!
                "click input[type='button'][value='Add']": this.AddButtonClicked
            };

            this.delegateEvents();

            this.collection.fetch();   // Fetch the data for the models to populate the collection
        }

        private AddButtonClicked = (): void =>
        {
            // Warning: 
            // Don't set the identity field in the model (usrId in this example), otherwise
            // you will do a REST PUT not a REST POST
            // Alternatively, set it to null
            // newModel.set("Id", undefined);
            // This newModel.isNew() method must return true
            // for a POST to occur.

            // Create new UserModel
            // Here the Collection creates the Model for us!!            
            this.collection.create({
                usrId: undefined,						// I've explicitly set the id to undefined to a POST but not setting it has the same effect.
                name: $("#addnametextbox").val(),
                email: $("#addemailtextbox").val(),
                username: $("#addusernametextbox").val(),
                password: $("#addpasswordtextbox").val()
            },
            { wait: true });        // Wait for server before adding model to collection
        }
    }
}

The composite view template looks like this:

<script id="compositeview-template" type="text/html">
	<div id="displayedusers"></div>   <----Collection is displayed here!!
	<br />Name:
	<input id="addnametextbox" type="text" />
	<br />Email:
	<input id="addemailtextbox" type="text" />
	<br />Username:
	<input id="addusernametextbox" type="text" />
	<br />Password:
	<input id="addpasswordtextbox" type="password" />
	<input type="button" value="Add" />
</script>

The child View for displaying the UserModel is a bit more complicated. We want to display the Model data but with a buttons for editing and deleting the Model. The template for the UserModel is shown below:

<script id="user-template" type="text/html">
	<!-- Note: I've added a HTML5 data attribute called data-msgid just for demo,
	we don't actually need it in this particular example. These attributes
	can be added to most HTML tags and must start with "data-".
	Use jQuery.data("msgid") to read it. -->
	<span data-msgid="<%= usrId %>">User <%= usrId %> <%= name %> <%= email %> <%= username %> <%= password %> <input type="button" value="Edit" /> <input type="button" value="Delete" /></span>
	<br />
</script>

Remember, the UserView will be displayed multiple times, once for each Model in the Collection. We don't want the template to produce tags with the same id values for each HTML tags being displayed for each Model. Instead of using id values, the approach we take when using DOM events is to select the buttons based on the type and value properties of the HTML tag.

"click input[type='button'][value='Edit']"
"click input[type='button'][value='Delete']"

[type='button'][value='Edit'] is an AND operation, while [value="X"],[value='Y'] is an OR operation (note the comma).

This is the typical approach you take when repeatedly displaying the same view (reusing the same template). These operations can also be combined. In actuallity, using the same id values doesn't cause a problem at all. However, you might be strict with yourself about these things ;-)

When the user clicks the Edit button, the template switches to:
<script id="edit-user-template-with-div" type="text/html">
	<div>
		Edit User <%= usrId %>
		<!-- Not using id attributes because more than one row can
		be edited simultaneously -->
		<br />Name: <input class="js-edit-name-textbox" value="<%= name%>" type="text" />
		<br />Email: <input class="js-edit-email-textbox" value="<%= email%>" type="text" />
		<br />Username: <input class="js-edit-username-textbox" value="<%= username%>" type="text" />
		<br />Password: <input class="js-edit-password-textbox" value="<%= password%>" type="password" />
		<br /><input type='button' value='Update' /><input type='button' value='Cancel' />
	</div>
	<br />
</script>

As a result, the UserView is a little more interesting because we have to keep track of the view state (whether it is in edit mode) and switch the events being used to match the rnedered HTML. Using delegateEvents() provides a number of advantages over manually using jQuery to bind events to child HTMLelements during render. All attached callbacks are bound to the view before being handed off to jQuery, so when the callbacks are invoked, "this" continues to refer to the view object. When delegateEvents is run again, perhaps with a different events hash, all callbacks are removed and delegated afresh -- useful for views that need to behave differently when in different modes as in this example where we want to switch to an Edit mode.

When the Update button is clicked, we change the Model attribute values using the Model classes properties. Since the Model belongs to the Collection, the Model is able to sync the changes to the REST webservice for us when we call save() on it. In this case, the save() call causes a REST PUT operation to be performed. The Model class gets the URL from the Collection it belong to, so as long as the url has been set on the Collection class the Model will automatically send the PUT request.

Note: For the edit process, I've used a bit of a bodge to allow me to use read the textboxes for the update operation.
Say the model id is 10, then the view is rendered as follows: <span id=10><input ..... class="js-edit-name-textbox" /> etc </span>
Which means we can identify the textbox using the id "10.js-edit-name-textbox", meaning we can use jQuery to look for the HTML element with the id of "10" (the Model id value) which has the CSS class "js-edit-name-textbox" applied to it. Of course, there is only one HTML textbox that will be selected this way for each Model in the Collection. Hence, the jQuery selector reading the "name" textbox is given by "#" + this.model.UsrId + " .js-edit-name-textbox".

When the Delete button is clicked, the Model is deleted by simply destroying it causing a REST DELETE operation. Destorying a model triggers a "destroy" event event on the model, which will bubble up through any collections that contain it.

The overall the View code looks like this:

export class UserView extends Backbone.Marionette.ItemView<UserModel>
{
	private _editMode: boolean = false;
	private _editEvents: any;

	constructor(childViewOptions: Backbone.Marionette.ItemViewOptions<UserModel>)
	{
		super(_.extend({
			// Add a span tag...any className, id and client attributes properties
			// priovided are associated with the span tag.
			// Hence, the 'id' here becomes the JS id attribute value when the
			// span tag is rendered.
			tagName: "span",
			id: childViewOptions.model.UsrId
		}, childViewOptions));

		//Provide DOM events for Edit and Delete
		this.events = <any>{
			"click input[type='button'][value='Edit']": this.EditButtonClick,
			"click input[type='button'][value='Delete']": this.DeleteButtonClick
		};

		// Note: I defined "editevents", it's  not part of Backbone or Marionette
		this._editEvents = {                   
			// We switch to these event handlers when we switch to the edit template
			"click input[type = 'button'][value = 'Update']": this.UpdateButtonClick,
			"click input[type = 'button'][value = 'Cancel']": this.CancelButtonClick
		}

		this.delegateEvents();
	}

	// Override
	getTemplate(): any
	{
		return this._editMode ? "#edit-user-template-with-div" : "#user-template";
	}

	private EditButtonClick = (): void =>
	{
		// SWITCH TO EDIT ROW MODE
		this._editMode = true;
		this.delegateEvents(this._editEvents);
		this.render();
	}

	private DeleteButtonClick = (): void =>
	{
		this._editMode = false;

		this.model.destroy();

		// You can also check for success or error
		//this.model.destroy({
		//    error: function () {
		//    }
		//});
	}

	private UpdateButtonClick = (): void =>
	{
		this._editMode = false;

		this.model.Name = $("#" + this.model.UsrId + " .js-edit-name-textbox").val();
		this.model.Email = $("#" + this.model.UsrId + " .js-edit-name-textbox").val();
		this.model.Username = $("#" + this.model.UsrId + " .js-edit-username-textbox").val();
		this.model.Password = $("#" + this.model.UsrId + " .js-edit-password-textbox").val();
		this.model.save();

		// SWITCH TO DISPLAY ROW MODE
		this.delegateEvents(<any>this.events);
		this.render();
	}

	private CancelButtonClick = (): void =>
	{
		this._editMode = false;

		// Do Nothing

		// SWITCH TO DISPLAY ROW MODE
		this.delegateEvents(<any>this.events);
		this.render();
	}
}

We use the CompositeView class like this:

class ExamplePage
{
    PageLoad(): void
    {
        let userCollection: ExampleRestHandlingNS.UserCollection = new ExampleRestHandlingNS.UserCollection();

        // I'm not interested in the "model" for this CompositeView, we are
        // just using the CompositeView to display the user list and an add new
        // user form. The add new user form can then use the "collection" to create
        // a new user and redisplay the list. You could do this using a separate CollectionView
        // and ItemView, and pass them the same collection object.
        let view: ExampleRestHandlingNS.UserCompositeView = new ExampleRestHandlingNS.UserCompositeView(
            userCollection,
            "#dataform"         // Attach the CompositeView to the HTML form
        );

        view.render();
    }
}

Details

Continuing the previous exercise, look at the code for the REST webservice you have written and check that it correctly handles, adding, editing and deleting books. Modify your application code to use the webservice for all the CRUD operations.

Hints

  1. Check the URL of your webservice
  2. If you are searching for a Model to edit, don't forget you will need to fetch() the Model data and modify the URL to add the book title (or another value) that you are searching for.
  3. Make sure the idAttribute matches the primary key field name from your database table