diff --git a/modules/angular2/core.ts b/modules/angular2/core.ts index c7ee651169..3318ddfa5f 100644 --- a/modules/angular2/core.ts +++ b/modules/angular2/core.ts @@ -17,5 +17,6 @@ export * from './src/core/compiler/dynamic_component_loader'; export {ViewRef, ProtoViewRef} from './src/core/compiler/view_ref'; export {ViewContainerRef} from './src/core/compiler/view_container_ref'; export {ElementRef} from './src/core/compiler/element_ref'; +export {EventEmitter} from './src/facade/async'; export {NgZone} from './src/core/zone/ng_zone'; \ No newline at end of file diff --git a/modules/angular2/http.ts b/modules/angular2/http.ts index 66e312a319..b1d52a7d43 100644 --- a/modules/angular2/http.ts +++ b/modules/angular2/http.ts @@ -6,30 +6,39 @@ * class. */ import {bind, Binding} from 'angular2/di'; -import {Http} from './src/http/http'; +import {Http} from 'angular2/src/http/http'; import {XHRBackend, XHRConnection} from 'angular2/src/http/backends/xhr_backend'; -import {BrowserXHR} from 'angular2/src/http/backends/browser_xhr'; +import {BrowserXhr} from 'angular2/src/http/backends/browser_xhr'; import {BaseRequestOptions, RequestOptions} from 'angular2/src/http/base_request_options'; import {ConnectionBackend} from 'angular2/src/http/interfaces'; export {MockConnection, MockBackend} from 'angular2/src/http/backends/mock_backend'; export {Request} from 'angular2/src/http/static_request'; export {Response} from 'angular2/src/http/static_response'; +import {BaseResponseOptions, ResponseOptions} from 'angular2/src/http/base_response_options'; export { IRequestOptions, - IResponse, + IResponseOptions, Connection, ConnectionBackend } from 'angular2/src/http/interfaces'; export {BaseRequestOptions, RequestOptions} from 'angular2/src/http/base_request_options'; +export {BaseResponseOptions, ResponseOptions} from 'angular2/src/http/base_response_options'; export {XHRBackend, XHRConnection} from 'angular2/src/http/backends/xhr_backend'; -export {Http} from './src/http/http'; +export {Http} from 'angular2/src/http/http'; export {Headers} from 'angular2/src/http/headers'; -export * from 'angular2/src/http/enums'; +export { + ResponseTypes, + ReadyStates, + RequestMethods, + RequestCredentialsOpts, + RequestCacheOpts, + RequestModesOpts +} from 'angular2/src/http/enums'; export {URLSearchParams} from 'angular2/src/http/url_search_params'; /** @@ -49,5 +58,11 @@ export {URLSearchParams} from 'angular2/src/http/url_search_params'; * ``` * */ -export var httpInjectables: List = - [bind(ConnectionBackend).toClass(XHRBackend), BrowserXHR, XHRBackend, BaseRequestOptions, Http]; +export var httpInjectables: List = [ + bind(ConnectionBackend) + .toClass(XHRBackend), + BrowserXhr, + bind(RequestOptions).toClass(BaseRequestOptions), + bind(ResponseOptions).toClass(BaseResponseOptions), + Http +]; diff --git a/modules/angular2/src/http/backends/browser_xhr.dart b/modules/angular2/src/http/backends/browser_xhr.dart index 41035d3621..8f21b47d94 100644 --- a/modules/angular2/src/http/backends/browser_xhr.dart +++ b/modules/angular2/src/http/backends/browser_xhr.dart @@ -4,7 +4,7 @@ import 'dart:html' show HttpRequest; import 'package:angular2/di.dart'; @Injectable() -class BrowserXHR { +class BrowserXhr { HttpRequest build() { return new HttpRequest(); } diff --git a/modules/angular2/src/http/backends/browser_xhr.ts b/modules/angular2/src/http/backends/browser_xhr.ts index 9f484290aa..cb27b01a83 100644 --- a/modules/angular2/src/http/backends/browser_xhr.ts +++ b/modules/angular2/src/http/backends/browser_xhr.ts @@ -1,10 +1,8 @@ -declare var window; - import {Injectable} from 'angular2/di'; // Make sure not to evaluate this in a non-browser environment! @Injectable() -export class BrowserXHR { +export class BrowserXhr { constructor() {} - build(): any { return (new window.XMLHttpRequest()); } + build(): any { return (new XMLHttpRequest()); } } diff --git a/modules/angular2/src/http/backends/mock_backend.ts b/modules/angular2/src/http/backends/mock_backend.ts index 43c56eabe4..f74e021acb 100644 --- a/modules/angular2/src/http/backends/mock_backend.ts +++ b/modules/angular2/src/http/backends/mock_backend.ts @@ -9,11 +9,7 @@ import {IMPLEMENTS, BaseException} from 'angular2/src/facade/lang'; /** * - * Connection class used by MockBackend - * - * This class is typically not instantiated directly, but instances can be retrieved by subscribing - *to the `connections` Observable of - * {@link MockBackend} in order to mock responses to requests. + * Mock Connection to represent a {@link Connection} for tests. * **/ @IMPLEMENTS(Connection) @@ -32,9 +28,8 @@ export class MockConnection { request: Request; /** - * [RxJS - * Observable](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/observable.md) - * of {@link Response}. Can be subscribed to in order to be notified when a response is available. + * {@link EventEmitter} of {@link Response}. Can be subscribed to in order to be notified when a + * response is available. */ response: EventEmitter; @@ -55,7 +50,7 @@ export class MockConnection { /** * Sends a mock response to the connection. This response is the value that is emitted to the - * `Observable` returned by {@link Http}. + * {@link EventEmitter} returned by {@link Http}. * * #Example * @@ -91,7 +86,8 @@ export class MockConnection { // TODO(jeffbcross): consider using Response type /** - * Emits the provided error object as an error to the {@link Response} observable returned + * Emits the provided error object as an error to the {@link Response} {@link EventEmitter} + * returned * from {@link Http}. */ mockError(err?) { @@ -137,8 +133,7 @@ export class MockConnection { @IMPLEMENTS(ConnectionBackend) export class MockBackend { /** - * [RxJS - * Subject](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/subjects/subject.md) + * {@link EventEmitter} * of {@link MockConnection} instances that have been created by this backend. Can be subscribed * to in order to respond to connections. * @@ -162,7 +157,7 @@ export class MockBackend { * http.request('something.json').subscribe(res => { * text = res.text(); * }); - * connection.mockRespond(new Response('Something')); + * connection.mockRespond(new Response({body: 'Something'})); * expect(text).toBe('Something'); * }); * ``` @@ -179,8 +174,8 @@ export class MockBackend { */ connectionsArray: Array; /** - * [Observable](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/observable.md) - * of {@link MockConnection} instances that haven't yet been resolved (i.e. with a `readyState` + * {@link EventEmitter} of {@link MockConnection} instances that haven't yet been resolved (i.e. + * with a `readyState` * less than 4). Used internally to verify that no connections are pending via the * `verifyNoPendingRequests` method. * @@ -193,7 +188,6 @@ export class MockBackend { ObservableWrapper.subscribe(this.connections, connection => this.connectionsArray.push(connection)); this.pendingConnections = new EventEmitter(); - // Observable.fromArray(this.connectionsArray).filter((c) => c.readyState < ReadyStates.DONE); } /** @@ -218,10 +212,10 @@ export class MockBackend { /** * Creates a new {@link MockConnection}. This is equivalent to calling `new * MockConnection()`, except that it also will emit the new `Connection` to the `connections` - * observable of this `MockBackend` instance. This method will usually only be used by tests + * emitter of this `MockBackend` instance. This method will usually only be used by tests * against the framework itself, not by end-users. */ - createConnection(req: Request) { + createConnection(req: Request): Connection { if (!isPresent(req) || !(req instanceof Request)) { throw new BaseException(`createConnection requires an instance of Request, got ${req}`); } diff --git a/modules/angular2/src/http/backends/xhr_backend.ts b/modules/angular2/src/http/backends/xhr_backend.ts index 9dc1f368fd..385757611a 100644 --- a/modules/angular2/src/http/backends/xhr_backend.ts +++ b/modules/angular2/src/http/backends/xhr_backend.ts @@ -2,8 +2,9 @@ import {ConnectionBackend, Connection} from '../interfaces'; import {ReadyStates, RequestMethods, RequestMethodsMap} from '../enums'; import {Request} from '../static_request'; import {Response} from '../static_response'; +import {ResponseOptions, BaseResponseOptions} from '../base_response_options'; import {Injectable} from 'angular2/di'; -import {BrowserXHR} from './browser_xhr'; +import {BrowserXhr} from './browser_xhr'; import {EventEmitter, ObservableWrapper} from 'angular2/src/facade/async'; import {isPresent, ENUM_INDEX} from 'angular2/src/facade/lang'; @@ -18,14 +19,14 @@ import {isPresent, ENUM_INDEX} from 'angular2/src/facade/lang'; export class XHRConnection implements Connection { request: Request; /** - * Response - * [Subject](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/subjects/subject.md) - * which emits a single {@link Response} value on load event of `XMLHttpRequest`. + * Response {@link EventEmitter} which emits a single {@link Response} value on load event of + * `XMLHttpRequest`. */ - response: EventEmitter; //; + response: EventEmitter; // TODO: Make generic of ; readyState: ReadyStates; - private _xhr; - constructor(req: Request, browserXHR: BrowserXHR) { + private _xhr; // TODO: make type XMLHttpRequest, pending resolution of + // https://github.com/angular/ts2dart/issues/230 + constructor(req: Request, browserXHR: BrowserXhr, baseResponseOptions?: ResponseOptions) { // TODO: get rid of this when enum lookups are available in ts2dart // https://github.com/angular/ts2dart/issues/221 var requestMethodsMap = new RequestMethodsMap(); @@ -34,12 +35,15 @@ export class XHRConnection implements Connection { this._xhr = browserXHR.build(); // TODO(jeffbcross): implement error listening/propagation this._xhr.open(requestMethodsMap.getMethod(ENUM_INDEX(req.method)), req.url); - this._xhr.addEventListener( - 'load', - (_) => {ObservableWrapper.callNext( - this.response, new Response({ - body: isPresent(this._xhr.response) ? this._xhr.response : this._xhr.responseText - }))}); + this._xhr.addEventListener('load', (_) => { + var responseOptions = new ResponseOptions( + {body: isPresent(this._xhr.response) ? this._xhr.response : this._xhr.responseText}); + if (isPresent(baseResponseOptions)) { + responseOptions = baseResponseOptions.merge(responseOptions); + } + + ObservableWrapper.callNext(this.response, new Response(responseOptions)) + }); // TODO(jeffbcross): make this more dynamic based on body type this._xhr.send(this.request.text()); } @@ -78,8 +82,8 @@ export class XHRConnection implements Connection { **/ @Injectable() export class XHRBackend implements ConnectionBackend { - constructor(private _browserXHR: BrowserXHR) {} + constructor(private _browserXHR: BrowserXhr, private _baseResponseOptions: ResponseOptions) {} createConnection(request: Request): XHRConnection { - return new XHRConnection(request, this._browserXHR); + return new XHRConnection(request, this._browserXHR, this._baseResponseOptions); } } diff --git a/modules/angular2/src/http/base_request_options.ts b/modules/angular2/src/http/base_request_options.ts index 2507551af6..5583ce582a 100644 --- a/modules/angular2/src/http/base_request_options.ts +++ b/modules/angular2/src/http/base_request_options.ts @@ -3,15 +3,14 @@ import {Headers} from './headers'; import {RequestModesOpts, RequestMethods, RequestCacheOpts, RequestCredentialsOpts} from './enums'; import {IRequestOptions} from './interfaces'; import {Injectable} from 'angular2/di'; -import {ListWrapper, StringMapWrapper, StringMap} from 'angular2/src/facade/collection'; -const INITIALIZER = CONST_EXPR({}); /** - * Creates a request options object with default properties as described in the [Fetch + * Creates a request options object similar to the `RequestInit` description + * in the [Fetch * Spec](https://fetch.spec.whatwg.org/#requestinit) to be optionally provided when instantiating a - * {@link Request}. This class is used implicitly by {@link Http} to merge in provided request - * options with the default options specified here. These same default options are injectable via - * the {@link BaseRequestOptions} class. + * {@link Request}. + * + * All values are null by default. */ export class RequestOptions implements IRequestOptions { /** @@ -19,7 +18,7 @@ export class RequestOptions implements IRequestOptions { * * Defaults to "GET". */ - method: RequestMethods = RequestMethods.GET; + method: RequestMethods; /** * Headers object based on the `Headers` class in the [Fetch * Spec](https://fetch.spec.whatwg.org/#headers-class). @@ -28,54 +27,42 @@ export class RequestOptions implements IRequestOptions { /** * Body to be used when creating the request. */ - // TODO: support FormData, Blob, URLSearchParams, JSON + // TODO: support FormData, Blob, URLSearchParams body: string; - mode: RequestModesOpts = RequestModesOpts.Cors; + mode: RequestModesOpts; credentials: RequestCredentialsOpts; cache: RequestCacheOpts; url: string; constructor({method, headers, body, mode, credentials, cache, url}: IRequestOptions = {}) { - this.method = isPresent(method) ? method : RequestMethods.GET; - this.headers = headers; - this.body = body; - this.mode = isPresent(mode) ? mode : RequestModesOpts.Cors; - this.credentials = credentials; - this.cache = cache; - this.url = url; + this.method = isPresent(method) ? method : null; + this.headers = isPresent(headers) ? headers : null; + this.body = isPresent(body) ? body : null; + this.mode = isPresent(mode) ? mode : null; + this.credentials = isPresent(credentials) ? credentials : null; + this.cache = isPresent(cache) ? cache : null; + this.url = isPresent(url) ? url : null; } /** * Creates a copy of the `RequestOptions` instance, using the optional input as values to override * existing values. */ - merge({url = null, method = null, headers = null, body = null, mode = null, credentials = null, - cache = null}: any = {}): RequestOptions { + merge(options?: IRequestOptions): RequestOptions { return new RequestOptions({ - method: isPresent(method) ? method : this.method, - headers: isPresent(headers) ? headers : this.headers, - body: isPresent(body) ? body : this.body, - mode: isPresent(mode) ? mode : this.mode, - credentials: isPresent(credentials) ? credentials : this.credentials, - cache: isPresent(cache) ? cache : this.cache, - url: isPresent(url) ? url : this.url + method: isPresent(options) && isPresent(options.method) ? options.method : this.method, + headers: isPresent(options) && isPresent(options.headers) ? options.headers : this.headers, + body: isPresent(options) && isPresent(options.body) ? options.body : this.body, + mode: isPresent(options) && isPresent(options.mode) ? options.mode : this.mode, + credentials: isPresent(options) && isPresent(options.credentials) ? options.credentials : + this.credentials, + cache: isPresent(options) && isPresent(options.cache) ? options.cache : this.cache, + url: isPresent(options) && isPresent(options.url) ? options.url : this.url }); } - - static fromStringWrapper(map: StringMap): RequestOptions { - return new RequestOptions({ - method: StringMapWrapper.get(map, 'method'), - headers: StringMapWrapper.get(map, 'headers'), - body: StringMapWrapper.get(map, 'body'), - mode: StringMapWrapper.get(map, 'mode'), - credentials: StringMapWrapper.get(map, 'credentials'), - cache: StringMapWrapper.get(map, 'cache'), - url:StringMapWrapper.get(map, 'url') - }) - } } /** - * Injectable version of {@link RequestOptions}. + * Injectable version of {@link RequestOptions}, with overridable default values. * * #Example * @@ -84,8 +71,8 @@ export class RequestOptions implements IRequestOptions { * ... * class MyComponent { * constructor(baseRequestOptions:BaseRequestOptions, http:Http) { - * var options = baseRequestOptions.merge({body: 'foobar'}); - * var request = new Request('https://foo', options); + * var options = baseRequestOptions.merge({body: 'foobar', url: 'https://foo'}); + * var request = new Request(options); * http.request(request).subscribe(res => this.bars = res.json()); * } * } @@ -94,5 +81,7 @@ export class RequestOptions implements IRequestOptions { */ @Injectable() export class BaseRequestOptions extends RequestOptions { - constructor() { super(); } + constructor() { + super({method: RequestMethods.GET, headers: new Headers(), mode: RequestModesOpts.Cors}); + } } diff --git a/modules/angular2/src/http/base_response_options.ts b/modules/angular2/src/http/base_response_options.ts index 94505e17f8..20352ee405 100644 --- a/modules/angular2/src/http/base_response_options.ts +++ b/modules/angular2/src/http/base_response_options.ts @@ -1,8 +1,54 @@ +import {Injectable} from 'angular2/di'; +import {isPresent, isJsObject} from 'angular2/src/facade/lang'; import {Headers} from './headers'; import {ResponseTypes} from './enums'; -import {ResponseOptions} from './interfaces'; +import {IResponseOptions} from './interfaces'; -export class BaseResponseOptions implements ResponseOptions { +/** + * Creates a response options object similar to the + * [ResponseInit](https://fetch.spec.whatwg.org/#responseinit) description + * in the Fetch + * Spec to be optionally provided when instantiating a + * {@link Response}. + * + * All values are null by default. + */ +export class ResponseOptions implements IResponseOptions { + // TODO: ArrayBuffer | FormData | Blob + body: string | Object; + status: number; + headers: Headers; + statusText: string; + type: ResponseTypes; + url: string; + constructor({body, status, headers, statusText, type, url}: IResponseOptions = {}) { + this.body = isPresent(body) ? body : null; + this.status = isPresent(status) ? status : null; + this.headers = isPresent(headers) ? headers : null; + this.statusText = isPresent(statusText) ? statusText : null; + this.type = isPresent(type) ? type : null; + this.url = isPresent(url) ? url : null; + } + + merge(options?: IResponseOptions): ResponseOptions { + return new ResponseOptions({ + body: isPresent(options) && isPresent(options.body) ? options.body : this.body, + status: isPresent(options) && isPresent(options.status) ? options.status : this.status, + headers: isPresent(options) && isPresent(options.headers) ? options.headers : this.headers, + statusText: isPresent(options) && isPresent(options.statusText) ? options.statusText : + this.statusText, + type: isPresent(options) && isPresent(options.type) ? options.type : this.type, + url: isPresent(options) && isPresent(options.url) ? options.url : this.url, + }); + } +} + +/** + * Injectable version of {@link ResponseOptions}, with overridable default values. + */ +@Injectable() +export class BaseResponseOptions extends ResponseOptions { + // TODO: Object | ArrayBuffer | JSON | FormData | Blob body: string | Object | ArrayBuffer | JSON | FormData | Blob; status: number; headers: Headers; @@ -11,11 +57,6 @@ export class BaseResponseOptions implements ResponseOptions { url: string; constructor() { - this.status = 200; - this.statusText = 'Ok'; - this.type = ResponseTypes.Default; - this.headers = new Headers(); + super({status: 200, statusText: 'Ok', type: ResponseTypes.Default, headers: new Headers()}); } } - -export var baseResponseOptions = new BaseResponseOptions(); diff --git a/modules/angular2/src/http/enums.ts b/modules/angular2/src/http/enums.ts index 92a1cc5cc5..e2da8affba 100644 --- a/modules/angular2/src/http/enums.ts +++ b/modules/angular2/src/http/enums.ts @@ -1,11 +1,19 @@ import {StringMap, StringMapWrapper} from 'angular2/src/facade/collection'; +/** + * Acceptable origin modes to be associated with a {@link Request}, based on + * [RequestMode](https://fetch.spec.whatwg.org/#requestmode) from the Fetch spec. + */ export enum RequestModesOpts { Cors, NoCors, SameOrigin } +/** + * Acceptable cache option to be associated with a {@link Request}, based on + * [RequestCache](https://fetch.spec.whatwg.org/#requestcache) from the Fetch spec. + */ export enum RequestCacheOpts { Default, NoStore, @@ -15,12 +23,19 @@ export enum RequestCacheOpts { OnlyIfCached } +/** + * Acceptable credentials option to be associated with a {@link Request}, based on + * [RequestCredentials](https://fetch.spec.whatwg.org/#requestcredentials) from the Fetch spec. + */ export enum RequestCredentialsOpts { Omit, SameOrigin, Include } +/** + * Supported http methods. + */ export enum RequestMethods { GET, POST, @@ -38,7 +53,11 @@ export class RequestMethodsMap { constructor() { this._methods = ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'HEAD', 'PATCH']; } getMethod(method: int): string { return this._methods[method]; } } - +/** + * All possible states in which a connection can be, based on + * [States](http://www.w3.org/TR/XMLHttpRequest/#states) from the `XMLHttpRequest` spec, but with an + * additional "CANCELLED" state. + */ export enum ReadyStates { UNSENT, OPEN, @@ -48,6 +67,10 @@ export enum ReadyStates { CANCELLED } +/** + * Acceptable response types to be associated with a {@link Response}, based on + * [ResponseType](https://fetch.spec.whatwg.org/#responsetype) from the Fetch spec. + */ export enum ResponseTypes { Basic, Cors, diff --git a/modules/angular2/src/http/headers.ts b/modules/angular2/src/http/headers.ts index 55314ee09c..ccab309d00 100644 --- a/modules/angular2/src/http/headers.ts +++ b/modules/angular2/src/http/headers.ts @@ -42,6 +42,9 @@ export class Headers { } } + /** + * Appends a header to existing list of header values for a given header name. + */ append(name: string, value: string): void { var mapName = this._headersMap.get(name); var list = isListLikeIterable(mapName) ? mapName : []; @@ -49,26 +52,36 @@ export class Headers { this._headersMap.set(name, list); } + /** + * Deletes all header values for the given name. + */ delete (name: string): void { MapWrapper.delete(this._headersMap, name); } forEach(fn: Function) { MapWrapper.forEach(this._headersMap, fn); } + /** + * Returns first header that matches given name. + */ get(header: string): string { return ListWrapper.first(this._headersMap.get(header)); } + /** + * Check for existence of header by given name. + */ has(header: string): boolean { return this._headersMap.has(header); } + /** + * Provides names of set headers + */ keys(): List { return MapWrapper.keys(this._headersMap); } + /** + * Sets or overrides header value for given name. + */ set(header: string, value: string | List): void { var list = []; - var isDart = false; - // Dart hack - if (list.toString().length === 2) { - isDart = true; - } + if (isListLikeIterable(value)) { - var pushValue = (>value).toString(); - if (isDart) pushValue = pushValue.substring(1, pushValue.length - 1); + var pushValue = (>value).join(','); list.push(pushValue); } else { list.push(value); @@ -77,12 +90,21 @@ export class Headers { this._headersMap.set(header, list); } + /** + * Returns values of all headers. + */ values(): List> { return MapWrapper.values(this._headersMap); } + /** + * Returns list of header values for a given name. + */ getAll(header: string): Array { var headers = this._headersMap.get(header); return isListLikeIterable(headers) ? headers : []; } + /** + * This method is not implemented. + */ entries() { throw new BaseException('"entries" method is not implemented on Headers class'); } } diff --git a/modules/angular2/src/http/http.ts b/modules/angular2/src/http/http.ts index e25ea763db..ef00babefc 100644 --- a/modules/angular2/src/http/http.ts +++ b/modules/angular2/src/http/http.ts @@ -13,12 +13,21 @@ function httpRequest(backend: ConnectionBackend, request: Request): EventEmitter function mergeOptions(defaultOpts, providedOpts, method, url): RequestOptions { var newOptions = defaultOpts; if (isPresent(providedOpts)) { - newOptions = newOptions.merge(providedOpts); + // Hack so Dart can used named parameters + newOptions = newOptions.merge(new RequestOptions({ + method: providedOpts.method, + url: providedOpts.url, + headers: providedOpts.headers, + body: providedOpts.body, + mode: providedOpts.mode, + credentials: providedOpts.credentials, + cache: providedOpts.cache + })); } if (isPresent(method)) { - return newOptions.merge({method: method, url: url}); + return newOptions.merge(new RequestOptions({method: method, url: url})); } else { - return newOptions.merge({url: url}); + return newOptions.merge(new RequestOptions({url: url})); } } @@ -26,9 +35,8 @@ function mergeOptions(defaultOpts, providedOpts, method, url): RequestOptions { * Performs http requests using `XMLHttpRequest` as the default backend. * * `Http` is available as an injectable class, with methods to perform http requests. Calling - * `request` returns an - * [Observable](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/observable.md), - * which will emit a single {@link Response} when a response is + * `request` returns an {@link EventEmitter} which will emit a single {@link Response} when a + *response is * received. * * #Example @@ -73,7 +81,7 @@ function mergeOptions(defaultOpts, providedOpts, method, url): RequestOptions { **/ @Injectable() export class Http { - constructor(private _backend: ConnectionBackend, private _defaultOptions: BaseRequestOptions) {} + constructor(private _backend: ConnectionBackend, private _defaultOptions: RequestOptions) {} /** * Performs any type of http request. First argument is required, and can either be a url or @@ -96,7 +104,7 @@ export class Http { /** * Performs a request with `get` http method. */ - get(url: string, options?: IRequestOptions) { + get(url: string, options?: IRequestOptions): EventEmitter { return httpRequest(this._backend, new Request(mergeOptions(this._defaultOptions, options, RequestMethods.GET, url))); } @@ -104,25 +112,27 @@ export class Http { /** * Performs a request with `post` http method. */ - post(url: string, body: string, options?: IRequestOptions) { - return httpRequest(this._backend, - new Request(mergeOptions(this._defaultOptions.merge({body: body}), options, - RequestMethods.POST, url))); + post(url: string, body: string, options?: IRequestOptions): EventEmitter { + return httpRequest( + this._backend, + new Request(mergeOptions(this._defaultOptions.merge(new RequestOptions({body: body})), + options, RequestMethods.POST, url))); } /** * Performs a request with `put` http method. */ - put(url: string, body: string, options?: IRequestOptions) { - return httpRequest(this._backend, - new Request(mergeOptions(this._defaultOptions.merge({body: body}), options, - RequestMethods.PUT, url))); + put(url: string, body: string, options?: IRequestOptions): EventEmitter { + return httpRequest( + this._backend, + new Request(mergeOptions(this._defaultOptions.merge(new RequestOptions({body: body})), + options, RequestMethods.PUT, url))); } /** * Performs a request with `delete` http method. */ - delete (url: string, options?: IRequestOptions) { + delete (url: string, options?: IRequestOptions): EventEmitter { return httpRequest(this._backend, new Request(mergeOptions(this._defaultOptions, options, RequestMethods.DELETE, url))); } @@ -130,16 +140,17 @@ export class Http { /** * Performs a request with `patch` http method. */ - patch(url: string, body: string, options?: IRequestOptions) { - return httpRequest(this._backend, - new Request(mergeOptions(this._defaultOptions.merge({body: body}), options, - RequestMethods.PATCH, url))); + patch(url: string, body: string, options?: IRequestOptions): EventEmitter { + return httpRequest( + this._backend, + new Request(mergeOptions(this._defaultOptions.merge(new RequestOptions({body: body})), + options, RequestMethods.PATCH, url))); } /** * Performs a request with `head` http method. */ - head(url: string, options?: IRequestOptions) { + head(url: string, options?: IRequestOptions): EventEmitter { return httpRequest(this._backend, new Request(mergeOptions(this._defaultOptions, options, RequestMethods.HEAD, url))); } diff --git a/modules/angular2/src/http/interfaces.ts b/modules/angular2/src/http/interfaces.ts index 0b832dbdd9..d796542382 100644 --- a/modules/angular2/src/http/interfaces.ts +++ b/modules/angular2/src/http/interfaces.ts @@ -13,18 +13,31 @@ import {BaseException} from 'angular2/src/facade/lang'; import {EventEmitter} from 'angular2/src/facade/async'; import {Request} from './static_request'; +/** + * Abstract class from which real backends are derived. + * + * The primary purpose of a `ConnectionBackend` is to create new connections to fulfill a given + * {@link Request}. + */ export class ConnectionBackend { constructor() {} createConnection(request: any): Connection { throw new BaseException('Abstract!'); } } +/** + * Abstract class from which real connections are derived. + */ export class Connection { readyState: ReadyStates; request: Request; - response: EventEmitter; //; + response: EventEmitter; // TODO: generic of ; dispose(): void { throw new BaseException('Abstract!'); } } +/** + * Interface for options to construct a Request, based on + * [RequestInit](https://fetch.spec.whatwg.org/#requestinit) from the Fetch spec. + */ export interface IRequestOptions { url?: string; method?: RequestMethods; @@ -36,27 +49,16 @@ export interface IRequestOptions { cache?: RequestCacheOpts; } -export interface ResponseOptions { +/** + * Interface for options to construct a Response, based on + * [ResponseInit](https://fetch.spec.whatwg.org/#responseinit) from the Fetch spec. + */ +export interface IResponseOptions { // TODO: Support Blob, ArrayBuffer, JSON body?: string | Object | FormData; status?: number; statusText?: string; - headers?: Headers | Object; + headers?: Headers; type?: ResponseTypes; url?: string; } - -export interface IResponse { - headers: Headers; - ok: boolean; - status: number; - statusText: string; - type: ResponseTypes; - url: string; - totalBytes: number; - bytesLoaded: number; - blob(): any; // TODO: Blob - arrayBuffer(): any; // TODO: ArrayBuffer - text(): string; - json(): Object; -} diff --git a/modules/angular2/src/http/static_request.ts b/modules/angular2/src/http/static_request.ts index 063b750d31..e246a5ecb4 100644 --- a/modules/angular2/src/http/static_request.ts +++ b/modules/angular2/src/http/static_request.ts @@ -1,13 +1,17 @@ import {RequestMethods, RequestModesOpts, RequestCredentialsOpts, RequestCacheOpts} from './enums'; import {RequestOptions} from './base_request_options'; -import {IRequestOptions} from './interfaces'; import {Headers} from './headers'; -import {BaseException, RegExpWrapper, CONST_EXPR, isPresent} from 'angular2/src/facade/lang'; -import {StringMap, StringMapWrapper} from 'angular2/src/facade/collection'; +import { + BaseException, + RegExpWrapper, + CONST_EXPR, + isPresent, + isJsObject +} from 'angular2/src/facade/lang'; // TODO(jeffbcross): properly implement body accessors /** - * Creates `Request` instances with default values. + * Creates `Request` instances from provided values. * * The Request's interface is inspired by the Request constructor defined in the [Fetch * Spec](https://fetch.spec.whatwg.org/#request-class), @@ -33,12 +37,8 @@ export class Request { // TODO: support URLSearchParams | FormData | Blob | ArrayBuffer private _body: string; cache: RequestCacheOpts; - // TODO(jeffbcross): determine way to add type to destructured args - constructor(options?: IRequestOptions) { - var requestOptions: RequestOptions = options instanceof - StringMap ? RequestOptions.fromStringWrapper(>options) : - options; - + constructor(requestOptions: RequestOptions) { + // TODO: assert that url is present this.url = requestOptions.url; this._body = requestOptions.body; this.method = requestOptions.method; @@ -47,10 +47,11 @@ export class Request { // Defaults to 'omit', consistent with browser // TODO(jeffbcross): implement behavior this.credentials = requestOptions.credentials; - this.headers = requestOptions.headers; + this.headers = new Headers(requestOptions.headers); this.cache = requestOptions.cache; } + /** * Returns the request's body as string, assuming that body exists. If body is undefined, return * empty diff --git a/modules/angular2/src/http/static_response.ts b/modules/angular2/src/http/static_response.ts index 941f26162a..dc0516dc04 100644 --- a/modules/angular2/src/http/static_response.ts +++ b/modules/angular2/src/http/static_response.ts @@ -1,13 +1,17 @@ -import {IResponse, ResponseOptions} from './interfaces'; import {ResponseTypes} from './enums'; -import {baseResponseOptions} from './base_response_options'; -import {BaseException, isJsObject, isString, isPresent, Json} from 'angular2/src/facade/lang'; +import { + BaseException, + CONST_EXPR, + isJsObject, + isString, + isPresent, + Json +} from 'angular2/src/facade/lang'; import {Headers} from './headers'; +import {ResponseOptions} from './base_response_options'; -// TODO: make this injectable so baseResponseOptions can be overridden, mostly for the benefit of -// headers merging. /** - * Creates `Response` instances with default values. + * Creates `Response` instances from provided values. * * Though this object isn't * usually instantiated by end-users, it is the primary object interacted with when it comes time to @@ -19,12 +23,12 @@ import {Headers} from './headers'; * http.request('my-friends.txt').subscribe(response => this.friends = response.text()); * ``` * - * The Response's interface is inspired by the Request constructor defined in the [Fetch + * The Response's interface is inspired by the Response constructor defined in the [Fetch * Spec](https://fetch.spec.whatwg.org/#response-class), but is considered a static value whose body * can be accessed many times. There are other differences in the implementation, but this is the * most significant. */ -export class Response implements IResponse { +export class Response { /** * One of "basic", "cors", "default", "error, or "opaque". * @@ -74,16 +78,13 @@ export class Response implements IResponse { headers: Headers; // TODO: Support ArrayBuffer, JSON, FormData, Blob private _body: string | Object; - constructor({body, status, statusText, headers, type, url}: ResponseOptions = {}) { - if (isJsObject(headers)) { - headers = new Headers(headers); - } - this._body = isPresent(body) ? body : baseResponseOptions.body; - this.status = isPresent(status) ? status : baseResponseOptions.status; - this.statusText = isPresent(statusText) ? statusText : baseResponseOptions.statusText; - this.headers = isPresent(headers) ? headers : baseResponseOptions.headers; - this.type = isPresent(type) ? type : baseResponseOptions.type; - this.url = isPresent(url) ? url : baseResponseOptions.url; + constructor(responseOptions: ResponseOptions) { + this._body = responseOptions.body; + this.status = responseOptions.status; + this.statusText = responseOptions.statusText; + this.headers = responseOptions.headers; + this.type = responseOptions.type; + this.url = responseOptions.url; } /** diff --git a/modules/angular2/src/http/url_search_params.ts b/modules/angular2/src/http/url_search_params.ts index 2468fb44b5..1481a5c958 100644 --- a/modules/angular2/src/http/url_search_params.ts +++ b/modules/angular2/src/http/url_search_params.ts @@ -21,6 +21,11 @@ function paramParser(rawParams: string): Map> { return map; } +/** + * Map-like representation of url search parameters, based on + * [URLSearchParams](https://url.spec.whatwg.org/#urlsearchparams) in the url living standard. + * + */ export class URLSearchParams { paramsMap: Map>; constructor(public rawParams: string) { this.paramsMap = paramParser(rawParams); } diff --git a/modules/angular2/test/http/backends/xhr_backend_spec.ts b/modules/angular2/test/http/backends/xhr_backend_spec.ts index 4c3decf106..43fce548f0 100644 --- a/modules/angular2/test/http/backends/xhr_backend_spec.ts +++ b/modules/angular2/test/http/backends/xhr_backend_spec.ts @@ -1,5 +1,6 @@ import { AsyncTestCompleter, + afterEach, beforeEach, ddescribe, describe, @@ -10,35 +11,47 @@ import { xit, SpyObject } from 'angular2/test_lib'; -import {BrowserXHR} from 'angular2/src/http/backends/browser_xhr'; +import {ObservableWrapper} from 'angular2/src/facade/async'; +import {BrowserXhr} from 'angular2/src/http/backends/browser_xhr'; import {XHRConnection, XHRBackend} from 'angular2/src/http/backends/xhr_backend'; import {bind, Injector} from 'angular2/di'; import {Request} from 'angular2/src/http/static_request'; -import {StringMapWrapper} from 'angular2/src/facade/collection'; -import {RequestOptions} from 'angular2/src/http/base_request_options'; +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 {ResponseTypes} from 'angular2/src/http/enums'; var abortSpy; var sendSpy; var openSpy; var addEventListenerSpy; +var existingXHRs = []; -class MockBrowserXHR extends BrowserXHR { +class MockBrowserXHR extends BrowserXhr { abort: any; send: any; open: any; - addEventListener: any; response: any; responseText: string; + callbacks: Map; constructor() { super(); var spy = new SpyObject(); this.abort = abortSpy = spy.spy('abort'); this.send = sendSpy = spy.spy('send'); this.open = openSpy = spy.spy('open'); - this.addEventListener = addEventListenerSpy = spy.spy('addEventListener'); + this.callbacks = new Map(); } - build() { return new MockBrowserXHR(); } + addEventListener(type: string, cb: Function) { this.callbacks.set(type, cb); } + + dispatchEvent(type: string) { this.callbacks.get(type)({}); } + + build() { + var xhr = new MockBrowserXHR(); + existingXHRs.push(xhr); + return xhr; + } } export function main() { @@ -47,17 +60,35 @@ export function main() { var sampleRequest; beforeEach(() => { - var injector = - Injector.resolveAndCreate([bind(BrowserXHR).toClass(MockBrowserXHR), XHRBackend]); + var injector = Injector.resolveAndCreate([ + bind(ResponseOptions) + .toClass(BaseResponseOptions), + bind(BrowserXhr).toClass(MockBrowserXHR), + XHRBackend + ]); backend = injector.get(XHRBackend); - sampleRequest = new Request(new RequestOptions({url: 'https://google.com'})); + var base = new BaseRequestOptions(); + sampleRequest = new Request(base.merge(new RequestOptions({url: 'https://google.com'}))); }); + afterEach(() => { existingXHRs = []; }); + it('should create a connection', () => { expect(() => backend.createConnection(sampleRequest)).not.toThrow(); }); describe('XHRConnection', () => { + it('should use the injected BaseResponseOptions to create the response', + inject([AsyncTestCompleter], async => { + var connection = new XHRConnection(sampleRequest, new MockBrowserXHR(), + new ResponseOptions({type: ResponseTypes.Error})); + ObservableWrapper.subscribe(connection.response, res => { + expect(res.type).toBe(ResponseTypes.Error); + async.done(); + }); + existingXHRs[0].dispatchEvent('load'); + })); + it('should call abort when disposed', () => { var connection = new XHRConnection(sampleRequest, new MockBrowserXHR()); connection.dispose(); @@ -73,7 +104,9 @@ export function main() { it('should automatically call send on the backend with request body', () => { var body = 'Some body to love'; - new XHRConnection(new Request(new RequestOptions({body: body})), new MockBrowserXHR()); + var base = new BaseRequestOptions(); + new XHRConnection(new Request(base.merge(new RequestOptions({body: body}))), + new MockBrowserXHR()); expect(sendSpy).toHaveBeenCalledWith(body); }); }); diff --git a/modules/angular2/test/http/base_request_options_spec.ts b/modules/angular2/test/http/base_request_options_spec.ts index fe24c0003c..5ef109579f 100644 --- a/modules/angular2/test/http/base_request_options_spec.ts +++ b/modules/angular2/test/http/base_request_options_spec.ts @@ -9,22 +9,22 @@ import { it, xit } from 'angular2/test_lib'; -import {BaseRequestOptions} from 'angular2/src/http/base_request_options'; +import {BaseRequestOptions, RequestOptions} from 'angular2/src/http/base_request_options'; import {RequestMethods, RequestModesOpts} from 'angular2/src/http/enums'; export function main() { describe('BaseRequestOptions', () => { it('should create a new object when calling merge', () => { var options1 = new BaseRequestOptions(); - var options2 = options1.merge({method: RequestMethods.DELETE}); + var options2 = options1.merge(new RequestOptions({method: RequestMethods.DELETE})); expect(options2).not.toBe(options1); expect(options2.method).toBe(RequestMethods.DELETE); }); it('should retain previously merged values when merging again', () => { var options1 = new BaseRequestOptions(); - var options2 = options1.merge({method: RequestMethods.DELETE}); - var options3 = options2.merge({mode: RequestModesOpts.NoCors}); + var options2 = options1.merge(new RequestOptions({method: RequestMethods.DELETE})); + var options3 = options2.merge(new RequestOptions({mode: RequestModesOpts.NoCors})); expect(options3.mode).toBe(RequestModesOpts.NoCors); expect(options3.method).toBe(RequestMethods.DELETE); }); diff --git a/modules/angular2/test/http/http_spec.ts b/modules/angular2/test/http/http_spec.ts index fb97d7aabf..ccadc4e417 100644 --- a/modules/angular2/test/http/http_spec.ts +++ b/modules/angular2/test/http/http_spec.ts @@ -17,6 +17,7 @@ import {MockBackend} from 'angular2/src/http/backends/mock_backend'; import {Response} from 'angular2/src/http/static_response'; import {RequestMethods} from 'angular2/src/http/enums'; import {BaseRequestOptions, RequestOptions} from 'angular2/src/http/base_request_options'; +import {ResponseOptions} from 'angular2/src/http/base_response_options'; import {Request} from 'angular2/src/http/static_request'; import {EventEmitter, ObservableWrapper} from 'angular2/src/facade/async'; import {ConnectionBackend} from 'angular2/src/http/interfaces'; @@ -52,7 +53,7 @@ export function main() { ]); http = injector.get(Http); backend = injector.get(MockBackend); - baseResponse = new Response({body: 'base response'}); + baseResponse = new Response(new ResponseOptions({body: 'base response'})); }); afterEach(() => backend.verifyNoPendingRequests()); @@ -67,7 +68,7 @@ export function main() { inject([AsyncTestCompleter], (async) => { ObservableWrapper.subscribe(backend.connections, c => { expect(c.request.url).toBe('https://google.com'); - c.mockRespond(new Response({body: 'Thank you'})); + c.mockRespond(new Response(new ResponseOptions({body: 'Thank you'}))); async.done(); }); ObservableWrapper.subscribe( diff --git a/modules/examples/src/http/http_comp.ts b/modules/examples/src/http/http_comp.ts index 3dbd2fed37..4485440798 100644 --- a/modules/examples/src/http/http_comp.ts +++ b/modules/examples/src/http/http_comp.ts @@ -1,6 +1,6 @@ -import {bootstrap, Component, View, NgFor, Inject} from 'angular2/angular2'; +import {Component, View, NgFor} from 'angular2/angular2'; +import {Http} from 'angular2/http'; import {ObservableWrapper} from 'angular2/src/facade/async'; -import {Http, httpInjectables} from 'angular2/http'; @Component({selector: 'http-app'}) @View({ @@ -19,4 +19,4 @@ export class HttpCmp { constructor(http: Http) { ObservableWrapper.subscribe(http.get('./people.json'), res => this.people = res.json()); } -} \ No newline at end of file +} diff --git a/modules/examples/src/http/index.ts b/modules/examples/src/http/index.ts index f2a25ad79c..d00a1ff537 100644 --- a/modules/examples/src/http/index.ts +++ b/modules/examples/src/http/index.ts @@ -1,8 +1,6 @@ /// -import { - bootstrap, -} from 'angular2/angular2'; +import {bootstrap} from 'angular2/angular2'; import {reflector} from 'angular2/src/reflection/reflection'; import {ReflectionCapabilities} from 'angular2/src/reflection/reflection_capabilities'; import {httpInjectables} from 'angular2/http';