diff --git a/.gitignore b/.gitignore index 90157b1b..f8d83f47 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /node_modules +/test/specs.js .DS_Store \ No newline at end of file diff --git a/README.md b/README.md index 17dbce50..c31aa17d 100644 --- a/README.md +++ b/README.md @@ -46,27 +46,27 @@ The http service can be used globally `Vue.http` or in a Vue instance `this.$htt ### Methods -* `get(url, [data], [success], [options])` -* `post(url, [data], [success], [options])` -* `put(url, [data], [success], [options])` -* `patch(url, [data], [success], [options])` -* `delete(url, [data], [success], [options])` -* `jsonp(url, [data], [success], [options])` +* `get(url, [data], [options])` +* `post(url, [data], [options])` +* `put(url, [data], [options])` +* `patch(url, [data], [options])` +* `delete(url, [data], [options])` +* `jsonp(url, [data], [options])` ### Options * **url** - `string` - URL to which the request is sent -* **data** - `Object|string` - Data to be sent as the request message data * **method** - `string` - HTTP method (e.g. GET, POST, ...) +* **data** - `Object|string` - Data to be sent as the request message data * **params** - `Object` - Parameters object to be appended as GET parameters * **headers** - `Object` - Headers object to be sent as HTTP request headers -* **success** - `function(data, status, request)` - Callback function to be called when the request finishes -* **error** - `function(data, status, request)` - Callback function to be called when the request fails -* **beforeSend** - `function(request, options)` - Callback function to modify the request object before it is sent +* **beforeSend** - `function(request)` - Callback function to modify the request object before it is sent * **emulateHTTP** - `boolean` - Send PUT, PATCH and DELETE requests with a HTTP POST and set the `X-HTTP-Method-Override` header * **emulateJSON** - `boolean` - Send request data as `application/x-www-form-urlencoded` content type * **xhr** - `Object` - Parameters object to be set on the native XHR object * **jsonp** - `string` - Callback function name in a JSONP request +* **timeout** - `number` - Request timeout in milliseconds (`0` means no timeout) + ### Example @@ -76,14 +76,24 @@ new Vue({ ready: function() { // GET request - this.$http.get('/someUrl', function (data, status, request) { + this.$http.get('/someUrl').then(function (response) { + + // get status + response.status; + + // get all headers + response.headers(); + + // get 'expires' header + response.headers('expires'); // set data on vm - this.$set('someData', data) + this.$set('someData', response.data) + + }, function (response) { - }).error(function (data, status, request) { // handle error - }) + }); } @@ -115,28 +125,70 @@ new Vue({ ready: function() { - var resource = this.$resource('someItem/:id'); + var resource = this.$resource('someItem{/id}'); // get item - resource.get({id: 1}, function (item, status, request) { - this.$set('item', item) - }) + resource.get({id: 1}).then(function (response) { + this.$set('item', response.item) + }); // save item - resource.save({id: 1}, {item: this.item}, function (data, status, request) { + resource.save({id: 1}, {item: this.item}).then(function (response) { // handle success - }).error(function (data, status, request) { + }, function (response) { // handle error - }) + }); // delete item - resource.delete({id: 1}, function (data, status, request) { + resource.delete({id: 1}).then(function (response) { // handle success - }).error(function (data, status, request) { + }, function (response) { // handle error - }) + }); } }) ``` + +## Interceptors + +Interceptors can be defined globally and are used for pre- and postprocessing of a request. + +```javascript +Vue.http.interceptors.push({ + + request: function (request) { + return request; + }, + + response: function (response) { + return response; + } + +}); +``` + +#### Interceptor Factory + +If Promises are needed inside of a Interceptor, a factory function can be used. + +```javascript +Vue.http.interceptors.push(function (Promise) { + return { + + request: function (request) { + if (reject) { + return Promise.reject(); + } + }, + + response: function (response) { + if (reject) { + return Promise.reject(); + } + } + + }; +}); +``` diff --git a/bower.json b/bower.json index 46e5cf6d..d731040e 100644 --- a/bower.json +++ b/bower.json @@ -2,7 +2,7 @@ "name": "vue-resource", "main": "dist/vue-resource.js", "description": "A web request service for Vue.js", - "version": "0.1.17", + "version": "0.5.0", "homepage": "https://github.com/vuejs/vue-resource", "license": "MIT", "ignore": [ diff --git a/dist/vue-resource.js b/dist/vue-resource.js index 80f760bf..edb7d85b 100644 --- a/dist/vue-resource.js +++ b/dist/vue-resource.js @@ -1,5 +1,5 @@ /** - * vue-resource v0.1.17 + * vue-resource v0.5.0 * https://github.com/vuejs/vue-resource * Released under the MIT License. */ @@ -69,8 +69,9 @@ return /******/ (function(modules) { // webpackBootstrap var _ = __webpack_require__(1)(Vue); Vue.url = __webpack_require__(2)(_); - Vue.http = __webpack_require__(3)(_); - Vue.resource = __webpack_require__(7)(_); + Vue.http = __webpack_require__(4)(_); + Vue.resource = __webpack_require__(18)(_); + Vue.promise = __webpack_require__(5)(_); Object.defineProperties(Vue.prototype, { @@ -101,6 +102,7 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = install; + /***/ }, /* 1 */ /***/ function(module, exports) { @@ -111,7 +113,27 @@ return /******/ (function(modules) { // webpackBootstrap module.exports = function (Vue) { - var _ = Vue.util.extend({}, Vue.util); + var _ = Vue.util.extend({}, Vue.util), config = Vue.config, console = window.console; + + _.warn = function (msg) { + if (console && Vue.util.warn && (!config.silent || config.debug)) { + console.warn('[VueResource warn]: ' + msg); + } + }; + + _.error = function (msg) { + if (console) { + console.error(msg); + } + }; + + _.trim = function (str) { + return str.replace(/^\s*|\s*$/g, ''); + }; + + _.toLower = function (str) { + return str ? str.toLowerCase() : ''; + }; _.isString = function (value) { return typeof value === 'string'; @@ -189,20 +211,22 @@ return /******/ (function(modules) { // webpackBootstrap /***/ }, /* 2 */ -/***/ function(module, exports) { +/***/ function(module, exports, __webpack_require__) { /** * Service for URL templating. */ - var ie = document.documentMode; - var el = document.createElement('a'); + var UrlTemplate = __webpack_require__(3); module.exports = function (_) { + var ie = document.documentMode; + var el = document.createElement('a'); + function Url(url, params) { - var urlParams = {}, queryParams = {}, options = url, query; + var urlParams = Object.keys(Url.options.params), queryParams = {}, options = url, query; if (!_.isPlainObject(options)) { options = {url: url, params: params}; @@ -212,10 +236,14 @@ return /******/ (function(modules) { // webpackBootstrap Url.options, this.options, options ); - url = options.url.replace(/(\/?):([a-z]\w*)/gi, function (match, slash, name) { + url = UrlTemplate.expand(options.url, options.params, urlParams); + + url = url.replace(/(\/?):([a-z]\w*)/gi, function (match, slash, name) { + + _.warn('The `:' + name + '` parameter syntax has been deprecated. Use the `{' + name + '}` syntax instead.'); if (options.params[name]) { - urlParams[name] = true; + urlParams.push(name); return slash + encodeUriSegment(options.params[name]); } @@ -227,7 +255,7 @@ return /******/ (function(modules) { // webpackBootstrap } _.each(options.params, function (value, key) { - if (!urlParams[key]) { + if (urlParams.indexOf(key) === -1) { queryParams[key] = value; } }); @@ -352,143 +380,242 @@ return /******/ (function(modules) { // webpackBootstrap /***/ }, /* 3 */ -/***/ function(module, exports, __webpack_require__) { +/***/ function(module, exports) { /** - * Service for sending network requests. + * URL Template v2.0.6 (https://github.com/bramstein/url-template) */ - var xhr = __webpack_require__(4); - var jsonp = __webpack_require__(6); - var Promise = __webpack_require__(5); + exports.expand = function (url, params, variables) { - module.exports = function (_) { + var tmpl = this.parse(url), expanded = tmpl.expand(params); - var originUrl = _.url.parse(location.href); - var jsonType = {'Content-Type': 'application/json;charset=utf-8'}; + if (variables) { + variables.push.apply(variables, tmpl.vars); + } - function Http(url, options) { + return expanded; + }; - var promise; + exports.parse = function (template) { - if (_.isPlainObject(url)) { - options = url; - url = ''; - } + var operators = ['+', '#', '.', '/', ';', '?', '&'], variables = []; - options = _.extend({url: url}, options); - options = _.extend(true, {}, - Http.options, this.options, options - ); + return { + vars: variables, + expand: function (context) { + return template.replace(/\{([^\{\}]+)\}|([^\{\}]+)/g, function (_, expression, literal) { + if (expression) { - if (options.crossOrigin === null) { - options.crossOrigin = crossOrigin(options.url); - } + var operator = null, values = []; - options.method = options.method.toUpperCase(); - options.headers = _.extend({}, Http.headers.common, - !options.crossOrigin ? Http.headers.custom : {}, - Http.headers[options.method.toLowerCase()], - options.headers - ); + if (operators.indexOf(expression.charAt(0)) !== -1) { + operator = expression.charAt(0); + expression = expression.substr(1); + } - if (_.isPlainObject(options.data) && /^(GET|JSONP)$/i.test(options.method)) { - _.extend(options.params, options.data); - delete options.data; - } + expression.split(/,/g).forEach(function (variable) { + var tmp = /([^:\*]*)(?::(\d+)|(\*))?/.exec(variable); + values.push.apply(values, exports.getValues(context, operator, tmp[1], tmp[2] || tmp[3])); + variables.push(tmp[1]); + }); - if (options.emulateHTTP && !options.crossOrigin && /^(PUT|PATCH|DELETE)$/i.test(options.method)) { - options.headers['X-HTTP-Method-Override'] = options.method; - options.method = 'POST'; - } + if (operator && operator !== '+') { - if (options.emulateJSON && _.isPlainObject(options.data)) { - options.headers['Content-Type'] = 'application/x-www-form-urlencoded'; - options.data = _.url.params(options.data); - } + var separator = ','; - if (_.isObject(options.data) && /FormData/i.test(options.data.toString())) { - delete options.headers['Content-Type']; - } + if (operator === '?') { + separator = '&'; + } else if (operator !== '#') { + separator = operator; + } - if (_.isPlainObject(options.data)) { - options.data = JSON.stringify(options.data); + return (values.length !== 0 ? operator : '') + values.join(separator); + } else { + return values.join(','); + } + + } else { + return exports.encodeReserved(literal); + } + }); } + }; + }; - promise = (options.method == 'JSONP' ? jsonp : xhr).call(this.vm, _, options); - promise = extendPromise(promise.then(transformResponse, transformResponse), this.vm); + exports.getValues = function (context, operator, key, modifier) { - if (options.success) { - promise = promise.success(options.success); - } + var value = context[key], result = []; - if (options.error) { - promise = promise.error(options.error); - } + if (this.isDefined(value) && value !== '') { + if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { + value = value.toString(); - return promise; + if (modifier && modifier !== '*') { + value = value.substring(0, parseInt(modifier, 10)); + } + + result.push(this.encodeValue(operator, value, this.isKeyOperator(operator) ? key : null)); + } else { + if (modifier === '*') { + if (Array.isArray(value)) { + value.filter(this.isDefined).forEach(function (value) { + result.push(this.encodeValue(operator, value, this.isKeyOperator(operator) ? key : null)); + }, this); + } else { + Object.keys(value).forEach(function (k) { + if (this.isDefined(value[k])) { + result.push(this.encodeValue(operator, value[k], k)); + } + }, this); + } + } else { + var tmp = []; + + if (Array.isArray(value)) { + value.filter(this.isDefined).forEach(function (value) { + tmp.push(this.encodeValue(operator, value)); + }, this); + } else { + Object.keys(value).forEach(function (k) { + if (this.isDefined(value[k])) { + tmp.push(encodeURIComponent(k)); + tmp.push(this.encodeValue(operator, value[k].toString())); + } + }, this); + } + + if (this.isKeyOperator(operator)) { + result.push(encodeURIComponent(key) + '=' + tmp.join(',')); + } else if (tmp.length !== 0) { + result.push(tmp.join(',')); + } + } + } + } else { + if (operator === ';') { + result.push(encodeURIComponent(key)); + } else if (value === '' && (operator === '&' || operator === '?')) { + result.push(encodeURIComponent(key) + '='); + } else if (value === '') { + result.push(''); + } } - function extendPromise(promise, vm) { + return result; + }; - promise.success = function (fn) { + exports.isDefined = function (value) { + return value !== undefined && value !== null; + }; - return extendPromise(promise.then(function (response) { - return fn.call(vm, response.data, response.status, response) || response; - }), vm); + exports.isKeyOperator = function (operator) { + return operator === ';' || operator === '&' || operator === '?'; + }; - }; + exports.encodeValue = function (operator, value, key) { - promise.error = function (fn) { + value = (operator === '+' || operator === '#') ? this.encodeReserved(value) : encodeURIComponent(value); - return extendPromise(promise.then(undefined, function (response) { - return fn.call(vm, response.data, response.status, response) || response; - }), vm); + if (key) { + return encodeURIComponent(key) + '=' + value; + } else { + return value; + } + }; - }; + exports.encodeReserved = function (str) { + return str.split(/(%[0-9A-Fa-f]{2})/g).map(function (part) { + if (!/%[0-9A-Fa-f]/.test(part)) { + part = encodeURI(part); + } + return part; + }).join(''); + }; - promise.always = function (fn) { - var cb = function (response) { - return fn.call(vm, response.data, response.status, response) || response; - }; +/***/ }, +/* 4 */ +/***/ function(module, exports, __webpack_require__) { - return extendPromise(promise.then(cb, cb), vm); - }; + /** + * Service for sending network requests. + */ - return promise; - } + module.exports = function (_) { - function transformResponse(response) { + var Promise = __webpack_require__(5)(_); + var interceptor = __webpack_require__(7)(_); + var defaultClient = __webpack_require__(8)(_); + var jsonType = {'Content-Type': 'application/json'}; - try { - response.data = JSON.parse(response.responseText); - } catch (e) { - response.data = response.responseText; + function Http(url, options) { + + var client = defaultClient, request, promise; + + if (_.isPlainObject(url)) { + options = url; + url = ''; } - return response.ok ? response : Promise.reject(response); - } + request = _.extend({url: url}, options); + request = _.extend(true, {}, + Http.options, this.options, request + ); - function crossOrigin(url) { + Http.interceptors.forEach(function (i) { + client = interceptor(i, this.vm)(client); + }, this); - var requestUrl = _.url.parse(url); + promise = client(request).bind(this.vm).then(function (response) { - return (requestUrl.protocol !== originUrl.protocol || requestUrl.host !== originUrl.host); + response.ok = response.status >= 200 && response.status < 300; + return response.ok ? response : Promise.reject(response); + + }, function (response) { + + if (response instanceof Error) { + _.error(response); + } + + return Promise.reject(response); + }); + + if (request.success) { + promise.success(request.success); + } + + if (request.error) { + promise.error(request.error); + } + + return promise; } Http.options = { method: 'get', - params: {}, data: '', + params: {}, + headers: {}, xhr: null, jsonp: 'callback', beforeSend: null, crossOrigin: null, emulateHTTP: false, - emulateJSON: false + emulateJSON: false, + timeout: 0 }; + Http.interceptors = [ + __webpack_require__(10)(_), + __webpack_require__(11)(_), + __webpack_require__(12)(_), + __webpack_require__(14)(_), + __webpack_require__(15)(_), + __webpack_require__(16)(_), + __webpack_require__(17)(_) + ]; + Http.headers = { put: jsonType, post: jsonType, @@ -508,6 +635,11 @@ return /******/ (function(modules) { // webpackBootstrap data = undefined; } + if (_.isObject(success)) { + options = success; + success = undefined; + } + return this(url, _.extend({method: method, data: data, success: success}, options)); }; }); @@ -517,336 +649,784 @@ return /******/ (function(modules) { // webpackBootstrap /***/ }, -/* 4 */ +/* 5 */ /***/ function(module, exports, __webpack_require__) { /** - * XMLHttp request. + * Promise adapter. */ - var Promise = __webpack_require__(5); - var XDomain = window.XDomainRequest; + module.exports = function (_) { - module.exports = function (_, options) { + var Promise = window.Promise || __webpack_require__(6)(_); - var request = new XMLHttpRequest(), promise; + var Adapter = function (executor) { - if (XDomain && options.crossOrigin) { - request = new XDomainRequest(); options.headers = {}; - } + if (executor instanceof Promise) { + this.promise = executor; + } else { + this.promise = new Promise(executor); + } - if (_.isPlainObject(options.xhr)) { - _.extend(request, options.xhr); - } + this.context = undefined; + }; - if (_.isFunction(options.beforeSend)) { - options.beforeSend.call(this, request, options); - } + Adapter.all = function (iterable) { + return new Adapter(Promise.all(iterable)); + }; - promise = new Promise(function (resolve, reject) { + Adapter.resolve = function (value) { + return new Adapter(Promise.resolve(value)); + }; - request.open(options.method, _.url(options), true); + Adapter.reject = function (reason) { + return new Adapter(Promise.reject(reason)); + }; - _.each(options.headers, function (value, header) { - request.setRequestHeader(header, value); - }); + Adapter.race = function (iterable) { + return new Adapter(Promise.race(iterable)); + }; + + var p = Adapter.prototype; + + p.bind = function (context) { + this.context = context; + return this; + }; + + p.then = function (fulfilled, rejected) { + + if (fulfilled && fulfilled.bind && this.context) { + fulfilled = fulfilled.bind(this.context); + } + + if (rejected && rejected.bind && this.context) { + rejected = rejected.bind(this.context); + } + + this.promise = this.promise.then(fulfilled, rejected); + + return this; + }; + + p.catch = function (rejected) { - var handler = function (event) { + if (rejected && rejected.bind && this.context) { + rejected = rejected.bind(this.context); + } + + this.promise = this.promise.catch(rejected); + + return this; + }; - request.ok = event.type === 'load'; + p.finally = function (callback) { - if (request.ok && request.status) { - request.ok = request.status >= 200 && request.status < 300; + return this.then(function (value) { + callback.call(this); + return value; + }, function (reason) { + callback.call(this); + return Promise.reject(reason); } + ); + }; - (request.ok ? resolve : reject)(request); - }; + p.success = function (callback) { - request.onload = handler; - request.onabort = handler; - request.onerror = handler; + _.warn('The `success` method has been deprecated. Use the `then` method instead.'); - request.send(options.data); - }); + return this.then(function (response) { + return callback.call(this, response.data, response.status, response) || response; + }); + }; + + p.error = function (callback) { + + _.warn('The `error` method has been deprecated. Use the `catch` method instead.'); + + return this.catch(function (response) { + return callback.call(this, response.data, response.status, response) || response; + }); + }; - return promise; + p.always = function (callback) { + + _.warn('The `always` method has been deprecated. Use the `finally` method instead.'); + + var cb = function (response) { + return callback.call(this, response.data, response.status, response) || response; + }; + + return this.then(cb, cb); + }; + + return Adapter; }; /***/ }, -/* 5 */ +/* 6 */ /***/ function(module, exports) { /** - * Promises/A+ polyfill v1.1.0 (https://github.com/bramstein/promis) + * Promises/A+ polyfill v1.1.4 (https://github.com/bramstein/promis) */ - var RESOLVED = 0; - var REJECTED = 1; - var PENDING = 2; + module.exports = function (_) { - function Promise(executor) { + var RESOLVED = 0; + var REJECTED = 1; + var PENDING = 2; - this.state = PENDING; - this.value = undefined; - this.deferred = []; + function Promise(executor) { - var promise = this; + this.state = PENDING; + this.value = undefined; + this.deferred = []; - try { - executor(function (x) { - promise.resolve(x); - }, function (r) { - promise.reject(r); - }); - } catch (e) { - promise.reject(e); + var promise = this; + + try { + executor(function (x) { + promise.resolve(x); + }, function (r) { + promise.reject(r); + }); + } catch (e) { + promise.reject(e); + } } - } - Promise.reject = function (r) { - return new Promise(function (resolve, reject) { - reject(r); - }); - }; + Promise.reject = function (r) { + return new Promise(function (resolve, reject) { + reject(r); + }); + }; - Promise.resolve = function (x) { - return new Promise(function (resolve, reject) { - resolve(x); - }); - }; + Promise.resolve = function (x) { + return new Promise(function (resolve, reject) { + resolve(x); + }); + }; - Promise.all = function all(iterable) { - return new Promise(function (resolve, reject) { - var count = 0, - result = []; + Promise.all = function all(iterable) { + return new Promise(function (resolve, reject) { + var count = 0, result = []; - if (iterable.length === 0) { - resolve(result); - } + if (iterable.length === 0) { + resolve(result); + } - function resolver(i) { - return function (x) { - result[i] = x; - count += 1; + function resolver(i) { + return function (x) { + result[i] = x; + count += 1; - if (count === iterable.length) { - resolve(result); + if (count === iterable.length) { + resolve(result); + } + }; + } + + for (var i = 0; i < iterable.length; i += 1) { + Promise.resolve(iterable[i]).then(resolver(i), reject); + } + }); + }; + + Promise.race = function race(iterable) { + return new Promise(function (resolve, reject) { + for (var i = 0; i < iterable.length; i += 1) { + Promise.resolve(iterable[i]).then(resolve, reject); + } + }); + }; + + var p = Promise.prototype; + + p.resolve = function resolve(x) { + var promise = this; + + if (promise.state === PENDING) { + if (x === promise) { + throw new TypeError('Promise settled with itself.'); + } + + var called = false; + + try { + var then = x && x['then']; + + if (x !== null && typeof x === 'object' && typeof then === 'function') { + then.call(x, function (x) { + if (!called) { + promise.resolve(x); + } + called = true; + + }, function (r) { + if (!called) { + promise.reject(r); + } + called = true; + }); + return; } - }; - } + } catch (e) { + if (!called) { + promise.reject(e); + } + return; + } - for (var i = 0; i < iterable.length; i += 1) { - iterable[i].then(resolver(i), reject); + promise.state = RESOLVED; + promise.value = x; + promise.notify(); } - }); - }; + }; - Promise.race = function race(iterable) { - return new Promise(function (resolve, reject) { - for (var i = 0; i < iterable.length; i += 1) { - iterable[i].then(resolve, reject); + p.reject = function reject(reason) { + var promise = this; + + if (promise.state === PENDING) { + if (reason === promise) { + throw new TypeError('Promise settled with itself.'); + } + + promise.state = REJECTED; + promise.value = reason; + promise.notify(); } - }); + }; + + p.notify = function notify() { + var promise = this; + + _.nextTick(function () { + if (promise.state !== PENDING) { + while (promise.deferred.length) { + var deferred = promise.deferred.shift(), + onResolved = deferred[0], + onRejected = deferred[1], + resolve = deferred[2], + reject = deferred[3]; + + try { + if (promise.state === RESOLVED) { + if (typeof onResolved === 'function') { + resolve(onResolved.call(undefined, promise.value)); + } else { + resolve(promise.value); + } + } else if (promise.state === REJECTED) { + if (typeof onRejected === 'function') { + resolve(onRejected.call(undefined, promise.value)); + } else { + reject(promise.value); + } + } + } catch (e) { + reject(e); + } + } + } + }); + }; + + p.then = function then(onResolved, onRejected) { + var promise = this; + + return new Promise(function (resolve, reject) { + promise.deferred.push([onResolved, onRejected, resolve, reject]); + promise.notify(); + }); + }; + + p.catch = function (onRejected) { + return this.then(undefined, onRejected); + }; + + return Promise; }; - var p = Promise.prototype; - p.resolve = function resolve(x) { - var promise = this; +/***/ }, +/* 7 */ +/***/ function(module, exports, __webpack_require__) { - if (promise.state === PENDING) { - if (x === promise) { - throw new TypeError('Promise settled with itself.'); - } + /** + * Interceptor factory. + */ - var called = false; + module.exports = function (_) { - try { - var then = x && x['then']; + var Promise = __webpack_require__(5)(_); - if (x !== null && typeof x === 'object' && typeof then === 'function') { - then.call(x, function (x) { - if (!called) { - promise.resolve(x); - } - called = true; + return function (handler, vm) { + return function (client) { - }, function (r) { - if (!called) { - promise.reject(r); - } - called = true; - }); - return; - } - } catch (e) { - if (!called) { - promise.reject(e); + if (_.isFunction(handler)) { + handler = handler.call(vm, Promise); } - return; + + return function (request) { + + if (_.isFunction(handler.request)) { + request = handler.request.call(vm, request); + } + + return when(request, function (request) { + return when(client(request), function (response) { + + if (_.isFunction(handler.response)) { + response = handler.response.call(vm, response); + } + + return response; + }); + }); + }; + }; + }; + + function when(value, fulfilled, rejected) { + + var promise = Promise.resolve(value); + + if (arguments.length < 2) { + return promise; } - promise.state = RESOLVED; - promise.value = x; - promise.notify(); + + return promise.then(fulfilled, rejected); } + + }; + + +/***/ }, +/* 8 */ +/***/ function(module, exports, __webpack_require__) { + + /** + * Default client. + */ + + module.exports = function (_) { + + var xhrClient = __webpack_require__(9)(_); + + return function (request) { + return (request.client || xhrClient)(request); + }; + }; - p.reject = function reject(reason) { - var promise = this; - if (promise.state === PENDING) { - if (reason === promise) { - throw new TypeError('Promise settled with itself.'); +/***/ }, +/* 9 */ +/***/ function(module, exports, __webpack_require__) { + + /** + * XMLHttp client. + */ + + module.exports = function (_) { + + var Promise = __webpack_require__(5)(_); + + return function (request) { + return new Promise(function (resolve) { + + var xhr = new XMLHttpRequest(), response = {request: request}, handler; + + request.cancel = function () { + xhr.abort(); + }; + + xhr.open(request.method, _.url(request), true); + + if (_.isPlainObject(request.xhr)) { + _.extend(xhr, request.xhr); + } + + _.each(request.headers || {}, function (value, header) { + xhr.setRequestHeader(header, value); + }); + + handler = function (event) { + + response.data = xhr.responseText; + response.status = xhr.status; + response.statusText = xhr.statusText; + response.headers = getHeaders(xhr); + + resolve(response); + }; + + xhr.onload = handler; + xhr.onabort = handler; + xhr.onerror = handler; + + xhr.send(request.data); + }); + }; + + function getHeaders(xhr) { + + var headers; + + if (!headers) { + headers = parseHeaders(xhr.getAllResponseHeaders()); } - promise.state = REJECTED; - promise.value = reason; - promise.notify(); + return function (name) { + + if (name) { + return headers[_.toLower(name)]; + } + + return headers; + }; } - }; - p.notify = function notify() { - var promise = this; - - async(function () { - if (promise.state !== PENDING) { - while (promise.deferred.length) { - var deferred = promise.deferred.shift(), - onResolved = deferred[0], - onRejected = deferred[1], - resolve = deferred[2], - reject = deferred[3]; - - try { - if (promise.state === RESOLVED) { - if (typeof onResolved === 'function') { - resolve(onResolved.call(undefined, promise.value)); - } else { - resolve(promise.value); - } - } else if (promise.state === REJECTED) { - if (typeof onRejected === 'function') { - resolve(onRejected.call(undefined, promise.value)); - } else { - reject(promise.value); - } + function parseHeaders(str) { + + var headers = {}, value, name, i; + + if (_.isString(str)) { + _.each(str.split('\n'), function (row) { + + i = row.indexOf(':'); + name = _.trim(_.toLower(row.slice(0, i))); + value = _.trim(row.slice(i + 1)); + + if (headers[name]) { + + if (_.isArray(headers[name])) { + headers[name].push(value); + } else { + headers[name] = [headers[name], value]; } - } catch (e) { - reject(e); + + } else { + + headers[name] = value; } - } + + }); } - }); + + return headers; + } + }; - p.catch = function (onRejected) { - return this.then(undefined, onRejected); + +/***/ }, +/* 10 */ +/***/ function(module, exports) { + + /** + * Before Interceptor. + */ + + module.exports = function (_) { + + return { + + request: function (request) { + + if (_.isFunction(request.beforeSend)) { + request.beforeSend.call(this, request); + } + + return request; + } + + }; + }; - p.then = function then(onResolved, onRejected) { - var promise = this; - return new Promise(function (resolve, reject) { - promise.deferred.push([onResolved, onRejected, resolve, reject]); - promise.notify(); - }); +/***/ }, +/* 11 */ +/***/ function(module, exports) { + + /** + * Timeout Interceptor. + */ + + module.exports = function (_) { + + return function () { + + var timeout; + + return { + + request: function (request) { + + if (request.timeout) { + timeout = setTimeout(function () { + request.cancel(); + }, request.timeout); + } + + return request; + }, + + response: function (response) { + + clearTimeout(timeout); + + return response; + } + + }; + }; + }; - var queue = []; - var async = function (callback) { - queue.push(callback); - if (queue.length === 1) { - async.async(); - } +/***/ }, +/* 12 */ +/***/ function(module, exports, __webpack_require__) { + + /** + * JSONP Interceptor. + */ + + module.exports = function (_) { + + var jsonpClient = __webpack_require__(13)(_); + + return { + + request: function (request) { + + if (request.method == 'JSONP') { + request.client = jsonpClient; + } + + return request; + } + + }; + }; - async.run = function () { - while (queue.length) { - queue[0](); - queue.shift(); - } + +/***/ }, +/* 13 */ +/***/ function(module, exports, __webpack_require__) { + + /** + * JSONP client. + */ + + module.exports = function (_) { + + var Promise = __webpack_require__(5)(_); + + return function (request) { + return new Promise(function (resolve) { + + var callback = '_jsonp' + Math.random().toString(36).substr(2), response = {request: request, data: null}, handler, script; + + request.params[request.jsonp] = callback; + request.cancel = function () { + handler({type: 'cancel'}); + }; + + script = document.createElement('script'); + script.src = _.url(request); + script.type = 'text/javascript'; + script.async = true; + + window[callback] = function (data) { + response.data = data; + }; + + handler = function (event) { + + if (event.type === 'load' && response.data !== null) { + response.status = 200; + } else if (event.type === 'error') { + response.status = 404; + } else { + response.status = 0; + } + + resolve(response); + + delete window[callback]; + document.body.removeChild(script); + }; + + script.onload = handler; + script.onerror = handler; + + document.body.appendChild(script); + }); + }; + }; - if (window.MutationObserver) { - var el = document.createElement('div'); - var mo = new MutationObserver(async.run); - mo.observe(el, { - attributes: true - }); +/***/ }, +/* 14 */ +/***/ function(module, exports) { + + /** + * HTTP method override Interceptor. + */ + + module.exports = function (_) { + + return { + + request: function (request) { + + if (request.emulateHTTP && /^(PUT|PATCH|DELETE)$/i.test(request.method)) { + request.headers['X-HTTP-Method-Override'] = request.method; + request.method = 'POST'; + } + + return request; + } - async.async = function () { - el.setAttribute("x", 0); }; - } else { - async.async = function () { - setTimeout(async.run); + + }; + + +/***/ }, +/* 15 */ +/***/ function(module, exports) { + + /** + * Mime Interceptor. + */ + + module.exports = function (_) { + + return { + + request: function (request) { + + if (request.emulateJSON && _.isPlainObject(request.data)) { + request.headers['Content-Type'] = 'application/x-www-form-urlencoded'; + request.data = _.url.params(request.data); + } + + if (_.isObject(request.data) && /FormData/i.test(request.data.toString())) { + delete request.headers['Content-Type']; + } + + if (_.isPlainObject(request.data)) { + request.data = JSON.stringify(request.data); + } + + return request; + }, + + response: function (response) { + + try { + response.data = JSON.parse(response.data); + } catch (e) {} + + return response; + } + }; - } - module.exports = window.Promise || Promise; + }; /***/ }, -/* 6 */ -/***/ function(module, exports, __webpack_require__) { +/* 16 */ +/***/ function(module, exports) { /** - * JSONP request. + * Header Interceptor. */ - var Promise = __webpack_require__(5); + module.exports = function (_) { - module.exports = function (_, options) { + return { - var callback = '_jsonp' + Math.random().toString(36).substr(2), response = {}, script, body; + request: function (request) { - options.params[options.jsonp] = callback; + request.method = request.method.toUpperCase(); + request.headers = _.extend({}, _.http.headers.common, + !request.crossOrigin ? _.http.headers.custom : {}, + _.http.headers[request.method.toLowerCase()], + request.headers + ); - if (_.isFunction(options.beforeSend)) { - options.beforeSend.call(this, {}, options); - } + if (_.isPlainObject(request.data) && /^(GET|JSONP)$/i.test(request.method)) { + _.extend(request.params, request.data); + delete request.data; + } - return new Promise(function (resolve, reject) { + return request; + } - script = document.createElement('script'); - script.src = _.url(options); - script.type = 'text/javascript'; - script.async = true; + }; + + }; - window[callback] = function (data) { - body = data; - }; - var handler = function (event) { +/***/ }, +/* 17 */ +/***/ function(module, exports, __webpack_require__) { + + /** + * CORS Interceptor. + */ + + module.exports = function (_) { + + var originUrl = _.url.parse(location.href); + var xdrClient = __webpack_require__(13)(_); + var xhrCors = 'withCredentials' in new XMLHttpRequest(); - delete window[callback]; - document.body.removeChild(script); + return { - if (event.type === 'load' && !body) { - event.type = 'error'; + request: function (request) { + + if (request.crossOrigin === null) { + request.crossOrigin = crossOrigin(request); } - response.ok = event.type !== 'error'; - response.status = response.ok ? 200 : 404; - response.responseText = body ? body : event.type; + if (request.crossOrigin) { - (response.ok ? resolve : reject)(response); - }; + if (!xhrCors) { + request.client = xdrClient; + } - script.onload = handler; - script.onerror = handler; + request.emulateHTTP = false; + } - document.body.appendChild(script); - }); + return request; + } + + }; + + function crossOrigin(request) { + + var requestUrl = _.url.parse(_.url(request)); + + return (requestUrl.protocol !== originUrl.protocol || requestUrl.host !== originUrl.host); + } }; /***/ }, -/* 7 */ +/* 18 */ /***/ function(module, exports) { /** diff --git a/dist/vue-resource.min.js b/dist/vue-resource.min.js index 1fd63fcf..8fcabc61 100644 --- a/dist/vue-resource.min.js +++ b/dist/vue-resource.min.js @@ -1,7 +1,7 @@ /** - * vue-resource v0.1.17 + * vue-resource v0.5.0 * https://github.com/vuejs/vue-resource * Released under the MIT License. */ -!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.VueResource=e():t.VueResource=e()}(this,function(){return function(t){function e(r){if(n[r])return n[r].exports;var o=n[r]={exports:{},id:r,loaded:!1};return t[r].call(o.exports,o,o.exports,e),o.loaded=!0,o.exports}var n={};return e.m=t,e.c=n,e.p="",e(0)}([function(t,e,n){function r(t){var e=n(1)(t);t.url=n(2)(e),t.http=n(3)(e),t.resource=n(7)(e),Object.defineProperties(t.prototype,{$url:{get:function(){return e.options(t.url,this,this.$options.url)}},$http:{get:function(){return e.options(t.http,this,this.$options.http)}},$resource:{get:function(){return t.resource.bind(this)}}})}window.Vue&&Vue.use(r),t.exports=r},function(t,e){t.exports=function(t){function e(t,r,o){for(var a in r)o&&(n.isPlainObject(r[a])||n.isArray(r[a]))?(n.isPlainObject(r[a])&&!n.isPlainObject(t[a])&&(t[a]={}),n.isArray(r[a])&&!n.isArray(t[a])&&(t[a]=[]),e(t[a],r[a],o)):void 0!==r[a]&&(t[a]=r[a])}var n=t.util.extend({},t.util);return n.isString=function(t){return"string"==typeof t},n.isFunction=function(t){return"function"==typeof t},n.options=function(t,e,r){return r=r||{},n.isFunction(r)&&(r=r.call(e)),n.extend(t.bind({vm:e,options:r}),t,{options:r})},n.each=function(t,e){var r,o;if("number"==typeof t.length)for(r=0;r=200&&a.status<300),(a.ok?n:r)(a)};a.onload=o,a.onabort=o,a.onerror=o,a.send(e.data)})}},function(t,e){function n(t){this.state=a,this.value=void 0,this.deferred=[];var e=this;try{t(function(t){e.resolve(t)},function(t){e.reject(t)})}catch(n){e.reject(n)}}var r=0,o=1,a=2;n.reject=function(t){return new n(function(e,n){n(t)})},n.resolve=function(t){return new n(function(e,n){e(t)})},n.all=function(t){return new n(function(e,n){function r(n){return function(r){a[n]=r,o+=1,o===t.length&&e(a)}}var o=0,a=[];0===t.length&&e(a);for(var i=0;i=200&&t.status<300,t.ok?t:r.reject(t)},function(e){return e instanceof Error&&t.error(e),r.reject(e)}),u.success&&a.success(u.success),u.error&&a.error(u.error),a}var r=n(5)(t),o=n(7)(t),i=n(8)(t),s={"Content-Type":"application/json"};return e.options={method:"get",data:"",params:{},headers:{},xhr:null,jsonp:"callback",beforeSend:null,crossOrigin:null,emulateHTTP:!1,emulateJSON:!1,timeout:0},e.interceptors=[n(10)(t),n(11)(t),n(12)(t),n(14)(t),n(15)(t),n(16)(t),n(17)(t)],e.headers={put:s,post:s,patch:s,"delete":s,common:{Accept:"application/json, text/plain, */*"},custom:{"X-Requested-With":"XMLHttpRequest"}},["get","put","post","patch","delete","jsonp"].forEach(function(n){e[n]=function(e,r,o,i){return t.isFunction(r)&&(i=o,o=r,r=void 0),t.isObject(o)&&(i=o,o=void 0),this(e,t.extend({method:n,data:r,success:o},i))}}),t.http=e}},function(t,e,n){t.exports=function(t){var e=window.Promise||n(6)(t),r=function(t){t instanceof e?this.promise=t:this.promise=new e(t),this.context=void 0};r.all=function(t){return new r(e.all(t))},r.resolve=function(t){return new r(e.resolve(t))},r.reject=function(t){return new r(e.reject(t))},r.race=function(t){return new r(e.race(t))};var o=r.prototype;return o.bind=function(t){return this.context=t,this},o.then=function(t,e){return t&&t.bind&&this.context&&(t=t.bind(this.context)),e&&e.bind&&this.context&&(e=e.bind(this.context)),this.promise=this.promise.then(t,e),this},o["catch"]=function(t){return t&&t.bind&&this.context&&(t=t.bind(this.context)),this.promise=this.promise["catch"](t),this},o["finally"]=function(t){return this.then(function(e){return t.call(this),e},function(n){return t.call(this),e.reject(n)})},o.success=function(e){return t.warn("The `success` method has been deprecated. Use the `then` method instead."),this.then(function(t){return e.call(this,t.data,t.status,t)||t})},o.error=function(e){return t.warn("The `error` method has been deprecated. Use the `catch` method instead."),this["catch"](function(t){return e.call(this,t.data,t.status,t)||t})},o.always=function(e){t.warn("The `always` method has been deprecated. Use the `finally` method instead.");var n=function(t){return e.call(this,t.data,t.status,t)||t};return this.then(n,n)},r}},function(t,e){t.exports=function(t){function e(t){this.state=o,this.value=void 0,this.deferred=[];var e=this;try{t(function(t){e.resolve(t)},function(t){e.reject(t)})}catch(n){e.reject(n)}}var n=0,r=1,o=2;e.reject=function(t){return new e(function(e,n){n(t)})},e.resolve=function(t){return new e(function(e,n){e(t)})},e.all=function(t){return new e(function(n,r){function o(e){return function(r){s[e]=r,i+=1,i===t.length&&n(s)}}var i=0,s=[];0===t.length&&n(s);for(var u=0;u= 200 && response.status < 300; + return response.ok ? response : Promise.reject(response); - if (options.emulateHTTP && !options.crossOrigin && /^(PUT|PATCH|DELETE)$/i.test(options.method)) { - options.headers['X-HTTP-Method-Override'] = options.method; - options.method = 'POST'; - } + }, function (response) { - if (options.emulateJSON && _.isPlainObject(options.data)) { - options.headers['Content-Type'] = 'application/x-www-form-urlencoded'; - options.data = _.url.params(options.data); - } - - if (_.isObject(options.data) && /FormData/i.test(options.data.toString())) { - delete options.headers['Content-Type']; - } - - if (_.isPlainObject(options.data)) { - options.data = JSON.stringify(options.data); - } + if (response instanceof Error) { + _.error(response); + } - promise = (options.method == 'JSONP' ? jsonp : xhr).call(this.vm, _, options); - promise = extendPromise(promise.then(transformResponse, transformResponse), this.vm); + return Promise.reject(response); + }); - if (options.success) { - promise = promise.success(options.success); + if (request.success) { + promise.success(request.success); } - if (options.error) { - promise = promise.error(options.error); + if (request.error) { + promise.error(request.error); } return promise; } - function extendPromise(promise, vm) { - - promise.success = function (fn) { - - return extendPromise(promise.then(function (response) { - return fn.call(vm, response.data, response.status, response) || response; - }), vm); - - }; - - promise.error = function (fn) { - - return extendPromise(promise.then(undefined, function (response) { - return fn.call(vm, response.data, response.status, response) || response; - }), vm); - - }; - - promise.always = function (fn) { - - var cb = function (response) { - return fn.call(vm, response.data, response.status, response) || response; - }; - - return extendPromise(promise.then(cb, cb), vm); - }; - - return promise; - } - - function transformResponse(response) { - - try { - response.data = JSON.parse(response.responseText); - } catch (e) { - response.data = response.responseText; - } - - return response.ok ? response : Promise.reject(response); - } - - function crossOrigin(url) { - - var requestUrl = _.url.parse(url); - - return (requestUrl.protocol !== originUrl.protocol || requestUrl.host !== originUrl.host); - } - Http.options = { method: 'get', - params: {}, data: '', + params: {}, + headers: {}, xhr: null, jsonp: 'callback', beforeSend: null, crossOrigin: null, emulateHTTP: false, - emulateJSON: false + emulateJSON: false, + timeout: 0 }; + Http.interceptors = [ + require('./interceptor/before')(_), + require('./interceptor/timeout')(_), + require('./interceptor/jsonp')(_), + require('./interceptor/method')(_), + require('./interceptor/mime')(_), + require('./interceptor/header')(_), + require('./interceptor/cors')(_) + ]; + Http.headers = { put: jsonType, post: jsonType, @@ -152,6 +95,11 @@ module.exports = function (_) { data = undefined; } + if (_.isObject(success)) { + options = success; + success = undefined; + } + return this(url, _.extend({method: method, data: data, success: success}, options)); }; }); diff --git a/src/index.js b/src/index.js index 002927cc..71ae7fa0 100644 --- a/src/index.js +++ b/src/index.js @@ -9,6 +9,7 @@ function install(Vue) { Vue.url = require('./url')(_); Vue.http = require('./http')(_); Vue.resource = require('./resource')(_); + Vue.promise = require('./promise')(_); Object.defineProperties(Vue.prototype, { @@ -37,4 +38,4 @@ if (window.Vue) { Vue.use(install); } -module.exports = install; \ No newline at end of file +module.exports = install; diff --git a/src/interceptor/before.js b/src/interceptor/before.js new file mode 100644 index 00000000..09fb6c82 --- /dev/null +++ b/src/interceptor/before.js @@ -0,0 +1,20 @@ +/** + * Before Interceptor. + */ + +module.exports = function (_) { + + return { + + request: function (request) { + + if (_.isFunction(request.beforeSend)) { + request.beforeSend.call(this, request); + } + + return request; + } + + }; + +}; diff --git a/src/interceptor/cors.js b/src/interceptor/cors.js new file mode 100644 index 00000000..b0000541 --- /dev/null +++ b/src/interceptor/cors.js @@ -0,0 +1,40 @@ +/** + * CORS Interceptor. + */ + +module.exports = function (_) { + + var originUrl = _.url.parse(location.href); + var xdrClient = require('../client/jsonp')(_); + var xhrCors = 'withCredentials' in new XMLHttpRequest(); + + return { + + request: function (request) { + + if (request.crossOrigin === null) { + request.crossOrigin = crossOrigin(request); + } + + if (request.crossOrigin) { + + if (!xhrCors) { + request.client = xdrClient; + } + + request.emulateHTTP = false; + } + + return request; + } + + }; + + function crossOrigin(request) { + + var requestUrl = _.url.parse(_.url(request)); + + return (requestUrl.protocol !== originUrl.protocol || requestUrl.host !== originUrl.host); + } + +}; diff --git a/src/interceptor/header.js b/src/interceptor/header.js new file mode 100644 index 00000000..dbffb0b2 --- /dev/null +++ b/src/interceptor/header.js @@ -0,0 +1,28 @@ +/** + * Header Interceptor. + */ + +module.exports = function (_) { + + return { + + request: function (request) { + + request.method = request.method.toUpperCase(); + request.headers = _.extend({}, _.http.headers.common, + !request.crossOrigin ? _.http.headers.custom : {}, + _.http.headers[request.method.toLowerCase()], + request.headers + ); + + if (_.isPlainObject(request.data) && /^(GET|JSONP)$/i.test(request.method)) { + _.extend(request.params, request.data); + delete request.data; + } + + return request; + } + + }; + +}; diff --git a/src/interceptor/index.js b/src/interceptor/index.js new file mode 100644 index 00000000..a54c8017 --- /dev/null +++ b/src/interceptor/index.js @@ -0,0 +1,47 @@ +/** + * Interceptor factory. + */ + +module.exports = function (_) { + + var Promise = require('../promise')(_); + + return function (handler, vm) { + return function (client) { + + if (_.isFunction(handler)) { + handler = handler.call(vm, Promise); + } + + return function (request) { + + if (_.isFunction(handler.request)) { + request = handler.request.call(vm, request); + } + + return when(request, function (request) { + return when(client(request), function (response) { + + if (_.isFunction(handler.response)) { + response = handler.response.call(vm, response); + } + + return response; + }); + }); + }; + }; + }; + + function when(value, fulfilled, rejected) { + + var promise = Promise.resolve(value); + + if (arguments.length < 2) { + return promise; + } + + return promise.then(fulfilled, rejected); + } + +}; diff --git a/src/interceptor/jsonp.js b/src/interceptor/jsonp.js new file mode 100644 index 00000000..a6d084bb --- /dev/null +++ b/src/interceptor/jsonp.js @@ -0,0 +1,22 @@ +/** + * JSONP Interceptor. + */ + +module.exports = function (_) { + + var jsonpClient = require('../client/jsonp')(_); + + return { + + request: function (request) { + + if (request.method == 'JSONP') { + request.client = jsonpClient; + } + + return request; + } + + }; + +}; diff --git a/src/interceptor/method.js b/src/interceptor/method.js new file mode 100644 index 00000000..ebd146f2 --- /dev/null +++ b/src/interceptor/method.js @@ -0,0 +1,21 @@ +/** + * HTTP method override Interceptor. + */ + +module.exports = function (_) { + + return { + + request: function (request) { + + if (request.emulateHTTP && /^(PUT|PATCH|DELETE)$/i.test(request.method)) { + request.headers['X-HTTP-Method-Override'] = request.method; + request.method = 'POST'; + } + + return request; + } + + }; + +}; diff --git a/src/interceptor/mime.js b/src/interceptor/mime.js new file mode 100644 index 00000000..db2cb908 --- /dev/null +++ b/src/interceptor/mime.js @@ -0,0 +1,38 @@ +/** + * Mime Interceptor. + */ + +module.exports = function (_) { + + return { + + request: function (request) { + + if (request.emulateJSON && _.isPlainObject(request.data)) { + request.headers['Content-Type'] = 'application/x-www-form-urlencoded'; + request.data = _.url.params(request.data); + } + + if (_.isObject(request.data) && /FormData/i.test(request.data.toString())) { + delete request.headers['Content-Type']; + } + + if (_.isPlainObject(request.data)) { + request.data = JSON.stringify(request.data); + } + + return request; + }, + + response: function (response) { + + try { + response.data = JSON.parse(response.data); + } catch (e) {} + + return response; + } + + }; + +}; diff --git a/src/interceptor/timeout.js b/src/interceptor/timeout.js new file mode 100644 index 00000000..d5edb381 --- /dev/null +++ b/src/interceptor/timeout.js @@ -0,0 +1,34 @@ +/** + * Timeout Interceptor. + */ + +module.exports = function (_) { + + return function () { + + var timeout; + + return { + + request: function (request) { + + if (request.timeout) { + timeout = setTimeout(function () { + request.cancel(); + }, request.timeout); + } + + return request; + }, + + response: function (response) { + + clearTimeout(timeout); + + return response; + } + + }; + }; + +}; diff --git a/src/lib/jsonp.js b/src/lib/jsonp.js deleted file mode 100644 index 3df41845..00000000 --- a/src/lib/jsonp.js +++ /dev/null @@ -1,50 +0,0 @@ -/** - * JSONP request. - */ - -var Promise = require('./promise'); - -module.exports = function (_, options) { - - var callback = '_jsonp' + Math.random().toString(36).substr(2), response = {}, script, body; - - options.params[options.jsonp] = callback; - - if (_.isFunction(options.beforeSend)) { - options.beforeSend.call(this, {}, options); - } - - return new Promise(function (resolve, reject) { - - script = document.createElement('script'); - script.src = _.url(options); - script.type = 'text/javascript'; - script.async = true; - - window[callback] = function (data) { - body = data; - }; - - var handler = function (event) { - - delete window[callback]; - document.body.removeChild(script); - - if (event.type === 'load' && !body) { - event.type = 'error'; - } - - response.ok = event.type !== 'error'; - response.status = response.ok ? 200 : 404; - response.responseText = body ? body : event.type; - - (response.ok ? resolve : reject)(response); - }; - - script.onload = handler; - script.onerror = handler; - - document.body.appendChild(script); - }); - -}; diff --git a/src/lib/promise.js b/src/lib/promise.js index 0c65f928..346e64c3 100644 --- a/src/lib/promise.js +++ b/src/lib/promise.js @@ -1,210 +1,180 @@ /** - * Promises/A+ polyfill v1.1.0 (https://github.com/bramstein/promis) + * Promises/A+ polyfill v1.1.4 (https://github.com/bramstein/promis) */ -var RESOLVED = 0; -var REJECTED = 1; -var PENDING = 2; +module.exports = function (_) { -function Promise(executor) { + var RESOLVED = 0; + var REJECTED = 1; + var PENDING = 2; - this.state = PENDING; - this.value = undefined; - this.deferred = []; + function Promise(executor) { - var promise = this; + this.state = PENDING; + this.value = undefined; + this.deferred = []; - try { - executor(function (x) { - promise.resolve(x); - }, function (r) { - promise.reject(r); - }); - } catch (e) { - promise.reject(e); + var promise = this; + + try { + executor(function (x) { + promise.resolve(x); + }, function (r) { + promise.reject(r); + }); + } catch (e) { + promise.reject(e); + } } -} -Promise.reject = function (r) { - return new Promise(function (resolve, reject) { - reject(r); - }); -}; + Promise.reject = function (r) { + return new Promise(function (resolve, reject) { + reject(r); + }); + }; -Promise.resolve = function (x) { - return new Promise(function (resolve, reject) { - resolve(x); - }); -}; + Promise.resolve = function (x) { + return new Promise(function (resolve, reject) { + resolve(x); + }); + }; -Promise.all = function all(iterable) { - return new Promise(function (resolve, reject) { - var count = 0, - result = []; + Promise.all = function all(iterable) { + return new Promise(function (resolve, reject) { + var count = 0, result = []; - if (iterable.length === 0) { - resolve(result); - } + if (iterable.length === 0) { + resolve(result); + } - function resolver(i) { - return function (x) { - result[i] = x; - count += 1; + function resolver(i) { + return function (x) { + result[i] = x; + count += 1; - if (count === iterable.length) { - resolve(result); - } - }; - } + if (count === iterable.length) { + resolve(result); + } + }; + } - for (var i = 0; i < iterable.length; i += 1) { - iterable[i].then(resolver(i), reject); - } - }); -}; + for (var i = 0; i < iterable.length; i += 1) { + Promise.resolve(iterable[i]).then(resolver(i), reject); + } + }); + }; -Promise.race = function race(iterable) { - return new Promise(function (resolve, reject) { - for (var i = 0; i < iterable.length; i += 1) { - iterable[i].then(resolve, reject); - } - }); -}; + Promise.race = function race(iterable) { + return new Promise(function (resolve, reject) { + for (var i = 0; i < iterable.length; i += 1) { + Promise.resolve(iterable[i]).then(resolve, reject); + } + }); + }; -var p = Promise.prototype; + var p = Promise.prototype; -p.resolve = function resolve(x) { - var promise = this; + p.resolve = function resolve(x) { + var promise = this; - if (promise.state === PENDING) { - if (x === promise) { - throw new TypeError('Promise settled with itself.'); - } + if (promise.state === PENDING) { + if (x === promise) { + throw new TypeError('Promise settled with itself.'); + } - var called = false; + var called = false; - try { - var then = x && x['then']; + try { + var then = x && x['then']; - if (x !== null && typeof x === 'object' && typeof then === 'function') { - then.call(x, function (x) { - if (!called) { - promise.resolve(x); - } - called = true; + if (x !== null && typeof x === 'object' && typeof then === 'function') { + then.call(x, function (x) { + if (!called) { + promise.resolve(x); + } + called = true; - }, function (r) { - if (!called) { - promise.reject(r); - } - called = true; - }); + }, function (r) { + if (!called) { + promise.reject(r); + } + called = true; + }); + return; + } + } catch (e) { + if (!called) { + promise.reject(e); + } return; } - } catch (e) { - if (!called) { - promise.reject(e); - } - return; + + promise.state = RESOLVED; + promise.value = x; + promise.notify(); } - promise.state = RESOLVED; - promise.value = x; - promise.notify(); - } -}; + }; -p.reject = function reject(reason) { - var promise = this; + p.reject = function reject(reason) { + var promise = this; - if (promise.state === PENDING) { - if (reason === promise) { - throw new TypeError('Promise settled with itself.'); - } + if (promise.state === PENDING) { + if (reason === promise) { + throw new TypeError('Promise settled with itself.'); + } - promise.state = REJECTED; - promise.value = reason; - promise.notify(); - } -}; + promise.state = REJECTED; + promise.value = reason; + promise.notify(); + } + }; -p.notify = function notify() { - var promise = this; - - async(function () { - if (promise.state !== PENDING) { - while (promise.deferred.length) { - var deferred = promise.deferred.shift(), - onResolved = deferred[0], - onRejected = deferred[1], - resolve = deferred[2], - reject = deferred[3]; - - try { - if (promise.state === RESOLVED) { - if (typeof onResolved === 'function') { - resolve(onResolved.call(undefined, promise.value)); - } else { - resolve(promise.value); - } - } else if (promise.state === REJECTED) { - if (typeof onRejected === 'function') { - resolve(onRejected.call(undefined, promise.value)); - } else { - reject(promise.value); + p.notify = function notify() { + var promise = this; + + _.nextTick(function () { + if (promise.state !== PENDING) { + while (promise.deferred.length) { + var deferred = promise.deferred.shift(), + onResolved = deferred[0], + onRejected = deferred[1], + resolve = deferred[2], + reject = deferred[3]; + + try { + if (promise.state === RESOLVED) { + if (typeof onResolved === 'function') { + resolve(onResolved.call(undefined, promise.value)); + } else { + resolve(promise.value); + } + } else if (promise.state === REJECTED) { + if (typeof onRejected === 'function') { + resolve(onRejected.call(undefined, promise.value)); + } else { + reject(promise.value); + } } + } catch (e) { + reject(e); } - } catch (e) { - reject(e); } } - } - }); -}; - -p.catch = function (onRejected) { - return this.then(undefined, onRejected); -}; - -p.then = function then(onResolved, onRejected) { - var promise = this; - - return new Promise(function (resolve, reject) { - promise.deferred.push([onResolved, onRejected, resolve, reject]); - promise.notify(); - }); -}; - -var queue = []; -var async = function (callback) { - queue.push(callback); - - if (queue.length === 1) { - async.async(); - } -}; - -async.run = function () { - while (queue.length) { - queue[0](); - queue.shift(); - } -}; - -if (window.MutationObserver) { - var el = document.createElement('div'); - var mo = new MutationObserver(async.run); + }); + }; - mo.observe(el, { - attributes: true - }); + p.then = function then(onResolved, onRejected) { + var promise = this; - async.async = function () { - el.setAttribute("x", 0); + return new Promise(function (resolve, reject) { + promise.deferred.push([onResolved, onRejected, resolve, reject]); + promise.notify(); + }); }; -} else { - async.async = function () { - setTimeout(async.run); + + p.catch = function (onRejected) { + return this.then(undefined, onRejected); }; -} -module.exports = window.Promise || Promise; + return Promise; +}; diff --git a/src/lib/url-template.js b/src/lib/url-template.js new file mode 100644 index 00000000..803388b4 --- /dev/null +++ b/src/lib/url-template.js @@ -0,0 +1,150 @@ +/** + * URL Template v2.0.6 (https://github.com/bramstein/url-template) + */ + +exports.expand = function (url, params, variables) { + + var tmpl = this.parse(url), expanded = tmpl.expand(params); + + if (variables) { + variables.push.apply(variables, tmpl.vars); + } + + return expanded; +}; + +exports.parse = function (template) { + + var operators = ['+', '#', '.', '/', ';', '?', '&'], variables = []; + + return { + vars: variables, + expand: function (context) { + return template.replace(/\{([^\{\}]+)\}|([^\{\}]+)/g, function (_, expression, literal) { + if (expression) { + + var operator = null, values = []; + + if (operators.indexOf(expression.charAt(0)) !== -1) { + operator = expression.charAt(0); + expression = expression.substr(1); + } + + expression.split(/,/g).forEach(function (variable) { + var tmp = /([^:\*]*)(?::(\d+)|(\*))?/.exec(variable); + values.push.apply(values, exports.getValues(context, operator, tmp[1], tmp[2] || tmp[3])); + variables.push(tmp[1]); + }); + + if (operator && operator !== '+') { + + var separator = ','; + + if (operator === '?') { + separator = '&'; + } else if (operator !== '#') { + separator = operator; + } + + return (values.length !== 0 ? operator : '') + values.join(separator); + } else { + return values.join(','); + } + + } else { + return exports.encodeReserved(literal); + } + }); + } + }; +}; + +exports.getValues = function (context, operator, key, modifier) { + + var value = context[key], result = []; + + if (this.isDefined(value) && value !== '') { + if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { + value = value.toString(); + + if (modifier && modifier !== '*') { + value = value.substring(0, parseInt(modifier, 10)); + } + + result.push(this.encodeValue(operator, value, this.isKeyOperator(operator) ? key : null)); + } else { + if (modifier === '*') { + if (Array.isArray(value)) { + value.filter(this.isDefined).forEach(function (value) { + result.push(this.encodeValue(operator, value, this.isKeyOperator(operator) ? key : null)); + }, this); + } else { + Object.keys(value).forEach(function (k) { + if (this.isDefined(value[k])) { + result.push(this.encodeValue(operator, value[k], k)); + } + }, this); + } + } else { + var tmp = []; + + if (Array.isArray(value)) { + value.filter(this.isDefined).forEach(function (value) { + tmp.push(this.encodeValue(operator, value)); + }, this); + } else { + Object.keys(value).forEach(function (k) { + if (this.isDefined(value[k])) { + tmp.push(encodeURIComponent(k)); + tmp.push(this.encodeValue(operator, value[k].toString())); + } + }, this); + } + + if (this.isKeyOperator(operator)) { + result.push(encodeURIComponent(key) + '=' + tmp.join(',')); + } else if (tmp.length !== 0) { + result.push(tmp.join(',')); + } + } + } + } else { + if (operator === ';') { + result.push(encodeURIComponent(key)); + } else if (value === '' && (operator === '&' || operator === '?')) { + result.push(encodeURIComponent(key) + '='); + } else if (value === '') { + result.push(''); + } + } + + return result; +}; + +exports.isDefined = function (value) { + return value !== undefined && value !== null; +}; + +exports.isKeyOperator = function (operator) { + return operator === ';' || operator === '&' || operator === '?'; +}; + +exports.encodeValue = function (operator, value, key) { + + value = (operator === '+' || operator === '#') ? this.encodeReserved(value) : encodeURIComponent(value); + + if (key) { + return encodeURIComponent(key) + '=' + value; + } else { + return value; + } +}; + +exports.encodeReserved = function (str) { + return str.split(/(%[0-9A-Fa-f]{2})/g).map(function (part) { + if (!/%[0-9A-Fa-f]/.test(part)) { + part = encodeURI(part); + } + return part; + }).join(''); +}; diff --git a/src/lib/util.js b/src/lib/util.js index f9780a4b..3131e6f2 100644 --- a/src/lib/util.js +++ b/src/lib/util.js @@ -4,7 +4,27 @@ module.exports = function (Vue) { - var _ = Vue.util.extend({}, Vue.util); + var _ = Vue.util.extend({}, Vue.util), config = Vue.config, console = window.console; + + _.warn = function (msg) { + if (console && Vue.util.warn && (!config.silent || config.debug)) { + console.warn('[VueResource warn]: ' + msg); + } + }; + + _.error = function (msg) { + if (console) { + console.error(msg); + } + }; + + _.trim = function (str) { + return str.replace(/^\s*|\s*$/g, ''); + }; + + _.toLower = function (str) { + return str ? str.toLowerCase() : ''; + }; _.isString = function (value) { return typeof value === 'string'; diff --git a/src/lib/xhr.js b/src/lib/xhr.js deleted file mode 100644 index 009bc7dd..00000000 --- a/src/lib/xhr.js +++ /dev/null @@ -1,51 +0,0 @@ -/** - * XMLHttp request. - */ - -var Promise = require('./promise'); -var XDomain = window.XDomainRequest; - -module.exports = function (_, options) { - - var request = new XMLHttpRequest(), promise; - - if (XDomain && options.crossOrigin) { - request = new XDomainRequest(); options.headers = {}; - } - - if (_.isPlainObject(options.xhr)) { - _.extend(request, options.xhr); - } - - if (_.isFunction(options.beforeSend)) { - options.beforeSend.call(this, request, options); - } - - promise = new Promise(function (resolve, reject) { - - request.open(options.method, _.url(options), true); - - _.each(options.headers, function (value, header) { - request.setRequestHeader(header, value); - }); - - var handler = function (event) { - - request.ok = event.type === 'load'; - - if (request.ok && request.status) { - request.ok = request.status >= 200 && request.status < 300; - } - - (request.ok ? resolve : reject)(request); - }; - - request.onload = handler; - request.onabort = handler; - request.onerror = handler; - - request.send(options.data); - }); - - return promise; -}; diff --git a/src/promise.js b/src/promise.js new file mode 100644 index 00000000..6bf03765 --- /dev/null +++ b/src/promise.js @@ -0,0 +1,111 @@ +/** + * Promise adapter. + */ + +module.exports = function (_) { + + var Promise = window.Promise || require('./lib/promise')(_); + + var Adapter = function (executor) { + + if (executor instanceof Promise) { + this.promise = executor; + } else { + this.promise = new Promise(executor); + } + + this.context = undefined; + }; + + Adapter.all = function (iterable) { + return new Adapter(Promise.all(iterable)); + }; + + Adapter.resolve = function (value) { + return new Adapter(Promise.resolve(value)); + }; + + Adapter.reject = function (reason) { + return new Adapter(Promise.reject(reason)); + }; + + Adapter.race = function (iterable) { + return new Adapter(Promise.race(iterable)); + }; + + var p = Adapter.prototype; + + p.bind = function (context) { + this.context = context; + return this; + }; + + p.then = function (fulfilled, rejected) { + + if (fulfilled && fulfilled.bind && this.context) { + fulfilled = fulfilled.bind(this.context); + } + + if (rejected && rejected.bind && this.context) { + rejected = rejected.bind(this.context); + } + + this.promise = this.promise.then(fulfilled, rejected); + + return this; + }; + + p.catch = function (rejected) { + + if (rejected && rejected.bind && this.context) { + rejected = rejected.bind(this.context); + } + + this.promise = this.promise.catch(rejected); + + return this; + }; + + p.finally = function (callback) { + + return this.then(function (value) { + callback.call(this); + return value; + }, function (reason) { + callback.call(this); + return Promise.reject(reason); + } + ); + }; + + p.success = function (callback) { + + _.warn('The `success` method has been deprecated. Use the `then` method instead.'); + + return this.then(function (response) { + return callback.call(this, response.data, response.status, response) || response; + }); + }; + + p.error = function (callback) { + + _.warn('The `error` method has been deprecated. Use the `catch` method instead.'); + + return this.catch(function (response) { + return callback.call(this, response.data, response.status, response) || response; + }); + }; + + p.always = function (callback) { + + _.warn('The `always` method has been deprecated. Use the `finally` method instead.'); + + var cb = function (response) { + return callback.call(this, response.data, response.status, response) || response; + }; + + return this.then(cb, cb); + }; + + return Adapter; +}; diff --git a/src/url.js b/src/url.js index 67ea81ec..eb42a0f5 100644 --- a/src/url.js +++ b/src/url.js @@ -2,14 +2,16 @@ * Service for URL templating. */ -var ie = document.documentMode; -var el = document.createElement('a'); +var UrlTemplate = require('./lib/url-template'); module.exports = function (_) { + var ie = document.documentMode; + var el = document.createElement('a'); + function Url(url, params) { - var urlParams = {}, queryParams = {}, options = url, query; + var urlParams = Object.keys(Url.options.params), queryParams = {}, options = url, query; if (!_.isPlainObject(options)) { options = {url: url, params: params}; @@ -19,10 +21,14 @@ module.exports = function (_) { Url.options, this.options, options ); - url = options.url.replace(/(\/?):([a-z]\w*)/gi, function (match, slash, name) { + url = UrlTemplate.expand(options.url, options.params, urlParams); + + url = url.replace(/(\/?):([a-z]\w*)/gi, function (match, slash, name) { + + _.warn('The `:' + name + '` parameter syntax has been deprecated. Use the `{' + name + '}` syntax instead.'); if (options.params[name]) { - urlParams[name] = true; + urlParams.push(name); return slash + encodeUriSegment(options.params[name]); } @@ -34,7 +40,7 @@ module.exports = function (_) { } _.each(options.params, function (value, key) { - if (!urlParams[key]) { + if (urlParams.indexOf(key) === -1) { queryParams[key] = value; } }); diff --git a/test/data/test.json b/test/data/test.json new file mode 100644 index 00000000..18d7acf5 --- /dev/null +++ b/test/data/test.json @@ -0,0 +1 @@ +{"foo": "bar"} \ No newline at end of file diff --git a/test/http.js b/test/http.js new file mode 100644 index 00000000..85ecae8e --- /dev/null +++ b/test/http.js @@ -0,0 +1,80 @@ +var Vue = require('vue'); + +describe('Vue.http', function () { + + it('get: test.json', function (done) { + + Vue.http.get('data/test.json').then(function (res) { + + expect(res.ok).toBe(true); + expect(res.status).toBe(200); + expect(res.data.foo).toBe('bar'); + + done(); + }); + + }); + + it('get: cors-api.com', function (done) { + + Vue.http.get('http://server.cors-api.appspot.com/server?id=1&enable=true').then(function (res) { + + expect(res.ok).toBe(true); + expect(res.status).toBe(200); + expect(res.data.shift().requestType).toBe('cors'); + expect(res.headers('content-type')).toBe('application/json'); + + done(); + }); + + }); + +}); + +describe('this.$http', function () { + + it('get: test.json', function (done) { + + var vm = new Vue({ + + created: function () { + + this.$http.get('data/test.json').then(function (res) { + + expect(this).toBe(vm); + expect(res.ok).toBe(true); + expect(res.status).toBe(200); + expect(res.data.foo).toBe('bar'); + + done(); + + }); + + } + + }); + + }); + + it('get: notfound.json using catch()', function (done) { + + var vm = new Vue({ + + created: function () { + + this.$http.get('data/notfound.json').catch(function (res) { + + expect(this).toBe(vm); + expect(res.ok).toBe(false); + expect(res.status).toBe(404); + + done(); + }); + + } + + }); + + }); + +}); \ No newline at end of file diff --git a/test/index.html b/test/index.html new file mode 100644 index 00000000..a60aca56 --- /dev/null +++ b/test/index.html @@ -0,0 +1,18 @@ + + + + + Vue Resource - Jasmine + + + + + + + + + + + + + diff --git a/test/index.js b/test/index.js new file mode 100644 index 00000000..388d62c9 --- /dev/null +++ b/test/index.js @@ -0,0 +1,9 @@ +var Vue = require('vue'); +var Resource = require('../src'); + +Vue.use(Resource); + +// require specs +require('./url.js'); +require('./http.js'); +require('./promise.js'); diff --git a/test/promise.js b/test/promise.js new file mode 100644 index 00000000..9360fa18 --- /dev/null +++ b/test/promise.js @@ -0,0 +1,183 @@ +var Vue = require('vue'); + +var specs = function (Promise) { + + it('then fulfill', function (done) { + + Promise.resolve(1).then(function (value) { + expect(value).toBe(1); + done(); + }); + + }); + + it('then reject', function (done) { + + Promise.reject(1).then(undefined, function (value) { + expect(value).toBe(1); + done(); + }); + + }); + + it('catch', function (done) { + + Promise.reject(1).catch(function (value) { + expect(value).toBe(1); + done(); + }); + + }); + + it('finally fulfill', function (done) { + + Promise.resolve(1).finally(function (arg) { + expect(arg).toBe(undefined); + }).then(function (arg) { + expect(arg).toBe(1); + done(); + }); + + }); + + it('finally reject', function (done) { + + Promise.reject(1).finally(function (arg) { + expect(arg).toBe(undefined); + }).catch(function (arg) { + expect(arg).toBe(1); + done(); + }); + + }); + + it('all', function (done) { + + Promise.all([ + + Promise.resolve(1), + Promise.resolve(2) + + ]).then(function (values) { + expect(values[0]).toBe(1); + expect(values[1]).toBe(2); + done(); + }); + + }); + + it('duplicate', function (done) { + + Promise.all([ + + Promise.resolve(1).then(function (value) { + expect(value).toBe(1); + }), + + Promise.resolve(2).then(function (value) { + expect(value).toBe(2); + }) + + ]).then(done); + + }); + + it('context', function (done) { + + var context = {foo: 'bar'}; + + Promise.resolve().bind(context).then(function () { + expect(this).toBe(context); + done(); + }); + + }); + + it('context chain fulfill', function (done) { + + var context = {foo: 'bar'}; + + Promise.resolve().bind(context).catch(undefined).finally(function () { + expect(this).toBe(context); + }).then(function () { + expect(this).toBe(context); + done(); + }); + + }); + + it('context chain reject', function (done) { + + var context = {foo: 'bar'}; + + Promise.reject().bind(context).catch(function () { + expect(this).toBe(context); + return Promise.reject(); + }).finally(function () { + expect(this).toBe(context); + }).catch(function () { + expect(this).toBe(context); + done(); + }); + + }); + + it('no chain breaking', function (done) { + + var promise = Promise.reject(); + + Promise.all([ + + promise.catch(function () { + expect(true).toBe(true); + }), + + promise.catch(function () { + fail('Chain break'); + }) + + ]).then(done); + + }); + +}; + +describe('Vue.promise (native)', function () { + + if (window.Promise) { + + var Promise = require('../src/promise')(); + + it('is native', function () { + expect((new Promise.resolve()).promise instanceof window.Promise).toBe(true); + }); + + specs(Promise); + + } else { + + it('no native promise', function () { + expect(true).toBe(true); + }); + + } + +}); + +describe('Vue.promise (polyfill)', function () { + + var native = window.Promise; + + delete window.Promise; + + var Promise = require('../src/promise')(Vue.util); + + window.Promise = native; + + it('is polyfill', function () { + expect((new Promise.resolve()).promise.state).toBe(0); + }); + + specs(Promise); + +}); diff --git a/test/url.js b/test/url.js new file mode 100644 index 00000000..e26debd8 --- /dev/null +++ b/test/url.js @@ -0,0 +1,33 @@ +var Vue = require('vue'); + +describe('Vue.url', function () { + + it('data/:id', function () { + + expect(Vue.url('data/:id')).toBe('data'); + expect(Vue.url('data/:id', {id: 1})).toBe('data/1'); + + }); + + it('data{/id}', function () { + + expect(Vue.url('data{/id}')).toBe('data'); + expect(Vue.url('data{/id}', {id: 1})).toBe('data/1'); + + }); + + it('{+path}data', function () { + + expect(Vue.url('{+path}data')).toBe('data'); + expect(Vue.url('{+path}data', {path: 'path1/path2/'})).toBe('path1/path2/data'); + + }); + + it('{+base}data', function () { + + Vue.url.options.params.base = 'base/path/'; + expect(Vue.url('{+base}data')).toBe('base/path/data'); + + }); + +}); diff --git a/test/webpack.config.js b/test/webpack.config.js new file mode 100644 index 00000000..521edad3 --- /dev/null +++ b/test/webpack.config.js @@ -0,0 +1,7 @@ +module.exports = { + entry: __dirname + '/index.js', + output: { + path: __dirname + '/', + filename: 'specs.js' + } +};