Dojorama Part 3: Defining A RESTful Server-Side API And Integrating Dgrid

This article is part of the series Dojorama: Building a Dojo Single Page Application

Static pages are nice and everything but the application really comes to life only when we have some data to work with. In our case, the data will, at some point, be available through a RESTful API running on the same server where this JavaScript app is to be run. I'm saying "at some point", because "we" are only one developer and API implementation currently only exists on our lengthy todo list. We could, of course, switch our minds over into server-side-mode and start building an API with something like Node.js, Symfony or the Zend Framework. But we don't want to yet, let's just write up a first spec and stick with the client-side stuff for now:

An API Spec

List Releases

GET /api/releases
Range: items=$start-$end // optional
Optional Parameters
  • sort($order$field)
    • order: + or -
    • field: title or artist
Response
Status: 200 OK
Content-Range: $start-$end/$count

[
    {
        id: "123",
        title: "Menta Latte Remixe",
        artist: "Matias Aguayo",
        releaseDate: "2012-09-03",
        published: 1,
        info: "A groundbreaking cocktail of latin fueled songs"
    }
]

Get A Single Release

GET /api/releases/:id
Response
Status: 200 OK

{
    id: "123",
    title: "Menta Latte Remixe",
    artist: "Matias Aguayo",
    releaseDate: "2012-09-03",
    published: 1,
    info: "A groundbreaking cocktail of latin fueled songs"
}

Create A Release

POST /api/releases

{
    title: "Menta Latte Remixe", // required
    artist: "Matias Aguayo", // required
    releaseDate: "2012-09-03", // required
    published: 1, // optional, defaults to 0
    info: "A groundbreaking cocktail of latin fueled songs" // optional
}
Response
Status: 201 Created
Location: /api/releases/123

{
    id: "123",
    title: "Menta Latte Remixe",
    artist: "Matias Aguayo",
    releaseDate: "2012-09-03",
    published: 1,
    info: "A groundbreaking cocktail of latin fueled songs"
}

Edit A Release

PUT /api/releases/:id

{
    title: "Menta Latte Remixe",
    artist: "Matias Aguayo",
    releaseDate: "2012-09-03",
    published: 1,
    info: "A groundbreaking cocktail of latin fueled songs"
}
Response
Status: 200 OK
Location: /api/releases/123

{
    id: "123",
    title: "Menta Latte Remixe",
    artist: "Matias Aguayo",
    releaseDate: "2012-09-03",
    published: 1,
    info: "A groundbreaking cocktail of latin fueled songs"
}

Delete A Release

DELETE /api/releases/:id
Response
Status: 204 No Content

Error Responses

There are three different types of errors:

Not Found
404 Not Found
Invalid JSON
400 Bad Request
Invalid Fields
422 Unprocessable Entity

{
    message: 'Validation failed',
    errors: [
        {
            field: 'title',
            code: 'missing|invalid|exists'
        }
    ]
}

With this spec at hand, at least we know what to code against and we could probably just outsource the server-side programming to speed up overall development.

Mimicking The API

Since the API is not available yet, we need a way to mimic it. Let's start really easy by creating a file with some hardcoded JSON data.

data/releases.json

[
    {
        id: "123",
        title: "Menta Latte Remixe",
        artist: "Matias Aguayo",
        releaseDate: "2010-09-13",
        published: 1,
        info: "A groundbreaking cocktail of latin fueled songs"
    },
    {
        id: "456",
        title: "Looping State Of Mind Remixe",
        artist: "The Field",
        releaseDate: "2012-07-30",
        published: 1,
        info: "High-caliber remix collection featuring Junior Boys, Blondes and Mohn"
    }
]

Connecting To The Data

Once more, thanks to Dojo and dojo/store in particular, we don't have to manually write up all the connection stuff ourself. dojo/store is a uniform interface for the access and manipulation of stored data.

It is very well possible that we'll end up having many stores for our application and they might be accessed from many different components. For this reason we'll create a service module from which we can request the store centrally from one single place:

service/release-store.js

define([
    "require",
    "dojo/store/JsonRest"
], function (
    require,
    JsonRest
) {
    return new JsonRest({
        target: require.toUrl("/data/releases.json"),
        idProperty: "id"
    });
});

Integrating dgrid

Now is the time to integrate dgrid! Let's get the required vendor libs:

  • git submodule add git://github.com/SitePen/dgrid.git vendor/SitePen/dgrid
  • git submodule add git://github.com/kriszyp/put-selector.git vendor/kriszyp/put-selector
  • git submodule add git://github.com/kriszyp/xstyle.git vendor/kriszyp/xstyle

With dgrid and its dependencies in place, we can go about creating a widget holding the grid:

ui/release/widget/ReleaseGridWidget.js

define([
    "dojo/_base/declare",
    "mijit/_WidgetBase",
    "mijit/_TemplatedMixin",
    "dojomat/_StateAware",
    "dojo/_base/lang",
    "dgrid/OnDemandGrid",
    "dojo/store/Observable",
    "dojo/text!./templates/ReleaseGridWidget.html"
], function (
    declare,
    _WidgetBase,
    _TemplatedMixin,
    _StateAware,
    lang,
    OnDemandGrid,
    Observable,
    template
) {
    return declare([_WidgetBase, _TemplatedMixin, _StateAware], {
        router: null,
        store: null,
        templateString: template,
        gridWidget: null,

        constructor: function (params) {
            this.router = params.router;
            this.store = params.store;
        },

        postCreate: function () {
            this.inherited(arguments);
            this.store = Observable(this.store);
            this.gridWidget = this.buildGrid();
        },

        buildGrid: function () {
            var columns = {
                title: {
                    label: 'Title',
                    field: "title",
                    sortable: true
                },
                artist: {
                    label: 'Artist',
                    field: "artist",
                    sortable: true
                }
            };

            return new (declare([OnDemandGrid]))({
                store: this.store,
                columns: columns,
                queryOptions: { sort: [{ attribute: 'title', descending: false }] },
                loadingMessage: 'Loading...',
                noDataMessage: 'No Releases Available'
            }, this.gridNode);
        },

        setListeners: function () {
            // go to form
            this.gridWidget.on(".dgrid-column-task:click", lang.hitch(this, function (evt) {
                var cell = that.grid.cell(evt),
                    release = cell.row.data,
                    url = that.router.getRoute('releaseUpdate').assemble({ id: release.id })
                ;
                that.pushState(url);
            }));
        }
    });
});

ui/release/widget/templates/ReleaseGridWidget.html

<div>
    <div data-dojo-attach-point="gridNode"></div>
</div>

Putting It All Together

Our releases page needs some modifications: It has to request the store and initialize the grid widget:

ui/release/ReleaseIndexPage.js

define([
    "dojo/_base/declare",
    "mijit/_WidgetBase",
    "mijit/_TemplatedMixin",
    "dojomat/_AppAware",
    "dojo/query",
    "./widget/ReleaseGridWidget", // << updated
    "../../service/release-store", // << updated
    "../_global/widget/NavigationWidget",
    "dojo/text!./templates/ReleaseIndexPage.html",
    "dojo/text!./css/ReleaseIndexPage.css"
], function (
    declare,
    _WidgetBase,
    _TemplatedMixin,
    _AppAware,
    query,
    ReleaseGridWidget, // << updated
    releaseStore, // << updated
    NavigationWidget,
    template,
    css
) {
    return declare([_WidgetBase, _TemplatedMixin, _AppAware], {
        request: null,
        router: null,
        session: null,
        templateString: template,
        navigationWidget: null,
        gridWidget: null, // << updated

        constructor: function (params) {
            this.request = params.request;
            this.router = params.router;
            this.session = params.session;
        },

        postCreate: function () {
            this.inherited(arguments);
            this.setCss(css);
            this.setTitle('Releases');

            this.navigationWidget = new NavigationWidget({
                router: this.router
            }, this.navigationNode);

            this.gridWidget = new ReleaseGridWidget({ // << updated
                router: this.router,
                store: releaseStore
            }, this.gridNode);
        },

        startup: function () {
            this.inherited(arguments);
            this.navigationWidget.startup();
            this.gridWidget.startup(); // << updated
        }
    });
});

ui/release/templates/ReleaseIndexPage.html

<div>
    <div data-dojo-attach-point="navigationNode"></div>
    <h1>Releases</h1>
    <div data-dojo-attach-point="gridNode"></div>
</div>

tests/app/source/index.html

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>Dojorama</title>
        <script>
            document.write('<style media="all">#static { display: none; }</style>');
        </script>
    </head>
    <body>
        <div id="static">
            Some static or server-side generated content for search engines and clients with disabled JavaScript
        </div>

        <div id="app"></div>

        <script>
            var dojoConfig = {
                async: 1,
                cacheBust: 1,
                packages: [
                    { name: 'dojorama', location: '../../..' },
                    { name: 'mijit', location: '../../sirprize/mijit' },
                    { name: 'routed', location: '../../sirprize/routed' },
                    { name: 'dojomat', location: '../../sirprize/dojomat' },
                    { name: 'dgrid', location: '../../SitePen/dgrid' }, // << updated
                    { name: 'xstyle', location: '../../kriszyp/xstyle' }, // << updated
                    { name: 'put-selector', location: '../../kriszyp/put-selector' } // << updated
                ],
                'routing-map': {
                    pathPrefix: '/tests/app/source'
                }
            };
        </script>

        <script src="/vendor/dojo/dojo/dojo.js"></script>

        <script>
            require(['dojorama/App'], function(App) {
                new App({}, 'app');
            });
        </script>
    </body>
</html>

If everything goes well, our data should be listed in a grid. Back to the browser, refresh and yeah!

Conclusion

Ok ok, it's read-only but a lot has happened here. Imagine if we'd have to write something even slightly as functional as dgrid - it would probably keep us busy for a while. And as we'll see soon, this is just the tip of the iceberg. Let's not stop here, we want to be able to add our own releases, right? Input form, input validation, data model and data bindings. Here we come!