Skip to content

Commit

Permalink
Add adapter mixin which enables Ember Data to use fetch instead of jQ…
Browse files Browse the repository at this point in the history
…uery.ajax
  • Loading branch information
nlfurniss committed May 30, 2017
1 parent 994c0df commit 9f15853
Show file tree
Hide file tree
Showing 4 changed files with 245 additions and 1 deletion.
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,24 @@ export default Ember.Route.extend({
});
```

### Use with Ember Data
To have Ember Data utilize `fetch` instead of jQuery.ajax to make calls to your backend, extend your project's `application` adapter with the `adapter-fetch` mixin.

```js
// app/adapters/application.js
import DS from 'ember-data';
import AdapterFetch from 'ember-fetch/mixins/adapter-fetch';

export default RESTAdapter.extend(AdapterFetch, {
...
});
```

further docs: https://github.com/github/fetch

### Browser Support

* evergreen / IE9+ / Safari 6.1+ https://github.com/github/fetch#browser-support
* evergreen / IE10+ / Safari 6.1+ https://github.com/github/fetch#browser-support


### does this replace ic-ajax?
Expand Down
142 changes: 142 additions & 0 deletions addon/mixins/adapter-fetch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import Ember from 'ember';
import fetch from 'fetch';

const { assign, RSVP } = Ember;

/**
* Helper function that turns the data/body of a request into a query param string.
* @param {Object} queryParamsObject
* @returns {String}
*/
export function serialiazeQueryParams(queryParamsObject) {
return Object.keys(queryParamsObject).map((key) => {
return `${encodeURIComponent(key)}=${encodeURIComponent(queryParamsObject[key])}`;
}).join('&');
}

/**
* Helper function to create a plain object from the response's Headers.
* Consumed by the adapter's `handleResponse`.
* @param {Headers} headers
* @returns {Object}
*/
export function headersToObject(headers) {
let headersObject = {};
headers.forEach((value, key) => {
headersObject[key] = value;
});
return headersObject;
}
/**
* Helper function that translates the options passed to `jQuery.ajax` into a format that `fetch` expects.
* @param {Object} options
*/
export function mungOptionsForFetch(_options) {
const options = assign({
credentials: 'same-origin',
}, _options);

options.method = options.type;

// GET and HEAD requests can't have a `body`
if (options.data && Object.keys(options.data).length) {
if (options.method === 'GET' || options.method === 'HEAD') {
options.url += `?${serialiazeQueryParams(_options.data)}`;
} else {
options.body = options.data;
}
}

return options;
}

export default Ember.Mixin.create({
/**
* @param {String} url
* @param {String} type
* @param {Object} options
* @override
*/
ajax(url, type, options) {
const requestData = {
url,
method: type,
};

const hash = this.ajaxOptions(url, type, options);

return this._ajaxRequest(hash)
.catch((error, response, requestData) => {
throw this.ajaxError(error, response, requestData);
})
.then((response) => {
if (response.ok) {
const bodyPromise = response.json();
return this.ajaxSuccess(response, bodyPromise, requestData);
}
throw this.ajaxError(null, response, requestData);
});
},
/**
* Overrides the `_ajaxRequest` method to use `fetch` instead of jQuery.ajax
* @param {Object} options
* @override
*/
_ajaxRequest(options) {
const _options = mungOptionsForFetch(options);

return fetch(_options.url, _options);
},

/**
* @param {Object} response
* @param {Promise} bodyPromise
* @param {Object} requestData
* @override
*/
ajaxSuccess(response, bodyPromise, requestData) {
const headersObject = headersToObject(response.headers);

return bodyPromise.then((body) => {
const returnResponse = this.handleResponse(
response.status,
headersObject,
body,
requestData
);

if (returnResponse && returnResponse.isAdapterError) {
return RSVP.Promise.reject(returnResponse);
} else {
return returnResponse;
}
});
},

/**
* @param {Error} error
* @param {Object} response
* @param {Object} requestData
*/
ajaxError(error, response, requestData) {
let returnedError;

if (error instanceof Error) {
returnedError = error;
} else {
try {
const headersObject = headersToObject(response.headers);
returnedError = this.handleResponse(
response.status,
headersObject,
this.parseErrorResponse(response.statusText) || error,
requestData
);
} catch (e) {
throw e;
}
}

return returnedError;
}
});
1 change: 1 addition & 0 deletions app/mixins/adapter-fetch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from 'ember-fetch/mixins/adapter-fetch';
88 changes: 88 additions & 0 deletions tests/unit/mixins/adapter-fetch-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import Ember from 'ember';
import { module, test } from 'qunit';
import AdapterFetchMixin, {
mungOptionsForFetch,
headersToObject,
serialiazeQueryParams
} from 'ember-fetch/mixins/adapter-fetch';

module('Unit | Mixin | adapter-fetch', {
beforeEach() {
this.subject = Ember.Object.extend(AdapterFetchMixin).create();
}
});

test('mungOptionsForFetch transforms jQuery-style options into fetch compatible options', function(assert) {
assert.expect(2);

const jQueryGetOptions = {
url: 'https://emberjs.com',
type: 'GET',
headers: {
'x-fake-header': 13
},
data: {
a: 1,
b: 2
}
};

const fetchGetOptions = mungOptionsForFetch(jQueryGetOptions);

assert.deepEqual(fetchGetOptions, {
credentials: 'same-origin',
url: 'https://emberjs.com?a=1&b=2',
method: 'GET',
type: 'GET',
headers: {
'x-fake-header': 13
},
data: {
a: 1,
b: 2
}
}, 'GET call\'s options are correct');

const jqueryPostOptions = {
url: 'https://emberjs.com',
type: 'POST',
data: { a: 1 }
};

const fetchPostOptions = mungOptionsForFetch(jqueryPostOptions);

assert.deepEqual(fetchPostOptions, {
credentials: 'same-origin',
url: 'https://emberjs.com',
method: 'POST',
type: 'POST',
body: {
a: 1
},
data: {
a: 1
},
}, 'POST call\'s options are correct');
});

test('headersToObject turns an Headers instance into an object', function (assert) {
assert.expect(1);

const exampleHeaders = { etag: 'abc123' };
const headers = new Headers(exampleHeaders);
const headerObject = headersToObject(headers);

assert.deepEqual(headerObject, exampleHeaders);
});

test('serialiazeQueryParams turns an object into a query param string', function (assert) {
assert.expect(1);

const body = {
a: 1,
b: 2
};
const queryParamString = serialiazeQueryParams(body);

assert.equal(queryParamString, 'a=1&b=2');
});

0 comments on commit 9f15853

Please sign in to comment.