diff --git a/modules/angular2/src/http/backends/xhr_backend.ts b/modules/angular2/src/http/backends/xhr_backend.ts index e545abc5d57e7..43f12ab31d8f7 100644 --- a/modules/angular2/src/http/backends/xhr_backend.ts +++ b/modules/angular2/src/http/backends/xhr_backend.ts @@ -10,6 +10,7 @@ import {isPresent} from 'angular2/src/facade/lang'; import {Observable} from 'rxjs/Observable'; import {Observer} from 'rxjs/Observer'; import {isSuccess, getResponseURL} from '../http_utils'; +import {ContentType} from '../enums'; /** * Creates connections using `XMLHttpRequest`. Given a fully-qualified @@ -74,6 +75,8 @@ export class XHRConnection implements Connection { responseObserver.error(new Response(responseOptions)); }; + this.setDetectedContentType(req, _xhr); + if (isPresent(req.headers)) { req.headers.forEach((values, name) => _xhr.setRequestHeader(name, values.join(','))); } @@ -81,7 +84,7 @@ export class XHRConnection implements Connection { _xhr.addEventListener('load', onLoad); _xhr.addEventListener('error', onError); - _xhr.send(this.request.text()); + _xhr.send(this.request.getBody()); return () => { _xhr.removeEventListener('load', onLoad); @@ -90,6 +93,34 @@ export class XHRConnection implements Connection { }; }); } + + setDetectedContentType(req, _xhr) { + // Skip if a custom Content-Type header is provided + if (isPresent(req.headers) && isPresent(req.headers['Content-Type'])) { + return; + } + + // Set the detected content type + switch (req.contentType) { + case ContentType.NONE: + break; + case ContentType.JSON: + _xhr.setRequestHeader('Content-Type', 'application/json'); + break; + case ContentType.FORM: + _xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded;charset=UTF-8'); + break; + case ContentType.TEXT: + _xhr.setRequestHeader('Content-Type', 'text/plain'); + break; + case ContentType.BLOB: + var blob = req.blob(); + if (blob.type) { + _xhr.setRequestHeader('Content-Type', blob.type); + } + break; + } + } } /** diff --git a/modules/angular2/src/http/base_request_options.ts b/modules/angular2/src/http/base_request_options.ts index 133de727f722b..6bc10ffc1201c 100644 --- a/modules/angular2/src/http/base_request_options.ts +++ b/modules/angular2/src/http/base_request_options.ts @@ -43,8 +43,7 @@ export class RequestOptions { /** * Body to be used when creating a {@link Request}. */ - // TODO: support FormData, Blob, URLSearchParams - body: string; + body: any; /** * Url with which to perform a {@link Request}. */ diff --git a/modules/angular2/src/http/enums.ts b/modules/angular2/src/http/enums.ts index be56294748e60..20897d6ba3eb3 100644 --- a/modules/angular2/src/http/enums.ts +++ b/modules/angular2/src/http/enums.ts @@ -38,3 +38,16 @@ export enum ResponseType { Error, Opaque } + +/** + * Supported content type to be automatically associated with a {@link Request}. + */ +export enum ContentType { + NONE, + JSON, + FORM, + FORM_DATA, + TEXT, + BLOB, + ARRAY_BUFFER +} diff --git a/modules/angular2/src/http/http.ts b/modules/angular2/src/http/http.ts index 419b4f07d35f4..52f2729af2682 100644 --- a/modules/angular2/src/http/http.ts +++ b/modules/angular2/src/http/http.ts @@ -5,6 +5,7 @@ import {RequestOptionsArgs, Connection, ConnectionBackend} from './interfaces'; import {Request} from './static_request'; import {Response} from './static_response'; import {BaseRequestOptions, RequestOptions} from './base_request_options'; +import {URLSearchParams} from './url_search_params'; import {RequestMethod} from './enums'; import {Observable} from 'rxjs/Observable'; @@ -125,7 +126,7 @@ export class Http { /** * Performs a request with `post` http method. */ - post(url: string, body: string, options?: RequestOptionsArgs): Observable { + post(url: string, body: any, options?: RequestOptionsArgs): Observable { return httpRequest( this._backend, new Request(mergeOptions(this._defaultOptions.merge(new RequestOptions({body: body})), @@ -135,7 +136,7 @@ export class Http { /** * Performs a request with `put` http method. */ - put(url: string, body: string, options?: RequestOptionsArgs): Observable { + put(url: string, body: any, options?: RequestOptionsArgs): Observable { return httpRequest( this._backend, new Request(mergeOptions(this._defaultOptions.merge(new RequestOptions({body: body})), @@ -153,7 +154,7 @@ export class Http { /** * Performs a request with `patch` http method. */ - patch(url: string, body: string, options?: RequestOptionsArgs): Observable { + patch(url: string, body: any, options?: RequestOptionsArgs): Observable { return httpRequest( this._backend, new Request(mergeOptions(this._defaultOptions.merge(new RequestOptions({body: body})), diff --git a/modules/angular2/src/http/interfaces.ts b/modules/angular2/src/http/interfaces.ts index 82694502dcf40..5d74571d8920f 100644 --- a/modules/angular2/src/http/interfaces.ts +++ b/modules/angular2/src/http/interfaces.ts @@ -31,8 +31,7 @@ export interface RequestOptionsArgs { method?: string | RequestMethod; search?: string | URLSearchParams; headers?: Headers; - // TODO: Support Blob, ArrayBuffer, JSON, URLSearchParams, FormData - body?: string; + body?: any; } /** diff --git a/modules/angular2/src/http/static_request.ts b/modules/angular2/src/http/static_request.ts index 093fa97b2fc59..4262a706a22ba 100644 --- a/modules/angular2/src/http/static_request.ts +++ b/modules/angular2/src/http/static_request.ts @@ -1,12 +1,15 @@ import {RequestMethod} from './enums'; import {RequestArgs} from './interfaces'; import {Headers} from './headers'; +import {ContentType} from './enums'; +import {URLSearchParams} from './url_search_params'; import {normalizeMethodName} from './http_utils'; import { RegExpWrapper, CONST_EXPR, isPresent, isJsObject, + isPrimitive, StringWrapper } from 'angular2/src/facade/lang'; @@ -59,8 +62,10 @@ export class Request { headers: Headers; /** Url of the remote resource */ url: string; - // TODO: support URLSearchParams | FormData | Blob | ArrayBuffer - private _body: string; + /** Body of the request **/ + private _body: any; + /** Type of the request body **/ + private contentType: ContentType; constructor(requestOptions: RequestArgs) { // TODO: assert that url is present let url = requestOptions.url; @@ -77,6 +82,7 @@ export class Request { } } this._body = requestOptions.body; + this.contentType = this.detectContentType(); this.method = normalizeMethodName(requestOptions.method); // TODO(jeffbcross): implement behavior // Defaults to 'omit', consistent with browser @@ -91,4 +97,76 @@ export class Request { * string. */ text(): String { return isPresent(this._body) ? this._body.toString() : ''; } + + /** + * Returns the request's body as JSON string, assuming that body exists. If body is undefined, + * return + * empty + * string. + */ + json(): String { return isPresent(this._body) ? JSON.stringify(this._body) : ''; } + + /** + * Returns the request's body as array buffer, assuming that body exists. If body is undefined, + * return + * null. + */ + arrayBuffer(): ArrayBuffer { + if (this._body instanceof ArrayBuffer) return this._body; + throw "The request body isn't an array buffer"; + } + + /** + * Returns the request's body as blob, assuming that body exists. If body is undefined, return + * null. + */ + blob(): Blob { + if (this._body instanceof Blob) return this._body; + if (this._body instanceof ArrayBuffer) return new Blob([this._body]); + throw "The request body isn't either a blob or an array buffer"; + } + + /** + * Returns the content type of request's body based on its type. + */ + detectContentType() { + if (this._body == null) { + return ContentType.NONE; + } else if (this._body instanceof URLSearchParams) { + return ContentType.FORM; + } else if (this._body instanceof FormData) { + return ContentType.FORM_DATA; + } else if (this._body instanceof Blob) { + return ContentType.BLOB; + } else if (this._body instanceof ArrayBuffer) { + return ContentType.ARRAY_BUFFER; + } else if (isPrimitive(this._body)) { + return ContentType.TEXT; + } else { + return ContentType.JSON; + } + } + + /** + * Returns the request's body according to its type. If body is undefined, return + * null. + */ + getBody(): any { + switch (this.contentType) { + case ContentType.JSON: + return this.json(); + case ContentType.FORM: + return this.text(); + case ContentType.FORM_DATA: + return this._body; + case ContentType.TEXT: + return this.text(); + case ContentType.BLOB: + return this.blob(); + case ContentType.ARRAY_BUFFER: + return this.arrayBuffer(); + default: + return null; + }; + } } diff --git a/modules/angular2/src/testing/utils.ts b/modules/angular2/src/testing/utils.ts index 6c604a97d6faa..cdb7df8387b2a 100644 --- a/modules/angular2/src/testing/utils.ts +++ b/modules/angular2/src/testing/utils.ts @@ -65,6 +65,8 @@ export class BrowserDetection { } } +BrowserDetection.setup(); + export function dispatchEvent(element, eventType): void { DOM.dispatchEvent(element, DOM.createEvent(eventType)); } diff --git a/modules/angular2/test/http/backends/xhr_backend_spec.ts b/modules/angular2/test/http/backends/xhr_backend_spec.ts index d7542c9b8462e..06b72eafef80b 100644 --- a/modules/angular2/test/http/backends/xhr_backend_spec.ts +++ b/modules/angular2/test/http/backends/xhr_backend_spec.ts @@ -22,6 +22,7 @@ import {Map} from 'angular2/src/facade/collection'; import {RequestOptions, BaseRequestOptions} from 'angular2/src/http/base_request_options'; import {BaseResponseOptions, ResponseOptions} from 'angular2/src/http/base_response_options'; import {ResponseType} from 'angular2/src/http/enums'; +import {URLSearchParams} from 'angular2/src/http/url_search_params'; var abortSpy: any; var sendSpy: any; @@ -174,6 +175,127 @@ export function main() { expect(setRequestHeaderSpy).toHaveBeenCalledWith('X-Multi', 'a,b'); }); + it('should use object body and detect content type header to the request', () => { + var body = {test: 'val'}; + var base = new BaseRequestOptions(); + var connection = new XHRConnection( + new Request(base.merge(new RequestOptions({body: body}))), new MockBrowserXHR()); + connection.response.subscribe(); + expect(sendSpy).toHaveBeenCalledWith(JSON.stringify(body)); + expect(setRequestHeaderSpy).toHaveBeenCalledWith('Content-Type', 'application/json'); + }); + + it('should use number body and detect content type header to the request', () => { + var body = 23; + var base = new BaseRequestOptions(); + var connection = new XHRConnection( + new Request(base.merge(new RequestOptions({body: body}))), new MockBrowserXHR()); + connection.response.subscribe(); + expect(sendSpy).toHaveBeenCalledWith('23'); + expect(setRequestHeaderSpy).toHaveBeenCalledWith('Content-Type', 'text/plain'); + }); + + it('should use string body and detect content type header to the request', () => { + var body = 'some string'; + var base = new BaseRequestOptions(); + var connection = new XHRConnection( + new Request(base.merge(new RequestOptions({body: body}))), new MockBrowserXHR()); + connection.response.subscribe(); + expect(sendSpy).toHaveBeenCalledWith(body); + expect(setRequestHeaderSpy).toHaveBeenCalledWith('Content-Type', 'text/plain'); + }); + + it('should use URLSearchParams body and detect content type header to the request', () => { + var body = new URLSearchParams(); + body.set('test1', 'val1'); + body.set('test2', 'val2'); + var base = new BaseRequestOptions(); + var connection = new XHRConnection( + new Request(base.merge(new RequestOptions({body: body}))), new MockBrowserXHR()); + connection.response.subscribe(); + expect(sendSpy).toHaveBeenCalledWith('test1=val1&test2=val2'); + expect(setRequestHeaderSpy) + .toHaveBeenCalledWith('Content-Type', + 'application/x-www-form-urlencoded;charset=UTF-8'); + }); + + it('should use FormData body and detect content type header to the request', () => { + var body = new FormData(); + body.append('test1', 'val1'); + body.append('test2', 123456); + var blob = new Blob(['body { color: red; }'], {type: 'text/css'}); + body.append("userfile", blob); + var base = new BaseRequestOptions(); + var connection = new XHRConnection( + new Request(base.merge(new RequestOptions({body: body}))), new MockBrowserXHR()); + connection.response.subscribe(); + expect(sendSpy).toHaveBeenCalledWith(body); + expect(setRequestHeaderSpy).not.toHaveBeenCalledWith(); + }); + + it('should use blob body and detect content type header to the request', () => { + var body = new Blob(['body { color: red; }'], {type: 'text/css'}); + var base = new BaseRequestOptions(); + var connection = new XHRConnection( + new Request(base.merge(new RequestOptions({body: body}))), new MockBrowserXHR()); + connection.response.subscribe(); + expect(sendSpy).toHaveBeenCalledWith(body); + expect(setRequestHeaderSpy).toHaveBeenCalledWith('Content-Type', 'text/css'); + }); + + it('should use blob body without type to the request', () => { + var body = new Blob(['body { color: red; }']); + var base = new BaseRequestOptions(); + var connection = new XHRConnection( + new Request(base.merge(new RequestOptions({body: body}))), new MockBrowserXHR()); + connection.response.subscribe(); + expect(sendSpy).toHaveBeenCalledWith(body); + expect(setRequestHeaderSpy).not.toHaveBeenCalledWith(); + }); + + it('should use blob body without type with custom content type header to the request', () => { + var headers = new Headers({'Content-Type': 'text/css'}); + var body = new Blob(['body { color: red; }']); + var base = new BaseRequestOptions(); + var connection = new XHRConnection( + new Request(base.merge(new RequestOptions({body: body, headers: headers}))), + new MockBrowserXHR()); + connection.response.subscribe(); + expect(sendSpy).toHaveBeenCalledWith(body); + expect(setRequestHeaderSpy).toHaveBeenCalledWith('Content-Type', 'text/css'); + }); + + it('should use array buffer body to the request', () => { + var body = new ArrayBuffer(512); + var longInt8View = new Uint8Array(body); + for (var i = 0; i < longInt8View.length; i++) { + longInt8View[i] = i % 255; + } + var base = new BaseRequestOptions(); + var connection = new XHRConnection( + new Request(base.merge(new RequestOptions({body: body}))), new MockBrowserXHR()); + connection.response.subscribe(); + expect(sendSpy).toHaveBeenCalledWith(body); + expect(setRequestHeaderSpy).not.toHaveBeenCalledWith(); + }); + + it('should use array buffer body without type with custom content type header to the request', + () => { + var headers = new Headers({'Content-Type': 'text/css'}); + var body = new ArrayBuffer(512); + var longInt8View = new Uint8Array(body); + for (var i = 0; i < longInt8View.length; i++) { + longInt8View[i] = i % 255; + } + var base = new BaseRequestOptions(); + var connection = new XHRConnection( + new Request(base.merge(new RequestOptions({body: body, headers: headers}))), + new MockBrowserXHR()); + connection.response.subscribe(); + expect(sendSpy).toHaveBeenCalledWith(body); + expect(setRequestHeaderSpy).toHaveBeenCalledWith('Content-Type', 'text/css'); + }); + it('should return the correct status code', inject([AsyncTestCompleter], (async: AsyncTestCompleter) => { var statusCode = 418;