<img height="1" width="1" style="display:none" src="https://www.facebook.com/tr?id=1063935717132479&amp;ev=PageView&amp;noscript=1 https://www.facebook.com/tr?id=1063935717132479&amp;ev=PageView&amp;noscript=1 "> Bitovi Blog - UX and UI design, JavaScript and Front-end development
Loading

Bitovi |

Weekly Widget 6 - Instantaneous Web Apps

Weekly Widget 6 - Instantaneous Web Apps

The Bitovi Team

The Bitovi Team

Twitter Reddit

This week's widget shows how to make "instantaneous" web apps with queued AJAX requests using can.Model and the can/model/queue plugin. The plugin puts an end to spinners, progress-bars, and "loading" text. This article covers how to make apps more responsive with queued requests and recover from service errors.

Instantaneous Web Apps

Traditional, "blocking" web apps make users wait for their changes to be persisted with a:

  • spinner,
  • progress bar,
  • disabled submit button, or
  • loading text

Queued requests enable users to make changes to data without blocking, with the speed and responsiveness of a desktop app. To illustrate the difference, we built two versions of a widget that allows a user to move a character around on the screen.

Checkout the difference between the blocking and non-blocking versions:

Blocking

The character can't move until its position is persisted to the server.

Instantaneous

The player is allowed to move freely. This is achieved with the Queued requests plugin. Lets see how it works and after that, how it is used in this widget.

Queued requests plugin

The can/model/queue plugin provides following features on top of can.Model:

Requests are queued and run sequentially

Calling .save() adds a request to the queue. The request will be made only after previous requests have completed. For example, consider creating a Task, saving it to the server, and before the response comes back, changing it and saving it two more times:

var task = new Task({name: "Wash"});
task.save(); // 1

task.attr('name',"Wash car");
task.save(); // 2

task.attr('name','Wash clothes');
task.save(); // 3

The requests with their responses are made in the following correct order:

1. POST /tasks  
        name=Wash  
   => {id: 3, name: "Wash", updated: 1363623400000}

2. PUT  /tasks/3 
        name=Wash car
   => {id: 3, name: "Wash car", updated: 1363623401000}

3. PUT  /tasks/3 
        name=Wash clothes
   => {id: 3, name: "Wash clothes", updated: 1363623402000}

Non-conflicting property changes sent back by the server are set on the model instance.

When the first request returns, the task's .attr() data is set to:

{ id: 3, name: "Wash clothes", updated: 1363623400000 }

The task has the id and updated properties returned from the initial request, but name is not overwritten. Instead it remains the last value set by .attr().

Requests are sent with a snapshot of the model instance's data

When the second request is made task.attr('name') is "Wash clothes". However, name=Wash car is sent. This is because requests are sent with a snapshot of model instance taken at the moment .save() was called.

A backup of the last successful state is kept to allow graceful error recovery.

Call .restore() to set the model instance's properties back to their last successful state. For example, if .save() should fail, the following restores task's name property to its last valid value:

task.attr('name',"Something evil");
var def = task.save();
def.fail(function(){
  task.restore();
})

The Widget

The widget is implemented as the RPG can.Control. The control:

  1. Creates a player model instance.
  2. Creates an isMoving compute that indicates if the player is moving.
  3. Renders the map and live-bound player instance.
  4. Saves the initial position of the player.

    var RPG = can.Control({
      init: function() {
        this.player = new Player({
          position: {
            top : 0,
            left : 0,
            direction: “down”
          }
        });
      this.isMoving = can.compute(false);
      this.element.html(can.view(‘avatar’, {
        player : this.player,
        isMoving : this.isMoving
      }));
      this.savePosition();
    },
    

When an arrow key is pressed, the control updates the character's position. With live binding, the character moves as the position attributes are changed:

"{document} keydown": function(el, ev) {
  this.move(directions[ev.which]);
},
move: function(direction) {
  var position, direction, newPosition;
  if(direction) {
    this.isMoving(true);
    this.player.attr(
      'position.direction', 
      direction.toLowerCase()
    );
    if(direction === 'UP') {
      this.player.attr(
        'position.top', 
         min(this.player.attr('position.top') - 5, 0)
      );
    }
    ...
    this.savePosition();
  }
},
...

The control saves the current position of the character when its position is updated:

savePosition: function() {
  this.player.save(undefined, $.proxy(this.loadBackup, this));
}

If the request fails, loadBackup restores the character's position to the last valid state:

loadBackup: function() {
  alert('You walked in to the fire.' + 
        'Resurrecting you to the last good position');
  this.player.restore(true);
}

The widget without the queue plugin seems much slower because character's movements are blocked while the state is persisted.

With the queue plugin, you can just fire the save requests after each move and the plugin will take care of the requests ordering without blocking - moving around and replays are smooth and in the right order.

Queue state

Queue plugin exposes the hasQueuedRequests (can.Model.prototype.hasQueuedRequests) live-bindable function which allows you to monitor the queue state. In this demo, it is used to show the "Saving!" message while the requests are running:

<span>{{#hasQueuedRequests}}Saving!{{/hasQueuedRequests}}</span>

Conclusion

Queued requests allow you to save models early and often, without caring about pending requests. The can/model/queue plugin enables "instantaneous" web apps. You can use it in your projects without any changes to the code, it just works.

What will you build with it?