Skip to content

Commit

Permalink
Merge pull request #31 from nlfurniss/master
Browse files Browse the repository at this point in the history
Ember Data Adapter Mixin to use fetch
  • Loading branch information
stefanpenner authored Jun 15, 2017
2 parents c178bf8 + cf354ba commit 09e10ef
Show file tree
Hide file tree
Showing 6 changed files with 488 additions and 6 deletions.
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
205 changes: 205 additions & 0 deletions addon/mixins/adapter-fetch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
import Ember from 'ember';
import fetch from 'fetch';

const {
assign,
merge,
RSVP
} = Ember;

const RBRACKET = /\[\]$/;

/**
* Helper function that turns the data/body of a request into a query param string.
* This is directly copied from jQuery.param.
* @param {Object} queryParamsObject
* @returns {String}
*/
export function serialiazeQueryParams(queryParamsObject) {
var s = [];

function buildParams(prefix, obj) {
var i, len, key;

if (prefix) {
if (Array.isArray(obj)) {
for (i = 0, len = obj.length; i < len; i++) {
if (RBRACKET.test(prefix)) {
add(s, prefix, obj[i]);
} else {
buildParams(prefix + '[' + (typeof obj[i] === 'object' ? i : '') + ']', obj[i]);
}
}
} else if (obj && String(obj) === '[object Object]') {
for (key in obj) {
buildParams(prefix + '[' + key + ']', obj[key]);
}
} else {
add(s, prefix, obj);
}
} else if (Array.isArray(obj)) {
for (i = 0, len = obj.length; i < len; i++) {
add(s, obj[i].name, obj[i].value);
}
} else {
for (key in obj) {
buildParams(key, obj[key]);
}
}
return s;
}

return buildParams('', queryParamsObject).join('&').replace(/%20/g, '+');
}

/**
* Part of the `serialiazeQueryParams` helper function.
* @param {Array} s
* @param {String} k
* @param {String} v
*/
function add(s, k, v) {
v = typeof v === 'function' ? v() : v === null ? '' : v === undefined ? '' : v;
s[s.length] = `${encodeURIComponent(k)}=${encodeURIComponent(v)}`;
}

/**
* 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, adapter) {
// This allows this mixin to be backward compatible with Ember < 2.5.
const combineObjs = (assign || merge);
const options = combineObjs({
credentials: 'same-origin',
}, _options);

let adapterHeaders = adapter.get('headers');
if (adapterHeaders) {
// This double use of `combineObjs` is necessary because `merge` only accepts two arguments.
options.headers = combineObjs(combineObjs({}, options.headers || {}), adapterHeaders);
}
options.method = options.type;

// Mimics the default behavior in Ember Data's `ajaxOptions`
if (options.headers === undefined || !(options.headers['Content-Type'] || options.headers['content-type'])) {
options.headers = options.headers || {};
options.headers['Content-Type'] = 'application/json; charset=utf-8';
}

// 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) {
// We want to check that there is a body in the response, otherwise JSON.parse will throw an error.
// Instead, we'll let Ember Data handle an empty response.
const bodyPromise = response.headers.get('content-length') ? 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, this);
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';
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"ember-cli-release": "^0.2.9",
"ember-cli-shims": "^1.1.0",
"ember-cli-uglify": "^1.2.0",
"ember-data": "^2.13.1",
"ember-load-initializers": "^1.0.0",
"ember-resolver": "^4.0.0",
"ember-source": "~2.13.0",
Expand Down
Loading

0 comments on commit 09e10ef

Please sign in to comment.