diff --git a/modules/@angular/core/src/reflection/reflection.ts b/modules/@angular/core/src/reflection/reflection.ts index 1028ce0159..f376641fab 100644 --- a/modules/@angular/core/src/reflection/reflection.ts +++ b/modules/@angular/core/src/reflection/reflection.ts @@ -7,3 +7,4 @@ import {ReflectionCapabilities} from './reflection_capabilities'; * about symbols. */ export var reflector = new Reflector(new ReflectionCapabilities()); + diff --git a/modules/@angular/http/src/backends/xhr_backend.ts b/modules/@angular/http/src/backends/xhr_backend.ts index dd418ceb1f..3b827ffa2c 100644 --- a/modules/@angular/http/src/backends/xhr_backend.ts +++ b/modules/@angular/http/src/backends/xhr_backend.ts @@ -10,6 +10,7 @@ import {isPresent, isString} from '../../src/facade/lang'; import {Observable} from 'rxjs/Observable'; import {Observer} from 'rxjs/Observer'; import {isSuccess, getResponseURL} from '../http_utils'; +import {ContentType} from '../enums'; const XSSI_PREFIX = ')]}\',\n'; @@ -83,6 +84,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(','))); } @@ -90,7 +93,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); @@ -99,6 +102,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/@angular/http/src/base_request_options.ts b/modules/@angular/http/src/base_request_options.ts index ef9fe9a5c3..9eb579b095 100644 --- a/modules/@angular/http/src/base_request_options.ts +++ b/modules/@angular/http/src/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/@angular/http/src/enums.ts b/modules/@angular/http/src/enums.ts index 3c20cb157b..3be58fb595 100644 --- a/modules/@angular/http/src/enums.ts +++ b/modules/@angular/http/src/enums.ts @@ -36,3 +36,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/@angular/http/src/http.ts b/modules/@angular/http/src/http.ts index 61782119ff..f68dc5645b 100644 --- a/modules/@angular/http/src/http.ts +++ b/modules/@angular/http/src/http.ts @@ -5,6 +5,7 @@ import {RequestOptionsArgs, 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'; @@ -126,7 +127,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})), @@ -136,7 +137,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})), @@ -154,7 +155,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/@angular/http/src/interfaces.ts b/modules/@angular/http/src/interfaces.ts index fc7eafb7ed..4e48ba5db6 100644 --- a/modules/@angular/http/src/interfaces.ts +++ b/modules/@angular/http/src/interfaces.ts @@ -29,8 +29,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/@angular/http/src/static_request.ts b/modules/@angular/http/src/static_request.ts index bc7bf7e6f1..3ce95605e7 100644 --- a/modules/@angular/http/src/static_request.ts +++ b/modules/@angular/http/src/static_request.ts @@ -1,6 +1,8 @@ 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 {isPresent, StringWrapper} from '../src/facade/lang'; @@ -53,8 +55,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; @@ -71,6 +75,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 @@ -85,4 +90,82 @@ 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 (this._body && typeof this._body == 'object') { + return ContentType.JSON; + } else { + return ContentType.TEXT; + } + } + + /** + * 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; + } + } } + +const noop = function () {}; +const w = typeof window == 'object' ? window : noop; +const FormData = w['FormData'] || noop; +const Blob = w['Blob'] || noop; +const ArrayBuffer = w['ArrayBuffer'] || noop; diff --git a/modules/@angular/http/test/backends/xhr_backend_spec.ts b/modules/@angular/http/test/backends/xhr_backend_spec.ts index d290391fcf..63641174d6 100644 --- a/modules/@angular/http/test/backends/xhr_backend_spec.ts +++ b/modules/@angular/http/test/backends/xhr_backend_spec.ts @@ -20,6 +20,7 @@ import {Map} from '../../src/facade/collection'; import {RequestOptions, BaseRequestOptions} from '../../src/base_request_options'; import {BaseResponseOptions, ResponseOptions} from '../../src/base_response_options'; import {ResponseType} from '../../src/enums'; +import {URLSearchParams} from '../../src/url_search_params'; var abortSpy: any; var sendSpy: any; @@ -176,6 +177,129 @@ 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'); + }); + + if (global['Blob']) { + 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; diff --git a/modules/@angular/platform-browser/testing/browser_util.ts b/modules/@angular/platform-browser/testing/browser_util.ts index 086a7f099c..7ae3beebcd 100644 --- a/modules/@angular/platform-browser/testing/browser_util.ts +++ b/modules/@angular/platform-browser/testing/browser_util.ts @@ -50,6 +50,8 @@ export class BrowserDetection { } } +BrowserDetection.setup(); + export function dispatchEvent(element, eventType): void { getDOM().dispatchEvent(element, getDOM().createEvent(eventType)); }