diff --git a/README.md b/README.md index 2492922e..b9d9e23c 100644 --- a/README.md +++ b/README.md @@ -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? diff --git a/addon/mixins/adapter-fetch.js b/addon/mixins/adapter-fetch.js new file mode 100644 index 00000000..4413f02a --- /dev/null +++ b/addon/mixins/adapter-fetch.js @@ -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; + } +}); diff --git a/app/mixins/adapter-fetch.js b/app/mixins/adapter-fetch.js new file mode 100644 index 00000000..c4e2c6e6 --- /dev/null +++ b/app/mixins/adapter-fetch.js @@ -0,0 +1 @@ +export { default } from 'ember-fetch/mixins/adapter-fetch'; diff --git a/tests/unit/mixins/adapter-fetch-test.js b/tests/unit/mixins/adapter-fetch-test.js new file mode 100644 index 00000000..610b90f4 --- /dev/null +++ b/tests/unit/mixins/adapter-fetch-test.js @@ -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'); +});