Adding Items

Now that the model is bound to our template, the client will reflect the current state of the model in real time.  Now we can implement add, edit, and delete functionality. We are going to use jQuery and jQuery UI for the user interface behavior.

In order to add an item, the user simply clicks the "Add an item..." link.

When that happens we toggle visibility so that the link is hidden, and a text input is shown in its place.

The code to accomplish this is dead simple:

$(document.documentElement).on("click"".board-list-additem a"function() {
         $(this).hide().prev().show().focus();
});

Since DOM events bubble up from the element that fired the event, jQuery allows you to capture an event fired by an element or any of its ancestors that match a selector, in this case ".board-list-additem a", up to and including the current jQuery object, $(document.documentElement).  We use document.documentElement in order to watch for the event anywhere on the page.  The end result is that when any element matching ".board-list-additem a" is clicked, our event handler fires.  The rest is standard jQuery methods to toggle visibility of the two elements and focus into the text field.

The other piece of the puzzle is to respond to activity while the field has focus.  Once the user has typed the description of the item, they can add the new item by either hitting ENTER or TAB.  If they hit ENTER they are returned to the previous state.  If they hit TAB, the input stays in focus and they can add another item.  If they leave the field before doing either then the value is cleared out and the item is not added.

Action Result
User presses the TAB key Create the new item. Keep focus on the input so that the user can add another item.
User presses the ENTER key Create the new item and hide the input field.
User leaves the field (via click, not tab) Discard the text and hide the input field.

Again we will use event delegation, subscribing to 3 events; keydown, keypress, and blur; using the same selector.

// Create the new item and continue when the user presses the tab key
.on("keydown"".board-list-additem input[type=text]"function(e) {
         ... 
})
 
// Create the new item when the user presses the enter key
.on("keypress"".board-list-additem input[type=text]"function(e) {
         ... 
})
 
// Hide the add field when the leaves
.on("blur"".board-list-additem input[type=text]"function(e) {
         ...
})

The first event, keydown, is used to capture the tab key.  We can't use keypress here because by the time the keypress event fires the browser has already moved focus to the next input.  The code in the event handler is fairly simple.  It checks that the key pressed was tab, and if so it calls a function to add the new item, clears out the input value, and finally stops the browser from performing the default tab behavior.

The next event, keypress, is used to capture the enter key.  This is nearly identical to the tab scenario.  The only difference is that it also ends entry mode and returns to the original state by hiding the input and showing the link.

The last event, blur, is simply an inverse of the aformentioned click event handler.  It also hides the input and shows the link.

Now let's move on to the code that actually creates the new item.

var newItem = new ListItem({
         List: parentList,
         Priority: mediumPriority,
         Sequence: parentList.get_Items().length,
         Description: textEntered,
         DateCreated: now
});

This constructor syntax is meant to mimic C# object initializer syntax.  It is essentially equivalent to constructing the object and then calling the setter for each property in turn, like so:

newItem.set_List(parentList);

newItem.set_Priority(mediumPriority);

newItem.set_Sequence(parentList.get_Items().length);

newItem.set_Description(textEntered);

newItem.set_DateCreated(now);

Multiple properties can also be set after the object has been constructed using the set function.

newItem.set({
         List: parentList,
         Priority: mediumPriority,
         Sequence: parentList.get_Items().length,
         Description: textEntered,
         DateCreated: now
});

After the new item has been constructed, the next step is to add it to the list of items.

parentList.get_Items().add(newItem);

All list properties in ExoWeb are automatically made "observable".  This means that they are extended with methods that raise events according to the nature of the modification.  In this case, all we have to do is call the add method and a collection change event is raised indicating that an item was added to the array at a particular index.  Note that we can't call the standard Array push function, since it will not automatically raise the appropriate events.

Also, remember that our template binds to the "Items" list, so when it changes the template will update the DOM accordingly.  No need to do anything else.

One other point to note is the way that our code determines what list to add the item to.  Since we're adding the item from a jQuery event handler, it is convenient to go through the DOM to find the list.

var parentList = $parentContextData(thisnullnull, List);

The $parentContextData expects its first argument to a DOM element, and searches that element and its ancestors for a data context that matches the given criteria.  The second (optional) argument is used to specify the index of the element within a list.  The third (optional) argument can be used to specify a specific number of "levels" to go up in the heirarchy.  And the fourth (optional) argument, which we are using, is used to specify the type of data to search for.

Another option is to use the id of the object in question by embedding it in an onclick event handler, for example.  This approach is convenient, especially to generate a URL in your template.  However, it is less reliable and bookmarkable when dealing with newly constructed objects.


  1. Create the javascript file board-list-additem.js under scripts
  2. Insert the following snippet into scripts/board-list-additem.js:
    (function() {
    
    	function addNewItem(textEntered) {
    		// Find the parent list
    		var parentList = $parentContextData(this, null, null, List);
    
    		// Get the default priority (medium)
    		var mediumPriority = Priority.meta.get("2");
    
    		// Use the current date and time as the date created
    		var now = new Date();
    
    		// Create the new list item
    		var newItem = new ListItem({
    			List: parentList,
    			Priority: mediumPriority,
    			Sequence: parentList.get_Items().length,
    			Description: textEntered,
    			DateCreated: now
    		});
    
    		// Add the item to the list
    		parentList.get_Items().add(newItem);
    	}
    
    	$(document.documentElement)
    
    		// Create a new item when the add link is clicked
    		.on("click", ".board-list-additem a", function() {
    			// Hide link and show input
    			$(this).hide().prev().show().focus();
    		})
    
    		// Create the new item and continue when the user presses the tab key
    		.on("keydown", ".board-list-additem input[type=text]", function(e) {
    
    			// Check for TAB to signify completion
    			if (e.keyCode == jQuery.ui.keyCode.TAB) {
    				var $this = $(this);
    				var textEntered = $this.val();
    				if (!textEntered) {
    					alert("Enter description");
    				}
    				else {
    					// Add the new item
    					addNewItem.call(this, textEntered);
    				}
    
    				// Clear the input
    				$this.val("");
    
    				// Stop default tab behavior
    				e.stopPropagation();
    				return false;
    			}
    
    		})
    
    		// Create the new item when the user presses the enter key
    		.on("keypress", ".board-list-additem input[type=text]", function(e) {
    
    			// Check for ENTER to signify completion
    			if (e.keyCode == jQuery.ui.keyCode.ENTER) {
    				var $this = $(this);
    				var textEntered = $this.val();
    				if (!textEntered) {
    					alert("Enter description");
    				}
    				else {
    					// Add the new item
    					addNewItem.call(this, textEntered);
    
    					// Clear the input
    					$this.val("");
    
    					// Hide input and show link
    					$this.hide().next().show();
    					return false;
    				}
    			}
    
    		})
    
    		// Hide the add field when the user leaves
    		.on("blur", ".board-list-additem input[type=text]", function(e) {
    			// Hide input and show link
    			$(this).hide().next().show();
    			return false;
    		});
    
    }());
    				
  3. Append the following code snippet in Views\Home\Index.cshtml under the list of script references:
    <script src="@Url.Content("~/scripts/board-list-additem.js")"></script>