Sergio and the sigil

ASP.NET MVC with jQuery SuperLoad

Posted by Sergio on 2009-08-23
The other day I wrote about the jQuery SuperLoad plugin but I couldn't offer any real working sample of an application using it. Today I'm here to rectify that and talk about the new sample code I've recently added to the project repository.

Adding products to your shopping cart

The application has a single page, which lists a few books that you can add to your shopping cart. The cart is initially empty and when we add items to it, both the cart item list and the total price (at the top of the page) get updated from a single Ajax call.

The client side

Here's sample of what each of the products' HTML looks like. They each are inside a div which's id contains the product's id.
<div id="product_8" class="product">
	<img src="../../Content/images/prod_8.jpg" align="left" />

	<div class="title">Product 8</div>
	<div class="prodPrice" >$8.88</div>
	Qty: <input type="text" size="2"  maxlength="3" 
				class="quantity" value="1"/>
	<input type="button" value="Add to cart" class="addButton" />
</div>
We just need to fire one Ajax call whenever one of those "Add to cart" buttons is clicked. Here's the code that does that.
$('.addButton').click(function() {
	var prod = $(this).parent('.product');
	var prodId = prod.attr('id').split('_')[1];
	var qty = prod.find('.quantity').val();

	$.superLoad({
		url: '/Shopping/AddItem',
		type: 'POST',
		data: { product: prodId, quantity: qty },
		success: function() { $('#empty').remove(); }
	});
	
});
As you can see, we are making a superLoad() call to the AddItem action in the ShoppingController. Since this is a data modification call, we chose to use an HTTP POST request. The posted data goes in the data option. To tidy up things, we delete the empty text from the cart (if it's still there) once the call completes successfully.

Multiple results from a single action

The challenge on the server side is to come up with a sustainable method of reusing existing actions and combine them in a single action result, formatted to SuperLoad's liking and returned to the browser. To address that issue the sample comes with an implementation of a composite action result class specifically built for the response format we are trying to create. That class is the SuperLoadResult, listed below.
public class SuperLoadResult : ActionResult
{
	public IEnumerable<SuperLoadAjaxContent> ContentItems { get; private set; }

	public SuperLoadResult(params SuperLoadAjaxContent[] contentItems)
	{
		ContentItems = new List<SuperLoadAjaxContent>();
		((List<SuperLoadAjaxContent>)ContentItems).AddRange(contentItems);
	}

	public override void ExecuteResult(ControllerContext context)
	{
		context.HttpContext.Response.ContentType = MediaTypeNames.Text.Html;

		context.HttpContext.Response.Write("<div class=\"ajax-response\">");
		foreach (var item in ContentItems)
		{
			context.HttpContext.Response.Write(
				string.Format("<div class=\"ajax-content\" title=\"{0} {1}\">",
			                  item.Command.CommandText, item.Selector));
			item.GetResult().ExecuteResult(context);
			context.HttpContext.Response.Write("</div>");
		}
		context.HttpContext.Response.Write( "</div>");
	}
}
I bet things will become clearer once I show it being used from the AddItem action. So here it goes.
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult AddItem(int product, int quantity)
{
	var sku = GetProduct(product);
	var item = new CartItem(sku, quantity);
	var cart = GetCurrentCart();
	cart.Items.Add(item);

	//add all the separate action results that we want into 
	// a SuperLoad result, mapping each one to the right 
	// selector and update type.
	return new SuperLoadResult(
		new SuperLoadAjaxContent("#cartTotal", 
		                  SuperLoadCommand.Update, 
						  () => CartTotal(cart)),

		new SuperLoadAjaxContent("#cart", 
		                  SuperLoadCommand.Append, 
						  () => CartItem(item))
		);
}
That last parameter to the SuperLoadAjaxContent's constructor is what will be invoked to provide an ActionResult-derived object, which will be executed and injected in the right place of the combined response. If I needed to update more elements, I could simply pass more instances of SuperLoadAjaxContent to my SuperLoadResult. Both CartTotal and CartItem are regular action methods that return a partial view from an .ascx template. Here's how simple the CartItem action is.
public ActionResult CartItem(CartItem item)
{
	return PartialView("CartItem", item);
}
Again, the entire sample is available for browsing, forking, or downloading at the repository page.