Windows Azure Table Explorer

This morning I was reading back over the archives of Jeff Wilcox's blog and came across this post detailing a Single Page App that he built for browsing table service data on an Azure storage account.

As I've been learning Ember I thought it would be fun to take the jQuery front end and replace it with Ember.

Overview

So first thing I'll just do a quick run through Jeff's application as it stands (you can find the GitHub repo here).

The application is up and running on Azure websites here https://waztable.azurewebsites.net/ and allows you to browse the Table Service data for a particular Storage Account.

Jeff is using the Windows Azure SDK for Node to build a simple API in Node for his client side app. All requests to this API need to provide credentials which are the storage account name and access key, invalid credentials are rejected with a HTTP 401 error.

GET /json/table/?account={storage-account}&key={storage-key} - returns a list of tables defined on the storage account.

Response
1
2
3
4
5
6
7
{
  "ok":true,
  "result":{
    "tables":["logs","people"],
    "name":"brewingcode"
  }
}

GET /json/table/{table-name}?account={storage-account}&key={storage-key}&top={results-per-page}&nextPartitionKey={partition-key}&nextRowKey={row-key} - returns a number of records specified by the top parameter for the specified table. If more records are available a continuation token will also be returned, this can the be used in subsequent requests to page through remaining records via the optional nextPartitionKey & nextRowKey parameters.

Response
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
  "ok":true,
  "table":{
    "continuation":{"nextPartitionKey":"1!8!bG9ncw--","nextRowKey":"1!4!Mjc-"},
    "rows":[
      {
        "_":{
          "id":"https://brewingcode.table.core.windows.net/logs(PartitionKey='logs',RowKey='18')",
          "link":"logs(PartitionKey='logs',RowKey='18')",
          "updated":"2013-12-05T15:26:07Z",
          "etag":"W/\"datetime'2013-11-18T16%3A09%3A20.1183355Z'\""},
          "PartitionKey":"logs",
          "RowKey":"18",
          "Timestamp":"2013-11-18T16:09:20.118Z",
          "Level":"info",
          "Message":"Message - 18"
        },
        // ...
    ]
  }
}

DELETE /json/table/{table-name}/{partition-key}/{row-key}?account={storage-account}&key={storage-key} - deletes the specified row from the table.

Response
1
2
3
4
5
6
7
8
9
10
{
  "ok":true,
  "response":{
    "isSuccessful":true,
    "statusCode":204,
    "headers":{
      // ...
    }
  }
}

Front end

The UI itself is very simple and consists of the following pages.

First we have the login/credentials page where you enter your storage account name and access key.

Credentials page

If you've provided valid credentials then you'll see a listing of all the tables defined in the specified storage account.

Storage Account table listing

Finally you can then select and view table data as seen below. Multiple rows can be selected which activates a master/detail view and the option to delete the selection items as seen in the 2nd screen shot.

Storage Account table listing

Storage Account table listing with multiple items selected

Ember Tooling

Before we get into converting this into Ember we need to talk a little about tooling. If you're building an application with Ember then you're going to need to have some kind of tooling/build process in place. At the moment I like to use Yeoman for this.

For those of you have haven't heard of it before Yeoman is a collection of Grunt build scripts, scaffolding tools, Twitters Bower package manager and a bunch of opinion.

On Windows assuming you have Chocolatey installed (and if not why not?) then getting Yeoman installed is as simple as...

1
cinst Yeoman

With Yeoman installed next we'll want to add the scaffolding support for the Ember tools.

1
npm install -g generator-ember

With that we'll create a directory for our app and use the Yeoman scaffolding tools for Ember to create a basic structure.

1
2
3
mkdir emberapp
cd emberapp
yo ember

This will create the directory structure seen below.

Yeoman directory structure

The files/directories in the root directory to call out are.

  • bower.json - a list of packages from Bower that we are using in our app.
  • Grunt.js - the Grunt file contains the build scripts for our application, specifically we'll be using the following commands.
    • grunt server - runs your application in preview mode at http://localhost:4000. The file system is monitored for changes and the application is re-built as required.
    • grunt test - runs your application unit tests inside PhantomJS
    • grunt - builds the application in release mode and ready for deployment, the output of this task is created in the dist directory.
  • packages.json - a list of the npm packages required by Yeoman.
  • .\app - contains the source code for the Ember application.
  • .\dist - when the application is built in release mode the output of the task will be copied to this directory.
  • .\test - contains your unit tests for the application.

Before we use npm install to get all the required node modules for our application we need to make a small alternation to the packages.json that was just created the emberapp directory. There is problem with the grunt-contrib-imagemin module on Windows. It depends on jpegtran-bin and the latest version of this can't be built from source on Windows and thus installed via npm. Instead we can specify the previous version in the package.json, this should be above grunt-contrib-imagemin.

packages.json
1
2
3
4
// ...
"jpegtran-bin": "0.2.0",
"grunt-contrib-imagemin": "~0.2.0",
// ...

With this alteration in place you can go ahead and run npm install to get all the required dependencies.

So at this point we will have the basic structure of our Ember app scaffolded out and everything is ready to be built and run. We can boot the app with the grunt server command, this will run the app on http://localhost:4000.

Ember Concepts

Now we are ready to start building out the app however before that I'll just do a super quick run down through the main components in Ember.

  • Router - Used to define routes for your application. In Ember URLs contain the state required for the Router to create a corresponding set of Controllers, Routes & Templates that are needed to restore the app to the point defined by the URL.
  • Routes - a Route is responsible for loading a model and rendering a template into an outlet.
  • Model - a Model holds data that is persisted on the server, in our case Models will be created from the JSON returned by the API defined at the top of the post.
  • Controller - a Controller proxies it's related model and can be used to add any UI specific properties or functionality required. Coming from a XAML background I like to think of Ember Controllers as what we would call a ViewModel in XAML world.
  • Template - Ember uses the Handlebars templating language. A Template is bound to a Controller and is responsible for rendering UI for a specific portion of the page which is known as an Outlet. Templates can also contain Outlets so you can easily build your UI with multiple templates nested within each other.
  • View - in Ember Views are used when you need more control over exactly how the DOM is manipulated, typically they are quite useful when integrating with other libraries such as jQuery plugins.

Finally we'll have a quick look at the .\app directory which contains the source for our Ember application.

Ember application directory structure

  • .\bower_components - packages referenced in bower.json are stored in this directory.
  • .\img - We're using Bootstrap so this directory contains some glyphicon image files.
  • .\scripts - the JavaScript source. In the root we have a file app.js which is the application entry point, after that we have separate sub-directories for our various Ember components e.g. controllers, models, routes & views.
  • .\styles - contains the SASS source used to produce our CSS.
  • .\templates - Handlebars templates
  • index.html - the container for our Ember application.

An excerpt of the index.html file is listed below, the important parts are the build:js snippets. Yeoman uses these markers to insert Bower libraries, Handlebars templates and the application source itself. Depending on the grunt task executed what's injected into these placeholders will be different, for example when running grunt the application is built in release mode and so files are concatenated, minified & uglified.

index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<!DOCTYPE html>
    // ...
    <body>
        <!-- build:js scripts/components.js -->
        <script src="bower_components/jquery/jquery.js"></script>
        <script src="bower_components/momentjs/moment.js"></script>
        <script src="bower_components/underscore/underscore.js"></script>
        <script src="bower_components/handlebars/handlebars.runtime.js"></script>
        <script src="bower_components/ember/ember.js"></script>
        <!-- endbuild -->

        <!-- build:js(.tmp) scripts/templates.js -->
        <script src="scripts/compiled-templates.js"></script>
        <!-- endbuild -->

        <!-- build:js(.tmp) scripts/main.js -->
        <script src="scripts/combined-scripts.js"></script>
        <!-- endbuild -->

        <!-- build:js scripts/plugins.js -->
        <script src="bower_components/bootstrap-sass/js/bootstrap-affix.js"></script>
        <script src="bower_components/bootstrap-sass/js/bootstrap-alert.js"></script>
        // ...
        <!-- endbuild -->
    </body>
</html>

Replacing jQuery with Ember

The first thing you want to do when starting an Ember app is to get your routes defined. Below are the route definitions for the app, I've also included comments for routes that Ember will create by itself. Ember is a very convention based framework and in particular route names are important as these are used in the controller, model, route & template naming conventions.

Application Routes
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
App.Router.map(function () {

  // application
  // index

  this.resource("tables", function() {
    // tables.index

    this.resource("table", { path: ":table_id" }, function() {
      // table.index

      this.route("page", { path: ":page_id" } );
    });
  });

});

With these routes defined we can start fleshing out the templates and layout of our application. The application.hbs template will be rendered by Ember when your application starts so it's the place to define your page layout structure. The header and footer sections have been created as separate template fragments header.hbs and footer.hbs and are included using the {{partial "{template-name}"}} helper. Note that partial templates must always be named with an underscore prefix however this prefix is excluded when using the partial helper. e.g. A partial template _header.hbs is included in a template using {{partial "header"}}. Also the binding context of the partial is not changed, the fragment is just treated as if it were inline with the main template.

Along with partials we also have two outlets defined in this template. Outlets are just placeholders that other templates can be rendered into. Templates can have a single default outlet which is not given a name, if a template has multiple outlets then the non-default items must be allocated unique names. In this case we have a separate outlet for modal dialogs which will be covered later in the post.

application.hbs
1
2
3
4
5
6
7
8
9
10
11
12
{{partial "header"}}

<div class="container">

  {{outlet}}

  <hr/>

  {{partial "footer"}}

</div>
{{outlet "modalOutlet"}}

The next default template that Ember will render is the index.hbs template. As we want the landing page of our application to be the credentials/login screen we will create an IndexController that will be bound to this template and control the credentials/login process.

IndexController.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
App.IndexController = Ember.ObjectController.extend({
  storageAccount: "",
  storageKey: "",
  attemptedTransition: null,
  credentials: null,
  
  usingSSL: function() {
      return (document.location.protocol === "https:");
  }.property(),

  loginDisabled: function() {
      var credentials = this.getProperties("storageAccount", "storageKey");

      if (credentials.storageAccount.length > 0 && credentials.storageKey.length > 0 ) {
          return false;
      }

      return true;

  }.property("storageAccount", "storageKey"),

  login: function() {
      var self = this,
          credentials = this.getProperties("storageAccount", "storageKey"),
          attemptedTransition = this.get("attemptedTransition"),
          tables = App.Tables.find(credentials);

      tables.then(function(data) {
          // success
          self.set("credentials", credentials);

          if (attemptedTransition) {
              attemptedTransition.retry();
              self.set("attemptedTransition", null);
          } else {
              self.transitionToRoute("tables.index");
          }

      }, function(reason) {
          // failure
          self.set("credentials", null);
          self.send("error", reason);
      });

  }
});
  • storageAccount - bound to the corresponding input element in the template.
  • storageKey - as above.
  • attemptedTransition - route transitions can be cancelled if the application doesn't have valid credentials. If this is the case the application will redirect back to the index route and store the originally attempted route transition here. If the credentials/login is successful we can then retry the original transition and return the app to the requested URL.
  • credentials - If the supplied storageAccount and storageKey are valid then these credentials will be stored here and used for subsequent API requests throughout the app.
  • usingSSL - true/false depending on whether the page is using SSL. This is used to toggle the warning message within the template that is displayed if the page is not using SSL.
  • loginDisabled - toggles whether the "Explore" button is enabled/disabled. This is a computed field and is dependent on the storageAccount & storageKey fields, if either field is empty then the button is disabled.
  • login - handles the click action of the "Explore" button. This action will use the method App.Tables.find passing the credentials entered by the user, this returns a promise & when it resolves successfully we will store the credentials and redirect to the appropriate route. If the promise resolves with an error then credentials are cleared and we send an error message. This will eventually bubble up to the ApplicationRoute which will in turn trigger a modal dialog to display an error message.

Below is the template that will be bound to the IndexController. What we are doing here is binding some CSS classes to corresponding properties on the controller using the {{bindAttr}} helper. This helper can be used to bind any element attribute to a corresponding property on the controller.

We then use the Ember.TextField view to create a text and password type input fields and bind them to the storageAccount and storageKey fields on the controller.

Finally the click action on the "Explore" button is set to be handled by the login function on our controller.

index.hbs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<div id="credentials" class="hero-unit">
  <h1>Explore a storage account</h1>

  <div id="nossl" {{bindAttr class=":alert :alert-block usingSSL:hide:show"}}>
    <h4>Security Warning!</h4>
    <p>You are not currently browsing this site through an encrypted SSL session. If you submit your
    Windows Azure storage account name and access key, this information will be sent over the
    wire unencrypted. Don't do it. Please.</p>
  </div>

  <p id="ssl" {{bindAttr class="usingSSL:show:hide"}}>
    You're using an SSL connection with this cloud service - your credentials will be transmitted securely. The credentials are sent with every dynamic request and immediately destroyed.
  </p>

  <p>
    {{view Ember.TextField valueBinding="storageAccount" placeholder="Storage account" class="span5"}}
  </p>

  <div class="input-prepend">
    <span class="add-on"><i class="icon-lock"></i></span>
    {{view Ember.TextField valueBinding="storageKey" placeholder="Access account" type="password" class="span5"}}
  </div>

  <p>
    <button class="btn btn-primary" {{action login}} {{bindAttr disabled="loginDisabled"}} id="setCredentials">Explore &raquo;</button>
    <a href="https://github.com/jeffwilcox/azure-table-explorer" class="btn" target="_blank">App source &raquo;</a>
  </p>
</div>

Models

In order to build out the rest of the application we'll need to go ahead and create the various models that we will need we are..

  • Tables - storage account details and a list of associated tables.
  • Table - table id and a list of continuation tokens that are required for pagination.
  • Page - A set of rows for a particular table.
  • Row - A individual table record.

As we are not using Ember Data we'll just extend Ember.Object when creating our models. The Ember.Object.extend method is used to define properties for each model, once defined an instance of the Tables model can be created using the App.Tables.create method. The reopenClass method can be used to add static properties and methods to the model.

Tables.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
App.Tables = Ember.Object.extend({
  id: null,
  tables: null
});

App.Tables.reopenClass({
  find: function(credentials) {

    var requestData = {
      account: credentials.storageKey ? credentials.storageAccount : undefined,
      key: credentials.storageKey,
      top: 10
    };

    return Ember.RSVP.Promise(function(resolve, reject) {

        App.ajax("/json/table", {data: requestData}).then(function(value) {
          // success
          resolve(
            App.Tables.create({
              id: value.result.name,
              tables: _.map(value.result.tables, function(item) {
                return App.Table.create({id: item});
              })
            })
          );
        }, function(error) {
          // fail
          reject(error);
        });

    });
  }
});
  • id - the storage account name.
  • tables - a list of App.Table objects.
  • find - uses an internal Ajax helper to make the API request, this helper returns a promise. The find method itself returns a promise that wraps that returned by the ajax helper, when the ajax promise resolves the JSON response is unpacked and converted into an App.Tables instance.
Table.js
1
2
3
4
App.Table = Ember.Object.extend({
  id: null,
  continuationTokens: null
});
  • id - the table name, this is unique within the storage account.
  • continuationTokens - a list of continuation tokens that are required for pagination. This will be covered in more detail later in the post.
Page.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
App.Page = Ember.Object.extend({
  id: null,
  continuation: null,
  rows: null
});

App.Page.reopenClass({
  find: function(tableName, credentials, pageId, continuation) {

    var partitionKey,
      requestData = {
      account: credentials.storageKey ? credentials.storageAccount : undefined,
      key: credentials.storageKey,
      top: 10
    };

    // ...
  }
});
  • id - the page number.
  • continuation - if more results are available after the current page then this will contain the continuation token returned by Azure.
  • rows - a list of App.Row instances that make up the page results.
  • find - as with App.Tables.find this returns a promise that will eventually resolve to return an instance of an App.Page model.
Row.js
1
2
3
4
5
6
7
App.Row = Ember.Object.extend({
  id: null,
  PartitionKey: null,
  RowKey: null,
  isSelected: false,
  partitionKeyChanged: false
});
  • id - a unique id for the row, this is a combination of the PartitionKey & RowKey.
  • PartitionKey - the Azure table service partition key.
  • RowKey - the Azure table service row key.
  • isSelected - this probably shouldn't be a property on the model, it's used to toggle the selection status of rows within the table.
  • partitionKeyChanged - as above this property is used when rendering the table, when the partition key is different from the previous row a new partition key header is rendered.

Authentication

In order to use the application a valid set of credentials needs to be defined. Outside of the login/landing page none of the other routes should be accessible without valid credentials, instead we just want to redirect back to the login page. If valid credentials are subsequently entered we can then redirect back to the original route that was requested.

This is easy to do in Ember, what we can do is define an AuthenticatedRoute as seen below and have all the routes that require credentials extend from this.

We can use the beforeModel hook on the route which allows us to cancel the current route transition and redirect elsewhere. In the _redirectToLogin function we grab an instance of the index controller and then set the currently attempted route transition before redirecting the index route itself. This allows us to redirect back to the original requested route if needed.

Finally we can use the events hash to define an event handler for any errors that bubble up to the route. The Node API will throw a HTTP 401 Unauthorized error for any requests with invalid credentials, in this case we can also just redirect back to the login page. For any other errors we will just re-throw the error which will eventually be handled by the Application route.

AuthenticatedRoute.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
App.AuthenticatedRoute = Ember.Route.extend({
  credentials: null,

  _redirectToLogin: function(transition) {
      var indexController = this.controllerFor("index");

      indexController.set("attemptedTransition", transition);
      this.transitionTo("index");
  },

  beforeModel: function(transition) {
      var credentials = this.controllerFor("index").get("credentials");

      if (!credentials) {
          this._redirectToLogin(transition);
      } else {
          this.set("credentials", credentials);
      }
  },

  events: {
      error: function(reason, transition) {
          if (reason.status === 401) {
              this._redirectToLogin(transition);
          } else {
              throw {error: reason, transition: transition};
          }
      }
  }
});
ApplicationRoute.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
App.ApplicationRoute = Ember.Route.extend({

  events: {
    error: function(error) {
      var errorController = this.router.container.lookup("controller:error");

      errorController.set("model", error);

      this.render("errorModal", {
        into: "application",
        outlet: "modalOutlet",
        controller: errorController
      });
    },

    closeModal: function() {
      this.render("emptyTemplate", {
        into: "application",
        outlet: "modalOutlet"
      });
    }
  }

});
  • events - in ApplicationRoute the events hash lets us define application wide handlers for certain items. For example in BootstrapModalView we trigger a closeModal event, this will then bubble up through the corresponding Controller, Route and finally ApplicationRoute until it is handled.
  • error - used to provide an application wide hook for any unhandled errors, once an error is caught a modal dialog is displayed.
  • closeModal - closes the currently opened modal dialog, to do this we just render an empty template into the modal outlet.

Modal Dialogs

We are using the Bootstrap modal dialog to display errors that occur in the application. When we want to use other libraries that manipulate the DOM we need to do a little bit of work to ensure that whatever we are using doesn't step on Embers toes and interfere with its rendering.

Typically what you'll end up doing is creating a View which wraps the library/plugin you are using. A View contains two import hooks which are didInsertElement and willDestroyElement. From Embers perspective a View is represented by a single DOM element and after this has been inserted into the DOM it will call didInsertElement. We can use this hook to create an instance of the Bootstrap Modal dialog plugin, render it within our container DOM element and hook up any events to corresponding handlers within the view.

BootstrapModalView.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
App.BootstrapModalView = Ember.View.extend({
  layoutName: "modal_layout",

  backdrop: true,
  keyboard: true,
  backdropClass: "",
  positionSelector: null,
  position: "bottom",

  didInsertElement: function() {
    var controller = this.get("controller"),
      pos, tp, actualWidth, actualHeight;

    this.$(".modal").modal({
      dynamic: true,
      keyboard: this.get("keyboard"),
      backdropClass: this.get("backdropClass")
    });

    this.$(".modal").one("hidden", function() {
      if (controller) {
        controller.send("closeModal");
      }
    });

    // Dialog placement code
    // ...
  },

  close: function() {
    this.$(".modal").modal("hide");

    this.get("controller").send("closeModal");
  }
});
  • layoutName - a layout template can be specified, in our case modal_layout is simply a DIV with its css class set to modal. The {{yield}} keyword is a placeholder that indicates where the view will be rendered within the layout template.
  • backdrop - true/false, sets whether dialog should include a backdrop element.
  • keyboard - true/false, closes the modal when the escape key is pressed.
  • backdropClass - the css class of the backdrop element.
  • positionSelector - the dialog can be displayed relative to another element, this should be a selector expression.
  • position - if position relative to another element this should be one of top, right, bottom or left.
  • didInsertElement - creates an instance of the Bootstrap Modal dialog plugin.
  • close - the errorModal.hbs template triggers this action when it's close button is clicked. We in turn trigger a closeModal which will eventually be handled by the application route.

This view can then be simply used as seen below in the errorModal.hbs template.

errorModal.hbs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{{#view App.BootstrapModalView backdropClass="modal-backdrop-transparent"}}
  <div class="modal-header">
      <button type="button" class="close" {{action close target="view"}}>&times;</button>

      <h3 id="modal_error_subject">Error</h3>
  </div>

  <div class="modal-body">
    <p id="modal_error_text">Whoops, something has gone wrong!</p>
  </div>

  <div class="modal-footer">
    <a class="btn" {{action close target="view"}}>Close</a>
  </div>
{{/view}}

Table list

With authentication and the modal dialog support in place we can move on to the next page that is required after entering in storage account credentials which is the table listing.

This simply displays a list of the tables that are associated with the storage account.

TablesRoute.js
1
2
3
4
5
App.TablesRoute = App.AuthenticatedRoute.extend({
  model: function() {
    return App.Tables.find(this.get("credentials"));
  }
});
  • model - as we extend from AuthenticatedRoute we have the users storage account credentials available, we then make a call to the JSON API to retrieve a list of tables defined for the account. As we haven't defined a controller Ember will automatically create one, set the model & bind it to the corresponding template.
tables.hbs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<div class="row">
  <div class="span9" id="results">
    {{outlet}}
  </div>

  <div class="span3">
    <h1 id="accountName">{{storageAccountName}}</h1>

    <div id="tableList">
      <ul class="nav nav-list">
        <li class="nav-header">Tables</li>
      {{#each table in tables}}
        <li>
          {{#linkTo table.page table App.Page.FIRSTPAGE}}
            {{table.id}}
          {{/linkTo}}
        </li>
      {{else}}
        <li>No tables defined for this storage account.</li>
      {{/each}}
      </ul>
    </div>

    <div id="rowDetails">
      {{outlet "selectedRowsOutlet"}}
    </div>
  </div>
</div>
  • outlet - before a table is selected the tables/index.hbs template will be rendered into this outlet.
  • storageAccountName - the storage account name.
  • {{#each table in tables}} - renders a list of tables that are defined in the storage account, the linkTo helper creates links to the first page of records for the selected table.
  • selectedRowsOutlet - by default nothing is rendered into this template. It is populated once the user has navigated to a particular table and has selected records. The template table/selectedRows.hbs will then be rendered into this outlet.

When first loaded Ember will render tables/index.hbs into the default outlet of the tables.hbs template. Once the user selects a table to view the table results will be rendered into the outlet instead.

tables/index.hbs
1
2
<h2>Get started.</h2>
<p>Please select a table on the side to get started.</p>

Pagination

When a table is selected the application will navigate to the first page of results, if more results are available then the Azure storage API will include what is known as a continuation token. The continuation token is made up of a partition key and a row key and must be included in the subsequent request in order to obtain the next set of results.

TablePageRoute.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
App.TablePageRoute = App.AuthenticatedRoute.extend({
  renderTemplate: function() {
    var selectedRowsController = this.controllerFor("selectedRows");
    this.render();

    this.render("table/selectedRows", {
      into: "tables",
      outlet: "selectedRowsOutlet",
      controller: selectedRowsController
    });
  },

  setupController: function(controller, model) {
    var self = this,
      pageId = model.id,
      tableId = this.modelFor("table").get("id"),
      credentials = this.get("credentials"),
      continuation = this._getPageContinuationToken(pageId - 1);

    if (!model.rows) {
      model = App.Page.find(tableId, credentials, pageId, continuation);

      model.then(function(value) {
        // success
        self._setPageContinuationToken(value.id, value.continuation);
        controller.set("model", value);
      }, function(error) {
        // failure
        throw error;
      });
    }

  },

  model: function(params) {
    var pageId = parseInt(params.page_id, 10) || 1;

    return App.Page.create({id: pageId});
  },

  _getPageContinuationToken: function(pageId) {
    var continuationToken = null,
      table = this.modelFor("table"),
      continuationTokens = table.get("continuationTokens");

    if (continuationTokens) {
      continuationToken =  continuationTokens[pageId];
    }

    return continuationToken;
  },

  _setPageContinuationToken: function(pageId, continuationToken) {
    var table = this.modelFor("table"),
      continuationTokens = table.get("continuationTokens");

    if (!continuationTokens) {
      continuationTokens = {};
      table.set("continuationTokens", continuationTokens);
    }

    continuationTokens[pageId] = continuationToken;
  }
});
  • renderTemplate - this renders not only the default template but also the table/selectedRows template.
  • setupController - this hook is called after the model hook, here we check if the model is fully loaded, if not we get the current continuation token and request the detailed results for the current page.
  • model - returns an instance of Page with only the id property set, the model will be fully loaded in setupController.
  • _getPageContinuationToken - gets the current Table and loads the continuation token for the current page of results.
  • _setPageContinuationToken - stores a continuation token for a given page on the Table model instance.
TablePageController.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
App.TablePageController = Ember.ObjectController.extend({
  toggleRowSelection: function(row) {
    var isSelected = row.get("isSelected") || false;

    row.set("isSelected", !isSelected);
  },

  columns: function() {
    var columns = [],
      excludedColumns = {
        id: true,
        PartitionKey: true,
        RowKey: true,
        isSelected: true,
        partitionKeyChanged: true,
        _: true
      };

    _.each(this.get("rows"), function(element) {
      for(var column in element) {
        if (!element.hasOwnProperty(column)) {
          break;
        }

        if (!excludedColumns[column] && _.contains(columns, column) !== true) {
          columns.push(column);
        }
      }
    });

    return columns;

  }.property("[email protected]"),

  columnHeaders: function() {
    var columnHeaders = _.clone(this.get("columns"));

    // blank header for "isSelected" column
    columnHeaders.unshift(" ");

    return columnHeaders;
  }.property("columns"),

  paginationLinks: function() {
    var links = [],
      currentPage = this.get("id"),
      startPage = (currentPage > 5) ? (currentPage - 5) : 1,
      endPage = currentPage;

    endPage += this.get("continuation") ? 2 : 1;

    // Create a list of App.Page objects
    // ...

    return links;
  }.property("id")
});
  • toggleRowSelection - toggles the isSelected property for a given row.
  • columns - returns a list of columns that will be displayed in the results table. A common set of properties are excluded from this list.
  • columnHeaders - returns a list of column headers that are displayed in the results table.
  • paginationLinks - a list Page instances that are used to render the pagination links.
table/page.hbs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<table class="table table-condensed">
  <thead>
    {{#each column in columnHeaders}}
      <th>
        <p>{{column}}</p>
      </th>
    {{/each}}
  </thead>
  <tbody>
  {{#each row in rows}}

    {{#if row.partitionKeyChanged}}
      <tr class="partition-key-row">
        <td></td>
        <td colspan="2">
          <h3>{{row.PartitionKey}}</h3>
        </td>
        <td>
          <p class="text-right">
            <small>PARTITION KEY</small>
          </p>
        </td>
      </tr>
    {{/if}}

    <tr
      {{action "toggleRowSelection" row on="click"}}
      {{bindAttr class=":master-row row.isSelected:master-row-selected"}}
    >
      <td>
        {{view Ember.Checkbox checkedBinding="row.isSelected" bubbles=false}}
      </td>
      {{#each column in ../columns}}
        <td>
          <p>
            {{tableCell ../row}}
          </p>
        </td>
      {{/each}}
    </tr>
  {{/each}}

  </tbody>
</table>

<div class="pagination">
  <ul>
    {{#each pageLink in paginationLinks}}
    <li {{bindAttr class="pageLink.isActive:active pageLink.disabled"}}>
        {{#unless pageLink.disabled}}
          {{#linkTo table.page pageLink}}
            {{safeString pageLink.displayText}}
          {{/linkTo}}
        {{/unless}}

        {{#if pageLink.disabled}}
          <span class="disabled">{{safeString pageLink.displayText}}</span>
        {{/if}}
    </li>
    {{/each}}
  </ul>
</div>

The template above uses a customised version of Ember.CheckBox and also a custom Handlebars helper tableCell.

In the template the user is allowed to toggle row selection by either checking the "isSelected" checkbox or by just clicking on the row itself. The problem with the standard Ember.CheckBox is that there is no way to stop the click event from bubbling up and subsequently triggering the toggleRowSelection action. The customised version below has an extra "bubbles" property which when set to false will disable click event propagation.

EmberCheckbox.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
Ember.Checkbox = Ember.View.extend({
  classNames: ["ember-checkbox"],

  tagName: "input",

  attributeBindings: ["type", "checked", "indeterminate", "disabled", "tabindex", "name"],

  type: "checkbox",
  checked: false,
  disabled: false,
  indeterminate: false,
  bubbles: true,

  init: function() {
    this._super();
    this.on("change", this, this._updateElementValue);
  },

  didInsertElement: function() {
    this._super();
    this.get("element").indeterminate = !!this.get("indeterminate");

    if (!this.get("bubbles")) {
      this.$().click(function(e) {
        e.stopPropagation();
      });
    }
  },

  _updateElementValue: function() {
    Ember.set(this, "checked", this.$().prop("checked"));
  }
});

The other custom item used by the template is a Handlebars helper which is used to specific values for each of the display columns within the row. If the value is a valid date then it is formatted using the moment.js library.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Ember.Handlebars.helper("tableCell", function(row, column) {
  var value = "",
    columnName = column.data.keywords.column;

  if (row[columnName]) {
    value = row[columnName];
  }

  if (moment(value).isValid()) {
    value = moment(value).calendar();
  }

  return value;
});

The final items to look at are the controller and template for selected items. As shown above the TablePageRoute renders this into the selectedRowsOutlet of the tables.hbs.

SelectedRowsController.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
App.SelectedRowsController = Ember.ObjectController.extend({
  tablePage: null,
  needs: "tablePage",
  tablePageBinding: "controllers.tablePage",
  currentIndex: 0,

  selectedRows: function() {
    var rows = this.get("tablePage").get("rows"),
      selectedRows = _.where(rows, {isSelected: true});

    if (selectedRows.length <= this.get("currentIndex") && selectedRows.length > 0) {
      this.set("currentIndex", selectedRows.length - 1);
    }

    return selectedRows;
  }.property("[email protected]"),

  hasSelectedRows: function() {
    return (this.get("selectedRows").length > 0);
  }.property("selectedRows"),

  moveNext: function() {
    var currentIndex = this.get("currentIndex");

    if (currentIndex < (this.get("selectedRows").length - 1)) {
      this.set("currentIndex", currentIndex + 1);
    }
  },

  movePrevious: function() {
    var currentIndex = this.get("currentIndex");

    if (currentIndex > 0) {
      this.set("currentIndex", currentIndex - 1);
    }
  },

  moveNextEnabled: function() {
    return (this.get("currentIndex") < (this.get("selectedRows").length) - 1);
  }.property("currentIndex", "selectedRows"),

  movePreviousEnabled: function() {
    return (this.get("currentIndex") > 0);
  }.property("currentIndex"),

  currentRow: function() {
    return this.get("selectedRows")[this.get("currentIndex")];
  }.property("selectedRows", "currentIndex"),

  currentDisplayIndex: function() {
    return this.get("currentIndex") + 1;
  }.property("currentIndex")

});
  • tablePage - we have a dependency on the table page controller, Ember will wire up a reference to this property.
  • needs - defines tablePage as a dependency.
  • tablePageBinding - sets the binding that will be used to wire tablePage.
  • currentIndex - the index of the record being displayed.
  • selectedRows - a list of all the rows that are currently selected on the table details page.
  • hasSelectedRows - true/false if there are any selected rows.
  • moveNext - displays the next selected record.
  • movePrevious - displays the previous selected record.
  • moveNextEnabled - true/false if there is another record to display.
  • movePreviousEnabled - true/false if there is a previous record to display.
  • currentRow - the current record.
  • currentDisplayIndex - the index of the current record as displayed to the user.
table/selectedRows.hbs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<p>&nbsp;</p>
<ul {{bindAttr class=":nav :nav-list hasSelectedRows:show:hide"}}>
    <li class="nav-header">
       <span>ROW DETAILS</span>
       <span>({{currentDisplayIndex}}/{{selectedRows.length}})</span>
       <span class="pull-right">
           <i {{action "movePrevious" on="click"}} {{bindAttr class=":icon-btn :icon :icon-chevron-left movePreviousEnabled:enabled:disabled"}}></i>
           <i {{action "moveNext" on="click"}} {{bindAttr class=":icon-btn :icon :icon-chevron-right moveNextEnabled:enabled:disabled"}}></i>
       </span>
    </li>
    <li>
       {{#with currentRow}}
        <small>
            <p>
              <strong>ID</strong>
              <br/>
              {{input type="text" value=_.id disabled=true}}
            </p>
            <p>
              <strong>Timestamp</strong>
              <br/>
              <span>{{Timestamp}}</span>
            </p>
            <p>
              <strong>Link</strong>
              <br/>
              {{input type="text" value=_.link disabled=true}}
            </p>
            <p>
              <strong>Updated</strong>
              <br/>
              <span>{{_.updated}}</span>
            </p>
            <p>
              <strong>ETag</strong>
              <br/>
              <span>{{_.etag}}</span>
            </p>
        </small>
        {{/with}}
    </li>
</ul>

Look ma, no unit tests!

So that's enough for this morning, it's not quite fully complete but getting there. Yes there aren't any unit tests, I suck. Also I haven't implemented the record deletion seen in Jeff's app and there is also a problem with URLs regarding pagination.

One of the core tenants of Ember is that you should be able to reload an app or share a link with someone and be able get back to exactly that point within the application. As they say this is one of the great things about the web and a major advantage we have over native applications.

Due to the way paging works on Azure tables it isn't possible to restore the application back to particular page based on the URL structure I have in place. I could always embed the continuation tokens within the URL or come up with a workaround but that's for another day.

Comments