Showing posts with label jQuery. Show all posts
Showing posts with label jQuery. Show all posts

Friday, June 22, 2012

Realtime data editing using an Excel-like grid and KnockoutJs

Sometimes you need to edit a batch data requiring an agile UI that allow:
  • Send changes without need confirmation
  • Quickly Navigate to any field that you want edit
  • Provide a visual indicator of the updated fields
An example: Realtime Weather Forecast Editor
On this example, the main requirement is allow quickly edit the cities temperatures at different day times. Basically for each city could edit the morning, afternoon or midnight temperature. For simplicity, the cities list are stored in a session variable.
This is a list of the implementation techniques for the app main features:
  • Updates are sent back to the server when textboxes lost focus: using a KnockoutJs extender the temperature fields changes are captured and sent to server using ajax calls.
  • Updated values are highlighted: a css binding changes the textbox background color after a field is updated successfully on server
  • Key based navigation: a jQuery handler allow navigate between textboxes using the arrows keys
App Screenshot

Using a KnockoutJs extender to send the realtime updates
A new extender named "realtime" is added to the Knockout extenders. At the initialization, it stores in two private fields (cityId and dayTime) the values required to identify the time slot to be updated on the ajax call after the observable property value changes. Also, a sub-observable (hasChanged) is used to notify the highlighter databinder.
ko.extenders.realtime = function(target, options) {
    // store private vars to use as parameters on updates
    // cityId: the key of entity to be updated
    // daytime: the field of entity to be updated (Morning/Afternoon/Midnight)
    target.cityId = options[0];
    target.daytime = options[1];
    
    //add a sub-observable to mantain a changed flag
    target.hasChanged = ko.observable(false);

    //define a function to send updates
    function update(newValue) {
             $.ajax({
                 url: "/home/updateforecast", 
                data: { cityId: target.cityId, daytime: target.daytime, value: newValue },
                type: "post", 
                success: function(result) 
                {
                    if(result) 
                        target.hasChanged(true);
                }
            });
    }

    //update whenever the value changes
    target.subscribe(update);

    //return the original value
    return target;
};

Applying the extender
The extender is applied on the ForecastModel temperature fields
var model = @Html.Raw(new JavaScriptSerializer().Serialize(Model));
var viewModel = { 
    forecasts :  ko.observableArray(ko.utils.arrayMap(model, function(forecast) {
        return new ForecastModel(forecast);
    }))
   
}; 
   
function ForecastModel(forecast) {
    this.City = ko.observable(forecast.City);
    // extend the observable fields that will be updated in realtime
    this.Morning = ko.observable(forecast.Morning).extend({realtime:[forecast.Id, "Morning"]});
    this.Afternoon = ko.observable(forecast.Afternoon).extend({realtime:[forecast.Id, "Afternoon"]});
    this.Midnight = ko.observable(forecast.Midnight).extend({realtime:[forecast.Id, "Midnight"]});
}

ko.applyBindings(viewModel);
Configuring the View
A table (tblForecasts) is used to display the cities names and edit the different day times temperatures. On each temperature textbox the "value binding" is applied to each temperature field. Also, a "css binding" to the hasChanged sub-observable is applied in order to highlight the textbox background color when a change callback returns successfully from server.
<h1>
    Realtime Weather Forecast Editor</h1>
<table id="tblForecasts">
    <thead>
        <tr>
            <th>
                City
            </th>
            <th>
                Morning
            </th>
            <th>
                Afternoon
            </th>
            <th>
                Midnight
            </th>
        </tr>
    </thead>
    <tbody data-bind="template: { name: 'forecastRowTemplate', foreach: forecasts }">
    </tbody>
</table>
<script type="text/html" id="forecastRowTemplate"> 
<tr> 
    <td><span data-bind="text: City"/></td> 
    <td><input data-bind="value: Morning, css: { hasChanged: Morning.hasChanged }"/></td> 
    <td><input data-bind="value: Afternoon, css: { hasChanged: Afternoon.hasChanged }"/></td> 
    <td><input data-bind="value: Midnight, css: { hasChanged: Midnight.hasChanged }"/></td> 
</tr>
</script>
Adding keyboard navigation to table
A simple handler is attached to the textboxes keydown event to detect the arrows keys pressing and move focus
$("#tblForecasts").on("keydown", "input:text", function(e) {
        // detect arrows pressing
        if (e.keyCode < 37 || e.keyCode > 40)
            return;

        e.preventDefault();
  
        var target;
        var cellAndRow = $(this).parents('td,tr');
        var cellIndex = cellAndRow[0].cellIndex;
        var rowIndex = cellAndRow[1].rowIndex;

        switch (e.keyCode) {
            // left arrow                 
            case 37:
                cellIndex = cellIndex - 1;                        
                break;
            // right arrow                 
            case 39:
               cellIndex = cellIndex + 1;                        
                break;
            // up arrow                 
            case 40:
                rowIndex = rowIndex + 1;
                break;
            // down arrow                 
            case 38:
                rowIndex = rowIndex - 1;
                break;
        }
        target = $('table tr').eq(rowIndex).find('td').eq(cellIndex).find("input:text");
        if (target != undefined) {
            target.focus();
            target.select();
        }
});
Server side code
The Forecast Model
public class Forecast
{
    public int Id { get; set; }
    public string City { get; set; }
    public int Morning { get; set; }
    public int Afternoon { get; set; }
    public int Midnight { get; set; }
}
The Controller
Just contains the code to initialize the cities list on session and a method to update the temperatures
private const string C_ForecastModels = "ForecastModels";

// Expose the stored Forecasts from Session
public IEnumerable<Forecast> ForecastModels
{
    get
    {
        if (Session[C_ForecastModels] == null)
        {
            Session[C_ForecastModels] = new[] {
                new Forecast { Id=1, City="Barcelona", Morning=90, Afternoon=45, Midnight=40},                new Forecast { Id=2, City="Berlin", Morning=78, Afternoon=34, Midnight=0 },
                new Forecast { Id=3, City="New York", Morning=45, Afternoon=35, Midnight=20 },
                new Forecast { Id=4, City="Sydney", Morning=65, Afternoon=45, Midnight=40 },
                new Forecast { Id=5, City="Tokio", Morning=74, Afternoon=70, Midnight=60 }
            };
        }
        return Session[C_ForecastModels] as IEnumerable<Forecast>;
    }
    set { Session[C_ForecastModels] = value; }
}

 public ActionResult Index()
 {
     // Return the Session stored Forecasts
     return View(ForecastModels);
 }

 //
 // Update Forecast value for selected daytime (Morning/Afternoon/Midnight)
 [HttpPost]
 public JsonResult Updateforecast(int cityId, string daytime, int value)
 {
     var cityForecast = ForecastModels.SingleOrDefault(forecastModel => forecastModel.Id == cityId);
     if (cityForecast == null)
         return Json(false);
     switch (daytime)
     {
         case "Morning":
             cityForecast.Morning = value;
             break;
         case "Afternoon":
             cityForecast.Afternoon = value;
             break;
         case "Midnight":
             cityForecast.Midnight = value;
             break;
     }
     return Json(true);
 }
Conclusion
This is a basic example to demonstrate the KnockoutJs extenders usage. The realtime edition on the grid and the keyboard navigation resembles the old DOS applications, but still is a powerful technique for some specific scenarios (I've worked in some projects on that this type of UI was explicitly required by the client or app end users).
Enjoy!
Project files

Sunday, June 29, 2008

jQuery Carousel Example

A cool way to show a items gallery is using a scrollable div as Amazon displays related books for the current selected book. This technique is known as "carousel". I made an example (Download source (85Kb)) that shows a list of motherboards using a jQuery Carousel Component populated dinamically via Ajax queries (useful when haves a large items quantity to show)



Server side pagination method
The request/response to the method that returns a "page" of the carousel are maded using JSON.
For simplicity the pagination is done against a generic collection, but easily can be a database query.


[WebMethod]

[ScriptMethod(ResponseFormat = ResponseFormat.Json)]

public static List<Motherboard> GetItems(int pageIndex, int pageSize)

{

List<Motherboard> motherboards = new List<Motherboard>

{

new Motherboard("Asus P5Q Deluxe", "Asus P5Q Deluxe Motherboard", "Intel Socket T LGA775, P45 Express, ATX, 16GB DDR2, 1600MHz FSB"),

new Motherboard("GigaByte GA-P35-DS3L", "GigaByte GA-P35-DS3L Motherboard", "Intel Socket T LGA775, P35 Express, ATX, 8GB DDR, 1333MHz FSB "),

new Motherboard("Intel D5400XS", "Intel D5400XS Motherboard", "Intel Socket J LGA771, P35 Express, Extended ATX, 8GB DDR2, 1600MHz FSB "),

new Motherboard("Asus M3N-HT Deluxe Mempipe", "Asus M3N-HT Deluxe Mempipe Motherboard", "Socket AM2+, nVIDIA nForce 780a, ATX, 8GB DDR2, 2600MHz FSB"),

new Motherboard("ABIT IP35 PRO XE", "ABIT IP35 PRO XE Motherboard", "Intel Socket T LGA775, P35 Express, ATX, 8GB DDR2, 1600MHz FSB"),

new Motherboard("Asus Striker Extreme", "Asus Striker Extreme Motherboard", "Quad-Core/Core 2 Duo Extreme/Core 2 Duo/Pentium D/Celeron D, Socket T"),

new Motherboard("XFX MB-N780-ISH9", "XFX MB-N780-ISH9 Motherboard", "Intel Socket T LGA775, nForce 780i SLI, ATX, 8GB DDR2, 1333MHz FSB "),

new Motherboard("HP Asus P5BW-LA", "HP Asus P5BW-LA Motherboard", "Intel Socket T LGA775, P965 Express, Micro ATX, 8GB DDR2, 1066MHz FSB"),

new Motherboard("Biostar P4M900-M4", "Biostar P4M900-M4 Motherboard", "Core 2 Quad/Pentium D/Pentium 4/Celeron D, Socket T SLI, ATX, 8GB DDR2, 1333MHz Bus"),

new Motherboard("MSI P6N Diamond", "MSI P6N Diamond Motherboard", "Intel Socket T LGA775, nForce 680i SLI, ATX, 8GB DDR2, 1333MHz FS"),

new Motherboard("Intel D915GAG", "Intel D915GAG Motherboard", "Pentium 4/Celeron D, Socket T, 915G, mATX, 4GB DDR, 800MHz FSB"),

new Motherboard("ABIT IP35-E", "ABIT IP35-E Motherboard", "Intel Socket T LGA775, P35 Express, ATX, 8GB DDR2, 1333MHz FSB "),

};

return motherboards.GetRange((pageIndex - 1) * pageSize, pageSize);

}


Client side pagination method

<script>



function mycarousel_makeRequest(carousel, first, last, per_page, page)

{

// Lock carousel until request has been made

carousel.lock();



// Call the server side method with pageIndex and pageSize parameters

$.ajax({

type: "POST",

url: "Default.aspx/GetItems",

data: "{'pageIndex':'" + page + "', 'pageSize':'" + per_page + "'}",

contentType: "application/json; charset=utf-8",

dataType: "json",

success: function(data) {

mycarousel_itemAddCallback(carousel, first, last, data.d, page);

}

});

}



function mycarousel_itemAddCallback(carousel, first, last, data, page)

{

// Unlock

carousel.unlock();



// Set size

carousel.size(12);



var per_page = carousel.last - carousel.first + 1;



for (var i = first; i <= last; i++)

{

var pos = i - 1;

var idx = Math.round(((pos / per_page) - Math.floor(pos / per_page)) * per_page);



carousel.add(i, mycarousel_getItemHTML(data[idx]));

}



};



/**

* Buil item html

*/

function mycarousel_getItemHTML(mother)

{

var item = "<div>";

item += "<img src='images/" + mother.Image + ".jpg' alt='" + mother.Title + "'>";

item += "</div>";

item += "<div class='Title'>";

item += mother.Title;

item += "</div>";

item += "<div class='Description'>";

item += mother.Description;

item += "</div>";

return item;

};



</script>



Download source: jQuery Carousel in asp.net.rar (85Kb)