How do you create a custom adapter for ember.js?

I'm planning on using ember.js, however my REST api doesn't exactly align with the packaged REST Adapter. I would like to "override" find and be able to put my own ajax in it. I dislike how an ember findAll retrieves all my documents with no options for pagination, so that along with other query parameters would be useful --which is why I want to write my own ajax. I've been unable to find any documentation on how I would go about doing this.


Solution 1:

For Ember Data

This is up to date as of Ember Data 1.0 beta 9.

Extend one of the Ember Data Adapters. To make it site wide:

App.ApplicationAdapter = DS.RESTAdapter.extend(....

To make it model specific:

App.FooAdapter = DS.RESTAdapter.extend(...

Then you will define the implementation you'd like to override. You always have the option to call this._super and revert to the base implementation. e.g.

App.NotesAdapter = DS.RESTAdapter.extend({
  find: function(store, type, id) {
    id = "foo" + id;
    return this._super(store, type, id);
  }
});

Or you can completely override the implementation:

App.NotesAdapter = DS.RESTAdapter.extend({
  find: function(store, type, id) {
    // Do your thing here
    return this.ajax(this.buildURL(type.typeKey, id), 'GET');
  },

  findAll: function(store, type, sinceToken) {
    // Do your thing here
    var query;

    if (sinceToken) {
      query = { since: sinceToken };
    }

    return this.ajax(this.buildURL(type.typeKey), 'GET', { data: query });
  },

  findQuery: function(store, type, query) {
    // Do your thing here
    return this.ajax(this.buildURL(type.typeKey), 'GET', { data: query });
  },

  findMany: function(store, type, ids, owner) {
    return this.ajax(this.buildURL(type.typeKey), 'GET', { data: { ids: ids } });
  },
   .....
});

To see the complete api you can override see: http://emberjs.com/api/data/classes/DS.RESTAdapter.html

Serializer

Often more important will be rolling your own serializer for massaging the data to fit your rest endpoint. Here's some useful information from the transition document https://github.com/emberjs/data/blob/master/TRANSITION.md .

The short version is that once an Ajax request has completed, the resulting payload is sent through the following hooks:

  1. The payload is sent to extractSingle if the original request was for a single record (like find/save) or extractArray if the original request was for an Array of records (like findAll/findQuery)
  2. The default behavior of those methods is to pull apart the top-level of the payload into multiple smaller records.
  3. Each of those smaller records is sent to normalize, which can do normalization a record at a time.
  4. Finally, specific types of records can be specially normalized.
    App.PostSerializer = DS.RESTSerializer.extend({
      extractSingle: function(store, type, payload, id) {
        // massage
        this._super(store, type, payload, id);
      },
      extractArray: function(store, type, payload) {
        // massage
        this._super(store, type, payload);
      },
      normalize: function(type, hash, property) {
        // massage
        this._super(type, hash, property);
      }
    });
  • use extractSingle and extractArray when the top-level of your payload is organized differently than Ember Data expects
  • use normalize to normalize sub-hashes if all sub-hashes in the payload can be normalized in the same way.
  • use normalizeHash to normalize specific sub-hashes.
  • make sure to call super if you override extractSingle, extractArray or normalize so the rest of the chain will get called.

Rolling your own

App.FooAdapter = Ember.Object.extend({
  find: function(id){
    return $.getJSON('http://www.foolandia.com/foooo/' + id);
  }
});

Then from your route, or wherever

App.FooRoute = Ember.Route.extend({
  model: function(){
    var adapter = App.FooAdapter.create();
    return adapter.find(1);
  }
});

Now personally I'd inject the adapter onto the routes just to make my life easier:

App.initializer({
    name: "fooAdapter",

    initialize: function (container, application) {
        application.register("my:manager", application.FooAdapter);
        application.inject("controller", "fooAdapter", "my:manager");
        application.inject("route", "fooAdapter", "my:manager");
    }
});

Then on the route you could be lazier and do:

App.FooRoute = Ember.Route.extend({
  model: function(){
    return this.fooAdapter.find(1);
  }
});

Example: http://emberjs.jsbin.com/OxIDiVU/676/edit

You can read more about Ember without Ember Data: Ember without Ember Data

Solution 2:

I had the same problem. I too wanted to use a slightly different format with my backend (cakePHP) and could not figure out how to do it. The previous answers are great but you might not need to redefine every method but simply change the format of the URL by overriding the buildURL in the RESTAdapter.

For example, I want to use cakePHP's extension and want my urls to looks like so, application wide:

  • /users.json (findAll)
  • /users/view/1.json (find)
  • /users/delete/1.json
  • /users/edit.json (POST)
  • /users/add.json (POST)

After much hair pulling and the realization that ember-data is essential I used the following code:

App.ApplicationAdapter = DS.RESTAdapter.extend({
  buildURL: function(type, id) {
    var url = '/' + this.pluralize(type.typeKey);

    if (id) {
        url += '/' + id;
    }

    url += '.json';

    return url;
  }
});

Ember's docs are good, but most of their examples use FIXTURE data. I wish they had a simple example how to write different types of adapters for different situations.

Solution 3:

For those who code adapter themselves, if you need to return a value from your adapter (for example, userId), you can either return json or promise. Here is example of returning promise:

App.RequestAdapter = Ember.Object.extend({
    newRequest: function (data) {
        return new Ember.RSVP.Promise(function (resolve, reject) {
            Ember.$.ajax({
                type: 'POST',  // method post
                url: '/Request/Create', //target url
                data: JSON.stringify(data), //the JSON.stringify converts data to JSON
                dataType: "json",
                contentType: "application/json; charset=utf-8",
                success: function (response) {
                    resolve(response);
                },
                error: function (reason) {
                    reject(reason);
                }
            });
        });
    }
});

//use this adapter in  your controller
var adapter = App.RequestAdapter.create();

adapter.newRequest(data).then(function (response) {   //newRequest is method of our adapter
    console.log(response.userId);  //specify response data
}, function(error){
    //handle error  
});

You can get more info about Ember promises here: https://hackhands.com/3-ways-ember-js-leverages-promises/ or here http://emberjs.com/api/classes/RSVP.Promise.html