From 21568106b114943b59ce37832d82c8fb9a63285b Mon Sep 17 00:00:00 2001 From: Jeff Cross Date: Tue, 28 Apr 2015 23:07:55 -0700 Subject: [PATCH] feat(http): add basic http service This implementation only works in JavaScript, while the Observable transpilation story gets worked out. Right now, the service just makes a simple request, and returns an Observable of Response. Additional functionality will be captured in separate issues. Fixes #2028 --- gulpfile.js | 10 ++ modules/angular2/http.ts | 11 ++ modules/angular2/src/facade/async.ts | 2 +- modules/angular2/src/facade/collection.ts | 1 + .../src/http/backends/browser_xhr.dart | 9 ++ .../angular2/src/http/backends/browser_xhr.ts | 9 ++ .../src/http/backends/mock_backend.ts | 106 ++++++++++++++++++ .../angular2/src/http/backends/xhr_backend.ts | 40 +++++++ .../angular2/src/http/base_request_options.ts | 21 ++++ .../src/http/base_response_options.ts | 23 ++++ modules/angular2/src/http/enums.ts | 12 ++ modules/angular2/src/http/headers.ts | 79 +++++++++++++ modules/angular2/src/http/http.ts | 68 +++++++++++ modules/angular2/src/http/interfaces.ts | 63 +++++++++++ modules/angular2/src/http/static_request.ts | 39 +++++++ modules/angular2/src/http/static_response.ts | 47 ++++++++ .../angular2/src/http/url_search_params.ts | 45 ++++++++ .../test/http/backends/xhr_backend_spec.ts | 78 +++++++++++++ modules/angular2/test/http/headers_spec.ts | 66 +++++++++++ modules/angular2/test/http/http_spec.ts | 96 ++++++++++++++++ .../test/http/url_search_params_spec.ts | 35 ++++++ modules/examples/e2e_test/http/http_spec.ts | 23 ++++ .../src/http/assign_local_directive.ts | 20 ++++ modules/examples/src/http/http_comp.ts | 30 +++++ modules/examples/src/http/index.html | 11 ++ modules/examples/src/http/index.ts | 30 +++++ modules/examples/src/http/index_dynamic.html | 11 ++ modules/examples/src/http/index_dynamic.ts | 15 +++ modules/examples/src/http/people.json | 1 + modules/examples/src/http/rx_pipe.ts | 38 +++++++ protractor-dart2js.conf.js | 3 + scripts/ci/test_e2e_dart.sh | 1 + scripts/ci/test_e2e_js.sh | 1 + tools/broccoli/trees/browser_tree.ts | 1 + tools/broccoli/trees/dart_tree.ts | 11 +- 35 files changed, 1054 insertions(+), 2 deletions(-) create mode 100644 modules/angular2/http.ts create mode 100644 modules/angular2/src/http/backends/browser_xhr.dart create mode 100644 modules/angular2/src/http/backends/browser_xhr.ts create mode 100644 modules/angular2/src/http/backends/mock_backend.ts create mode 100644 modules/angular2/src/http/backends/xhr_backend.ts create mode 100644 modules/angular2/src/http/base_request_options.ts create mode 100644 modules/angular2/src/http/base_response_options.ts create mode 100644 modules/angular2/src/http/enums.ts create mode 100644 modules/angular2/src/http/headers.ts create mode 100644 modules/angular2/src/http/http.ts create mode 100644 modules/angular2/src/http/interfaces.ts create mode 100644 modules/angular2/src/http/static_request.ts create mode 100644 modules/angular2/src/http/static_response.ts create mode 100644 modules/angular2/src/http/url_search_params.ts create mode 100644 modules/angular2/test/http/backends/xhr_backend_spec.ts create mode 100644 modules/angular2/test/http/headers_spec.ts create mode 100644 modules/angular2/test/http/http_spec.ts create mode 100644 modules/angular2/test/http/url_search_params_spec.ts create mode 100644 modules/examples/e2e_test/http/http_spec.ts create mode 100644 modules/examples/src/http/assign_local_directive.ts create mode 100644 modules/examples/src/http/http_comp.ts create mode 100644 modules/examples/src/http/index.html create mode 100644 modules/examples/src/http/index.ts create mode 100644 modules/examples/src/http/index_dynamic.html create mode 100644 modules/examples/src/http/index_dynamic.ts create mode 100644 modules/examples/src/http/people.json create mode 100644 modules/examples/src/http/rx_pipe.ts diff --git a/gulpfile.js b/gulpfile.js index eecf3c7ad5..3679f57363 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -845,6 +845,16 @@ gulp.task('!build/change_detect.dart', function(done) { proc.stdout.pipe(dartStream); }); +// ------------ +// additional tasks for building examples +gulp.task('build.http.example', function() { + //Copy over people.json used in http example + return gulp.src('modules/examples/src/http/people.json') + .pipe(gulp.dest(CONFIG.dest.js.prod.es5 + '/examples/src/http/')) + .pipe(gulp.dest(CONFIG.dest.js.dev.es5 + '/examples/src/http/')) + .pipe(gulp.dest(CONFIG.dest.js.dart2js + '/examples/src/http/')); +}); + // ------------ // angular material testing rules gulp.task('build.css.material', function() { diff --git a/modules/angular2/http.ts b/modules/angular2/http.ts new file mode 100644 index 0000000000..2eb5112e1f --- /dev/null +++ b/modules/angular2/http.ts @@ -0,0 +1,11 @@ +import {bind, Binding} from 'angular2/di'; +import {Http, HttpFactory} from './src/http/http'; +import {XHRBackend} from 'angular2/src/http/backends/xhr_backend'; +import {BrowserXHR} from 'angular2/src/http/backends/browser_xhr'; + +export {Http}; +export var httpInjectables: List = [ + XHRBackend, + bind(BrowserXHR).toValue(BrowserXHR), + bind(Http).toFactory(HttpFactory, [XHRBackend]) +]; diff --git a/modules/angular2/src/facade/async.ts b/modules/angular2/src/facade/async.ts index 17fca33d76..cc4a195bfb 100644 --- a/modules/angular2/src/facade/async.ts +++ b/modules/angular2/src/facade/async.ts @@ -107,5 +107,5 @@ export class EventEmitter extends Observable { throw(error) { this._subject.onError(error); } - return (value) { this._subject.onCompleted(); } + return (value?) { this._subject.onCompleted(); } } diff --git a/modules/angular2/src/facade/collection.ts b/modules/angular2/src/facade/collection.ts index 5c2ed52ee3..08660ddac8 100644 --- a/modules/angular2/src/facade/collection.ts +++ b/modules/angular2/src/facade/collection.ts @@ -238,6 +238,7 @@ export class ListWrapper { l.sort(); } } + static toString(l: List): string { return l.toString(); } } export function isListLikeIterable(obj): boolean { diff --git a/modules/angular2/src/http/backends/browser_xhr.dart b/modules/angular2/src/http/backends/browser_xhr.dart new file mode 100644 index 0000000000..3a099712c3 --- /dev/null +++ b/modules/angular2/src/http/backends/browser_xhr.dart @@ -0,0 +1,9 @@ +library angular2.src.http.backends.browser_xhr; + +/// import 'dart:html' show HttpRequest; +/// import 'package:angular2/di.dart'; + +/// @Injectable() +/// class BrowserXHR { +/// factory BrowserXHR() => new HttpRequest(); +/// } diff --git a/modules/angular2/src/http/backends/browser_xhr.ts b/modules/angular2/src/http/backends/browser_xhr.ts new file mode 100644 index 0000000000..cbb67164de --- /dev/null +++ b/modules/angular2/src/http/backends/browser_xhr.ts @@ -0,0 +1,9 @@ +declare var window; + +import {Injectable} from 'angular2/di'; + +// Make sure not to evaluate this in a non-browser environment! +@Injectable() +export class BrowserXHR { + constructor() { return (new window.XMLHttpRequest()); } +} diff --git a/modules/angular2/src/http/backends/mock_backend.ts b/modules/angular2/src/http/backends/mock_backend.ts new file mode 100644 index 0000000000..9d370f2130 --- /dev/null +++ b/modules/angular2/src/http/backends/mock_backend.ts @@ -0,0 +1,106 @@ +import {Injectable} from 'angular2/di'; +import {Request} from 'angular2/src/http/static_request'; +import {Response} from 'angular2/src/http/static_response'; +import {ReadyStates} from 'angular2/src/http/enums'; +import * as Rx from 'rx'; + +/** + * Connection represents a request and response for an underlying transport, like XHR or mock. + * The mock implementation contains helper methods to respond to connections within tests. + * API subject to change and expand. + **/ +export class Connection { + /** + * Observer to call on download progress, if provided in config. + **/ + downloadObserver: Rx.Observer; + + /** + * TODO + * Name `readyState` should change to be more generic, and states could be made to be more + * descriptive than XHR states. + **/ + + readyState: ReadyStates; + request: Request; + response: Rx.Subject; + + constructor(req: Request) { + // State + if (Rx.hasOwnProperty('default')) { + this.response = new ((Rx).default.Rx.Subject)(); + } else { + this.response = new Rx.Subject(); + } + + this.readyState = ReadyStates.OPEN; + this.request = req; + this.dispose = this.dispose.bind(this); + } + + dispose() { + if (this.readyState !== ReadyStates.DONE) { + this.readyState = ReadyStates.CANCELLED; + } + } + + /** + * Called after a connection has been established. + **/ + mockRespond(res: Response) { + if (this.readyState >= ReadyStates.DONE) { + throw new Error('Connection has already been resolved'); + } + this.readyState = ReadyStates.DONE; + this.response.onNext(res); + this.response.onCompleted(); + } + + mockDownload(res: Response) { + this.downloadObserver.onNext(res); + if (res.bytesLoaded === res.totalBytes) { + this.downloadObserver.onCompleted(); + } + } + + mockError(err?) { + // Matches XHR semantics + this.readyState = ReadyStates.DONE; + this.response.onError(err); + this.response.onCompleted(); + } +} + +@Injectable() +export class MockBackend { + connections: Rx.Subject; + connectionsArray: Array; + pendingConnections: Rx.Observable; + constructor() { + this.connectionsArray = []; + if (Rx.hasOwnProperty('default')) { + this.connections = new (Rx).default.Rx.Subject(); + } else { + this.connections = new Rx.Subject(); + } + this.connections.subscribe(connection => this.connectionsArray.push(connection)); + this.pendingConnections = this.connections.filter((c) => c.readyState < ReadyStates.DONE); + } + + verifyNoPendingRequests() { + let pending = 0; + this.pendingConnections.subscribe((c) => pending++); + if (pending > 0) throw new Error(`${pending} pending connections to be resolved`); + } + + resolveAllConnections() { this.connections.subscribe((c) => c.readyState = 4); } + + createConnection(req: Request) { + if (!req || !(req instanceof Request)) { + throw new Error(`createConnection requires an instance of Request, got ${req}`); + } + let connection = new Connection(req); + this.connections.onNext(connection); + return connection; + } +} diff --git a/modules/angular2/src/http/backends/xhr_backend.ts b/modules/angular2/src/http/backends/xhr_backend.ts new file mode 100644 index 0000000000..258d8505a8 --- /dev/null +++ b/modules/angular2/src/http/backends/xhr_backend.ts @@ -0,0 +1,40 @@ +import {ConnectionBackend, Connection} from '../interfaces'; +import {ReadyStates, RequestMethods} from '../enums'; +import {Request} from '../static_request'; +import {Response} from '../static_response'; +import {Inject} from 'angular2/di'; +import {Injectable} from 'angular2/di'; +import {BrowserXHR} from './browser_xhr'; +import * as Rx from 'rx'; + +export class XHRConnection implements Connection { + request: Request; + response: Rx.Subject; + readyState: ReadyStates; + private _xhr; + constructor(req: Request, NativeConstruct: any) { + this.request = req; + if (Rx.hasOwnProperty('default')) { + this.response = new (Rx).default.Rx.Subject(); + } else { + this.response = new Rx.Subject(); + } + this._xhr = new NativeConstruct(); + this._xhr.open(RequestMethods[req.method], req.url); + this._xhr.addEventListener( + 'load', + () => {this.response.onNext(new Response(this._xhr.response || this._xhr.responseText))}); + // TODO(jeffbcross): make this more dynamic based on body type + this._xhr.send(this.request.text()); + } + + dispose(): void { this._xhr.abort(); } +} + +@Injectable() +export class XHRBackend implements ConnectionBackend { + constructor(private _NativeConstruct: BrowserXHR) {} + createConnection(request: Request): XHRConnection { + return new XHRConnection(request, this._NativeConstruct); + } +} diff --git a/modules/angular2/src/http/base_request_options.ts b/modules/angular2/src/http/base_request_options.ts new file mode 100644 index 0000000000..77b0165cde --- /dev/null +++ b/modules/angular2/src/http/base_request_options.ts @@ -0,0 +1,21 @@ +import {CONST_EXPR, CONST} from 'angular2/src/facade/lang'; +import {Headers} from './headers'; +import {URLSearchParams} from './url_search_params'; +import {RequestModesOpts, RequestMethods, RequestCacheOpts, RequestCredentialsOpts} from './enums'; +import {RequestOptions} from './interfaces'; +import {Injectable} from 'angular2/di'; + +@Injectable() +export class BaseRequestOptions implements RequestOptions { + method: RequestMethods; + headers: Headers; + body: URLSearchParams | FormData | string; + mode: RequestModesOpts; + credentials: RequestCredentialsOpts; + cache: RequestCacheOpts; + + constructor() { + this.method = RequestMethods.GET; + this.mode = RequestModesOpts.Cors; + } +} diff --git a/modules/angular2/src/http/base_response_options.ts b/modules/angular2/src/http/base_response_options.ts new file mode 100644 index 0000000000..e82ac36aee --- /dev/null +++ b/modules/angular2/src/http/base_response_options.ts @@ -0,0 +1,23 @@ +import {Headers} from './headers'; +import {ResponseTypes} from './enums'; +import {ResponseOptions} from './interfaces'; + +export class BaseResponseOptions implements ResponseOptions { + status: number; + headers: Headers | Object; + statusText: string; + type: ResponseTypes; + url: string; + + constructor({status = 200, statusText = 'Ok', type = ResponseTypes.Default, + headers = new Headers(), url = ''}: ResponseOptions = {}) { + this.status = status; + this.statusText = statusText; + this.type = type; + this.headers = headers; + this.url = url; + } +} +; + +export var baseResponseOptions = Object.freeze(new BaseResponseOptions()); diff --git a/modules/angular2/src/http/enums.ts b/modules/angular2/src/http/enums.ts new file mode 100644 index 0000000000..fac36b4fca --- /dev/null +++ b/modules/angular2/src/http/enums.ts @@ -0,0 +1,12 @@ + +export enum RequestModesOpts { Cors, NoCors, SameOrigin }; + +export enum RequestCacheOpts { Default, NoStore, Reload, NoCache, ForceCache, OnlyIfCached }; + +export enum RequestCredentialsOpts { Omit, SameOrigin, Include }; + +export enum RequestMethods { GET, POST, PUT, DELETE, OPTIONS, HEAD }; + +export enum ReadyStates { UNSENT, OPEN, HEADERS_RECEIVED, LOADING, DONE, CANCELLED }; + +export enum ResponseTypes { Basic, Cors, Default, Error, Opaque } diff --git a/modules/angular2/src/http/headers.ts b/modules/angular2/src/http/headers.ts new file mode 100644 index 0000000000..88efcae7e3 --- /dev/null +++ b/modules/angular2/src/http/headers.ts @@ -0,0 +1,79 @@ +import { + isPresent, + isBlank, + isJsObject, + isType, + StringWrapper, + BaseException +} from 'angular2/src/facade/lang'; +import { + isListLikeIterable, + List, + Map, + MapWrapper, + ListWrapper +} from 'angular2/src/facade/collection'; + +// (@jeffbcross): This is implemented mostly to spec, except that the entries method has been +// removed because it doesn't exist in dart, and it doesn't seem worth adding it to the facade. + +export class Headers { + _headersMap: Map>; + constructor(headers?: Headers | Object) { + if (isBlank(headers)) { + this._headersMap = MapWrapper.create(); + return; + } + + if (isPresent((headers)._headersMap)) { + this._headersMap = (headers)._headersMap; + } else if (isJsObject(headers)) { + this._headersMap = MapWrapper.createFromStringMap(headers); + MapWrapper.forEach(this._headersMap, (v, k) => { + if (!isListLikeIterable(v)) { + var list = ListWrapper.create(); + ListWrapper.push(list, v); + MapWrapper.set(this._headersMap, k, list); + } + }); + } + } + + append(name: string, value: string): void { + var list = MapWrapper.get(this._headersMap, name) || ListWrapper.create(); + ListWrapper.push(list, value); + MapWrapper.set(this._headersMap, name, list); + } + + delete (name: string): void { MapWrapper.delete(this._headersMap, name); } + + forEach(fn: Function) { return MapWrapper.forEach(this._headersMap, fn); } + + get(header: string): string { + return ListWrapper.first(MapWrapper.get(this._headersMap, header)); + } + + has(header: string) { return MapWrapper.contains(this._headersMap, header); } + + keys() { return MapWrapper.keys(this._headersMap); } + + // TODO: this implementation seems wrong. create list then check if it's iterable? + set(header: string, value: string | List): void { + var list = ListWrapper.create(); + if (!isListLikeIterable(value)) { + ListWrapper.push(list, value); + } else { + ListWrapper.push(list, ListWrapper.toString((>value))); + } + + MapWrapper.set(this._headersMap, header, list); + } + + values() { return MapWrapper.values(this._headersMap); } + + getAll(header: string): Array { + return MapWrapper.get(this._headersMap, header) || ListWrapper.create(); + } + + 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 new file mode 100644 index 0000000000..b4e1da0423 --- /dev/null +++ b/modules/angular2/src/http/http.ts @@ -0,0 +1,68 @@ +/// + +import {Injectable} from 'angular2/src/di/decorators'; +import {RequestOptions, Connection} from './interfaces'; +import {Request} from './static_request'; +import {Response} from './static_response'; +import {XHRBackend} from './backends/xhr_backend'; +import {BaseRequestOptions} from './base_request_options'; +import * as Rx from 'rx'; + +/** + * A function to perform http requests over XMLHttpRequest. + * + * #Example + * + * ``` + * @Component({ + * appInjector: [httpBindings] + * }) + * @View({ + * directives: [NgFor], + * template: ` + *
    + *
  • + * hello, {{person.name}} + *
  • + *
+ * ` + * }) + * class MyComponent { + * constructor(http:Http) { + * http('people.json').subscribe(res => this.people = res.json()); + * } + * } + * ``` + * + * + * This function is bound to a single underlying connection mechanism, such as XHR, which could be + * mocked with dependency injection by replacing the `Backend` binding. For other transports, like + * JSONP or Node, a separate http function would be created, such as httpJSONP. + * + * @exportedAs angular2/http + * + **/ + +// Abstract +@Injectable() +export class Http { +} + +var Observable; +if (Rx.hasOwnProperty('default')) { + Observable = (Rx).default.Rx.Observable; +} else { + Observable = Rx.Observable; +} +export function HttpFactory(backend: XHRBackend, defaultOptions: BaseRequestOptions) { + return function(url: string, options?: RequestOptions) { + return >(Observable.create(observer => { + var connection: Connection = backend.createConnection(new Request(url, options)); + var internalSubscription = connection.response.subscribe(observer); + return () => { + internalSubscription.dispose(); + connection.dispose(); + } + })) + } +} diff --git a/modules/angular2/src/http/interfaces.ts b/modules/angular2/src/http/interfaces.ts new file mode 100644 index 0000000000..c86e114eed --- /dev/null +++ b/modules/angular2/src/http/interfaces.ts @@ -0,0 +1,63 @@ +/// + +import { + ReadyStates, + RequestModesOpts, + RequestMethods, + RequestCacheOpts, + RequestCredentialsOpts, + ResponseTypes +} from './enums'; +import {Headers} from './headers'; +import {URLSearchParams} from './url_search_params'; + +export interface RequestOptions { + method?: RequestMethods; + headers?: Headers; + body?: URLSearchParams | FormData | string; + mode?: RequestModesOpts; + credentials?: RequestCredentialsOpts; + cache?: RequestCacheOpts; +} + +export interface Request { + method: RequestMethods; + mode: RequestModesOpts; + credentials: RequestCredentialsOpts; +} + +export interface ResponseOptions { + status?: number; + statusText?: string; + headers?: Headers | Object; + type?: ResponseTypes; + url?: string; +} + +export interface Response { + headers: Headers; + ok: boolean; + status: number; + statusText: string; + type: ResponseTypes; + url: string; + totalBytes: number; + bytesLoaded: number; + blob(): Blob; + arrayBuffer(): ArrayBuffer; + text(): string; + json(): Object; +} + +export interface ConnectionBackend { createConnection(observer: any, config: Request): Connection; } + +export interface Connection { + readyState: ReadyStates; + request: Request; + response: Rx.Subject; + dispose(): void; +} + +// Prefixed as IHttp because used in conjunction with Http class, but interface is callable +// constructor(@Inject(Http) http:IHttp) +export interface IHttp { (url: string, options?: RequestOptions): Rx.Observable } diff --git a/modules/angular2/src/http/static_request.ts b/modules/angular2/src/http/static_request.ts new file mode 100644 index 0000000000..c6175528b1 --- /dev/null +++ b/modules/angular2/src/http/static_request.ts @@ -0,0 +1,39 @@ +import {RequestMethods, RequestModesOpts, RequestCredentialsOpts} from './enums'; +import {URLSearchParams} from './url_search_params'; +import {RequestOptions, Request as IRequest} from './interfaces'; +import {Headers} from './headers'; +import {BaseException, RegExpWrapper} from 'angular2/src/facade/lang'; + +// TODO(jeffbcross): implement body accessors +export class Request implements IRequest { + method: RequestMethods; + mode: RequestModesOpts; + credentials: RequestCredentialsOpts; + headers: Headers; + /* + * Non-Standard Properties + */ + // This property deviates from the standard. Body can be set in constructor, but is only + // accessible + // via json(), text(), arrayBuffer(), and blob() accessors, which also change the request's state + // to "used". + private body: URLSearchParams | FormData | Blob | string; + + constructor(public url: string, {body, method = RequestMethods.GET, mode = RequestModesOpts.Cors, + credentials = RequestCredentialsOpts.Omit, + headers = new Headers()}: RequestOptions = {}) { + this.body = body; + // Defaults to 'GET', consistent with browser + this.method = method; + // Defaults to 'cors', consistent with browser + // TODO(jeffbcross): implement behavior + this.mode = mode; + // Defaults to 'omit', consistent with browser + // TODO(jeffbcross): implement behavior + this.credentials = credentials; + // Defaults to empty headers object, consistent with browser + this.headers = headers; + } + + text(): String { return this.body ? this.body.toString() : ''; } +} diff --git a/modules/angular2/src/http/static_response.ts b/modules/angular2/src/http/static_response.ts new file mode 100644 index 0000000000..d4305e2ced --- /dev/null +++ b/modules/angular2/src/http/static_response.ts @@ -0,0 +1,47 @@ +import {Response as IResponse, ResponseOptions} from './interfaces'; +import {ResponseTypes} from './enums'; +import {baseResponseOptions} from './base_response_options'; +import {BaseException, isJsObject, isString, global} from 'angular2/src/facade/lang'; +import {Headers} from './headers'; + +// TODO: make this injectable so baseResponseOptions can be overridden +export class Response implements IResponse { + type: ResponseTypes; + ok: boolean; + url: string; + status: number; + statusText: string; + bytesLoaded: number; + totalBytes: number; + headers: Headers; + constructor(private body?: string | Object | ArrayBuffer | JSON | FormData | Blob, + {status, statusText, headers, type, url}: ResponseOptions = baseResponseOptions) { + if (isJsObject(headers)) { + headers = new Headers(headers); + } + this.body = body; + this.status = status; + this.statusText = statusText; + this.headers = headers; + this.type = type; + this.url = url; + } + + blob(): Blob { + throw new BaseException('"blob()" method not implemented on Response superclass'); + } + + json(): JSON { + if (isJsObject(this.body)) { + return this.body; + } else if (isString(this.body)) { + return global.JSON.parse(this.body); + } + } + + text(): string { return this.body.toString(); } + + arrayBuffer(): ArrayBuffer { + throw new BaseException('"arrayBuffer()" method not implemented on Response superclass'); + } +} diff --git a/modules/angular2/src/http/url_search_params.ts b/modules/angular2/src/http/url_search_params.ts new file mode 100644 index 0000000000..47b1e1b7c9 --- /dev/null +++ b/modules/angular2/src/http/url_search_params.ts @@ -0,0 +1,45 @@ +import {isPresent, isBlank, StringWrapper} from 'angular2/src/facade/lang'; +import {Map, MapWrapper, List, ListWrapper} from 'angular2/src/facade/collection'; + +function paramParser(rawParams: string): Map> { + var map: Map> = MapWrapper.create(); + var params: List = StringWrapper.split(rawParams, '&'); + ListWrapper.forEach(params, (param: string) => { + var split: List = StringWrapper.split(param, '='); + var key = ListWrapper.get(split, 0); + var val = ListWrapper.get(split, 1); + var list = MapWrapper.get(map, key) || ListWrapper.create(); + ListWrapper.push(list, val); + MapWrapper.set(map, key, list); + }); + return map; +} + +export class URLSearchParams { + paramsMap: Map>; + constructor(public rawParams: string) { this.paramsMap = paramParser(rawParams); } + + has(param: string): boolean { return MapWrapper.contains(this.paramsMap, param); } + + get(param: string): string { return ListWrapper.first(MapWrapper.get(this.paramsMap, param)); } + + getAll(param: string): List { + return MapWrapper.get(this.paramsMap, param) || ListWrapper.create(); + } + + append(param: string, val: string): void { + var list = MapWrapper.get(this.paramsMap, param) || ListWrapper.create(); + ListWrapper.push(list, val); + MapWrapper.set(this.paramsMap, param, list); + } + + toString(): string { + var paramsList = ListWrapper.create(); + MapWrapper.forEach(this.paramsMap, (values, k) => { + ListWrapper.forEach(values, v => { ListWrapper.push(paramsList, k + '=' + v); }); + }); + return ListWrapper.join(paramsList, '&'); + } + + delete (param): void { MapWrapper.delete(this.paramsMap, param); } +} diff --git a/modules/angular2/test/http/backends/xhr_backend_spec.ts b/modules/angular2/test/http/backends/xhr_backend_spec.ts new file mode 100644 index 0000000000..dafea98217 --- /dev/null +++ b/modules/angular2/test/http/backends/xhr_backend_spec.ts @@ -0,0 +1,78 @@ +import { + AsyncTestCompleter, + beforeEach, + ddescribe, + describe, + expect, + iit, + inject, + it, + xit, + SpyObject +} from 'angular2/test_lib'; +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'; + +var abortSpy; +var sendSpy; +var openSpy; +var addEventListenerSpy; + +class MockBrowserXHR extends SpyObject { + abort: any; + send: any; + open: any; + addEventListener: any; + response: any; + responseText: string; + constructor() { + super(); + this.abort = abortSpy = this.spy('abort'); + this.send = sendSpy = this.spy('send'); + this.open = openSpy = this.spy('open'); + this.addEventListener = addEventListenerSpy = this.spy('addEventListener'); + } +} + +export function main() { + describe('XHRBackend', () => { + var backend; + var sampleRequest; + var constructSpy = new SpyObject(); + + beforeEach(() => { + var injector = + Injector.resolveAndCreate([bind(BrowserXHR).toValue(MockBrowserXHR), XHRBackend]); + backend = injector.get(XHRBackend); + sampleRequest = new Request('https://google.com'); + }); + + it('should create a connection', + () => { expect(() => backend.createConnection(sampleRequest)).not.toThrow(); }); + + + describe('XHRConnection', () => { + it('should call abort when disposed', () => { + var connection = new XHRConnection(sampleRequest, MockBrowserXHR); + connection.dispose(); + expect(abortSpy).toHaveBeenCalled(); + }); + + + it('should automatically call open with method and url', () => { + new XHRConnection(sampleRequest, MockBrowserXHR); + expect(openSpy).toHaveBeenCalledWith('GET', sampleRequest.url); + }); + + + it('should automatically call send on the backend with request body', () => { + var body = 'Some body to love'; + var request = new Request('https://google.com', {body: body}); + var connection = new XHRConnection(request, MockBrowserXHR); + expect(sendSpy).toHaveBeenCalledWith(body); + }); + }); + }); +} diff --git a/modules/angular2/test/http/headers_spec.ts b/modules/angular2/test/http/headers_spec.ts new file mode 100644 index 0000000000..149d24dc75 --- /dev/null +++ b/modules/angular2/test/http/headers_spec.ts @@ -0,0 +1,66 @@ +import {Headers} from 'angular2/src/http/headers'; +import {Map} from 'angular2/src/facade/collection'; +import { + AsyncTestCompleter, + beforeEach, + ddescribe, + describe, + expect, + iit, + inject, + it, + xit +} from 'angular2/test_lib'; + +export function main() { + describe('Headers', () => { + it('should conform to spec', () => { + // Examples borrowed from https://developer.mozilla.org/en-US/docs/Web/API/Headers/Headers + // Spec at https://fetch.spec.whatwg.org/#dom-headers + var myHeaders = new Headers(); // Currently empty + myHeaders.append('Content-Type', 'image/jpeg'); + expect(myHeaders.get('Content-Type')).toBe('image/jpeg'); + var httpHeaders = { + 'Content-Type': 'image/jpeg', + 'Accept-Charset': 'utf-8', + 'X-My-Custom-Header': 'Zeke are cool' + }; + var myHeaders = new Headers(httpHeaders); + var secondHeadersObj = new Headers(myHeaders); + expect(secondHeadersObj.get('Content-Type')).toBe('image/jpeg'); + }); + + + describe('initialization', () => { + it('should create a private headersMap map', + () => { expect(new Headers()._headersMap).toBeAnInstanceOf(Map); }); + + + it('should merge values in provided dictionary', () => { + var headers = new Headers({foo: 'bar'}); + expect(headers.get('foo')).toBe('bar'); + expect(headers.getAll('foo')).toEqual(['bar']); + }); + }); + + + describe('.set()', () => { + it('should clear all values and re-set for the provided key', () => { + var headers = new Headers({foo: 'bar'}); + expect(headers.get('foo')).toBe('bar'); + expect(headers.getAll('foo')).toEqual(['bar']); + headers.set('foo', 'baz'); + expect(headers.get('foo')).toBe('baz'); + expect(headers.getAll('foo')).toEqual(['baz']); + }); + + + it('should convert input array to string', () => { + var headers = new Headers(); + headers.set('foo', ['bar', 'baz']); + expect(headers.get('foo')).toBe('bar,baz'); + expect(headers.getAll('foo')).toEqual(['bar,baz']); + }); + }); + }); +} diff --git a/modules/angular2/test/http/http_spec.ts b/modules/angular2/test/http/http_spec.ts new file mode 100644 index 0000000000..15d031dd56 --- /dev/null +++ b/modules/angular2/test/http/http_spec.ts @@ -0,0 +1,96 @@ +import { + AsyncTestCompleter, + beforeEach, + ddescribe, + describe, + expect, + iit, + inject, + it, + xit, + SpyObject +} from 'angular2/test_lib'; +import {Http, HttpFactory} from 'angular2/src/http/http'; +import {XHRBackend} from 'angular2/src/http/backends/xhr_backend'; +import {httpInjectables} from 'angular2/http'; +import {Injector, bind} from 'angular2/di'; +import {MockBackend} from 'angular2/src/http/backends/mock_backend'; +import {Response} from 'angular2/src/http/static_response'; +import {ReadyStates} from 'angular2/src/http/enums'; + +class SpyObserver extends SpyObject { + onNext: Function; + onError: Function; + onCompleted: Function; + constructor() { + super(); + this.onNext = this.spy('onNext'); + this.onError = this.spy('onError'); + this.onCompleted = this.spy('onCompleted'); + } +} + +export function main() { + describe('http', () => { + var url = 'http://foo.bar'; + var http; + var injector; + var backend: MockBackend; + var baseResponse; + var sampleObserver; + beforeEach(() => { + injector = Injector.resolveAndCreate([MockBackend, bind(Http).toFactory(HttpFactory, [MockBackend])]); + http = injector.get(Http); + backend = injector.get(MockBackend); + baseResponse = new Response('base response'); + sampleObserver = new SpyObserver(); + }); + + afterEach(() => { /*backend.verifyNoPendingRequests();*/ }); + + + it('should return an Observable', () => { + expect(typeof http(url).subscribe).toBe('function'); + backend.resolveAllConnections(); + }); + + + it('should perform a get request for given url if only passed a string', + inject([AsyncTestCompleter], (async) => { + var connection; + backend.connections.subscribe((c) => connection = c); + var subscription = http('http://basic.connection') + .subscribe(res => { + expect(res.text()).toBe('base response'); + async.done(); + }); + connection.mockRespond(baseResponse) + })); + + + it('should perform a get request for given url if passed a ConnectionConfig instance', + inject([AsyncTestCompleter], async => { + var connection; + backend.connections.subscribe((c) => connection = c); + http('http://basic.connection', {method: ReadyStates.UNSENT}) + .subscribe(res => { + expect(res.text()).toBe('base response'); + async.done(); + }); + connection.mockRespond(baseResponse) + })); + + + it('should perform a get request for given url if passed a dictionary', + inject([AsyncTestCompleter], async => { + var connection; + backend.connections.subscribe((c) => connection = c); + http(url, {method: ReadyStates.UNSENT}) + .subscribe(res => { + expect(res.text()).toBe('base response'); + async.done(); + }); + connection.mockRespond(baseResponse) + })); + }); +} diff --git a/modules/angular2/test/http/url_search_params_spec.ts b/modules/angular2/test/http/url_search_params_spec.ts new file mode 100644 index 0000000000..40b2a7e940 --- /dev/null +++ b/modules/angular2/test/http/url_search_params_spec.ts @@ -0,0 +1,35 @@ +import { + AsyncTestCompleter, + beforeEach, + ddescribe, + describe, + expect, + iit, + inject, + it, + xit +} from 'angular2/test_lib'; +import {URLSearchParams} from 'angular2/src/http/url_search_params'; + +export function main() { + describe('URLSearchParams', () => { + it('should conform to spec', () => { + var paramsString = "q=URLUtils.searchParams&topic=api"; + var searchParams = new URLSearchParams(paramsString); + + // Tests borrowed from example at + // https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams + // Compliant with spec described at https://url.spec.whatwg.org/#urlsearchparams + expect(searchParams.has("topic")).toBe(true); + expect(searchParams.has("foo")).toBe(false); + expect(searchParams.get("topic")).toBe("api"); + expect(searchParams.getAll("topic")).toEqual(["api"]); + expect(searchParams.get("foo")).toBe(null); + searchParams.append("topic", "webdev"); + expect(searchParams.getAll("topic")).toEqual(["api", "webdev"]); + expect(searchParams.toString()).toBe("q=URLUtils.searchParams&topic=api&topic=webdev"); + searchParams.delete("topic"); + expect(searchParams.toString()).toBe("q=URLUtils.searchParams"); + }); + }); +} diff --git a/modules/examples/e2e_test/http/http_spec.ts b/modules/examples/e2e_test/http/http_spec.ts new file mode 100644 index 0000000000..450e6579d3 --- /dev/null +++ b/modules/examples/e2e_test/http/http_spec.ts @@ -0,0 +1,23 @@ +/// + +import {verifyNoBrowserErrors} from 'angular2/src/test_lib/e2e_util'; + +describe('http', function() { + + afterEach(verifyNoBrowserErrors); + + describe('fetching', function() { + var URL = 'examples/src/http/index.html'; + + it('should fetch and display people', function() { + browser.get(URL); + + expect(getComponentText('http-app', '.people')).toEqual('hello, Jeff'); + }); + }); +}); + +function getComponentText(selector, innerSelector) { + return browser.executeScript('return document.querySelector("' + selector + '").querySelector("' + + innerSelector + '").textContent.trim()'); +} diff --git a/modules/examples/src/http/assign_local_directive.ts b/modules/examples/src/http/assign_local_directive.ts new file mode 100644 index 0000000000..1ad43c8e37 --- /dev/null +++ b/modules/examples/src/http/assign_local_directive.ts @@ -0,0 +1,20 @@ +import {Directive, ViewContainerRef, ProtoViewRef} from "angular2/angular2"; + +@Directive({selector: '[assign-local]', properties: ['localVariable: assignLocalTo']}) +export class LocalVariable { + viewContainer: ViewContainerRef; + protoViewRef: ProtoViewRef; + view: any; + constructor(viewContainer: ViewContainerRef, protoViewRef: ProtoViewRef) { + this.viewContainer = viewContainer; + this.protoViewRef = protoViewRef; + } + + set localVariable(exp) { + if (!this.viewContainer.length) { + this.view = this.viewContainer.create(this.protoViewRef); + } + + this.view.setLocal("$implicit", exp); + } +} diff --git a/modules/examples/src/http/http_comp.ts b/modules/examples/src/http/http_comp.ts new file mode 100644 index 0000000000..2461a99a4e --- /dev/null +++ b/modules/examples/src/http/http_comp.ts @@ -0,0 +1,30 @@ +import {bootstrap, Component, View, NgFor, NgIf, Inject} from 'angular2/angular2'; +import {httpInjectables} from 'angular2/http'; +import {Http} from 'angular2/src/http/http'; +import {IHttp} from 'angular2/src/http/interfaces'; +import {Response} from 'angular2/src/http/static_response'; +import {LocalVariable} from './assign_local_directive'; + +@Component({selector: 'http-app', appInjector: [httpInjectables]}) +@View({ + directives: [NgFor, NgIf, LocalVariable], + template: ` +

people

+
+
    +
  • + hello, {{person.name}} +
  • +
+ + Fetching people... + +
+ ` +}) +export class HttpCmp { + people: Rx.Observable; + constructor(@Inject(Http) http: IHttp) { + this.people = http('./people.json').map(res => res.json()); + } +} \ No newline at end of file diff --git a/modules/examples/src/http/index.html b/modules/examples/src/http/index.html new file mode 100644 index 0000000000..69d910f0d4 --- /dev/null +++ b/modules/examples/src/http/index.html @@ -0,0 +1,11 @@ + + + Hello Http + + + Loading... + + + $SCRIPTS$ + + diff --git a/modules/examples/src/http/index.ts b/modules/examples/src/http/index.ts new file mode 100644 index 0000000000..32a97ea6f2 --- /dev/null +++ b/modules/examples/src/http/index.ts @@ -0,0 +1,30 @@ +/// + +import { + bootstrap, + ElementRef, + Component, + Directive, + View, + Injectable, + NgFor, + NgIf, + Inject +} from 'angular2/angular2'; +import {bind} from 'angular2/di'; +import {PipeRegistry, defaultPipes} from 'angular2/change_detection'; +import {reflector} from 'angular2/src/reflection/reflection'; +import {ReflectionCapabilities} from 'angular2/src/reflection/reflection_capabilities'; +import {httpBindings} from 'angular2/http'; +import {Http} from 'angular2/src/http/http'; +import {IHttp} from 'angular2/src/http/interfaces'; +import {Response} from 'angular2/src/http/static_response'; +import {LocalVariable} from './assign_local_directive'; +import {RxPipeFactory} from './rx_pipe'; +import {HttpCmp} from './http_comp'; + +export function main() { + reflector.reflectionCapabilities = new ReflectionCapabilities(); + defaultPipes.rx = [new RxPipeFactory()] bootstrap( + HttpCmp, [bind(PipeRegistry).toValue(new PipeRegistry(defaultPipes))]); +} diff --git a/modules/examples/src/http/index_dynamic.html b/modules/examples/src/http/index_dynamic.html new file mode 100644 index 0000000000..1eb9f87446 --- /dev/null +++ b/modules/examples/src/http/index_dynamic.html @@ -0,0 +1,11 @@ + + + Angular 2.0 Http (Reflection) + + + Loading... + + + $SCRIPTS$ + + diff --git a/modules/examples/src/http/index_dynamic.ts b/modules/examples/src/http/index_dynamic.ts new file mode 100644 index 0000000000..e63372f815 --- /dev/null +++ b/modules/examples/src/http/index_dynamic.ts @@ -0,0 +1,15 @@ +import {HttpCmp} from './http_comp'; +import {bootstrap} from 'angular2/angular2'; +import {bind} from 'angular2/di'; +import {reflector} from 'angular2/src/reflection/reflection'; +import {ReflectionCapabilities} from 'angular2/src/reflection/reflection_capabilities'; +import {PipeRegistry, defaultPipes} from 'angular2/change_detection'; +import {RxPipeFactory} from './rx_pipe'; + +export function main() { + // This entry point is not transformed and exists for testing purposes. + // See index.js for an explanation. + reflector.reflectionCapabilities = new ReflectionCapabilities(); + defaultPipes.rx = [new RxPipeFactory()] bootstrap( + HttpCmp, [bind(PipeRegistry).toValue(new PipeRegistry(defaultPipes))]); +} diff --git a/modules/examples/src/http/people.json b/modules/examples/src/http/people.json new file mode 100644 index 0000000000..0cee0ca0a9 --- /dev/null +++ b/modules/examples/src/http/people.json @@ -0,0 +1 @@ +[{"name":"Jeff"}] diff --git a/modules/examples/src/http/rx_pipe.ts b/modules/examples/src/http/rx_pipe.ts new file mode 100644 index 0000000000..44d0654215 --- /dev/null +++ b/modules/examples/src/http/rx_pipe.ts @@ -0,0 +1,38 @@ +/// + +import {isBlank, isPresent, CONST} from 'angular2/src/facade/lang'; +import {Observable, ObservableWrapper} from 'angular2/src/facade/async'; +import {Pipe, WrappedValue, PipeFactory} from 'angular2/src/change_detection/pipes/pipe'; +import {ObservablePipe} from 'angular2/src/change_detection/pipes/observable_pipe'; +import {ChangeDetectorRef} from 'angular2/src/change_detection/change_detector_ref'; +import * as Rx from 'rx'; + +export class RxPipe extends ObservablePipe { + supports(obs): boolean { + if (Rx.hasOwnProperty('default')) { + return obs instanceof (Rx).default.Rx.Observable; + } else { + return obs instanceof Rx.Observable; + } + } + + _subscribe(obs): void { + this._observable = obs; + this._subscription = + (obs).subscribe(value => {this._updateLatestValue(value)}, e => { throw e; }); + } +} + +/** + * Provides a factory for [ObervablePipe]. + * + * @exportedAs angular2/pipes + */ +@CONST() +export class RxPipeFactory extends PipeFactory { + constructor() { super(); } + + supports(obs): boolean { return obs instanceof (Rx).default.Rx.Observable } + + create(cdRef): Pipe { return new RxPipe(cdRef); } +} diff --git a/protractor-dart2js.conf.js b/protractor-dart2js.conf.js index 4506c5e428..b90e43e433 100644 --- a/protractor-dart2js.conf.js +++ b/protractor-dart2js.conf.js @@ -5,10 +5,13 @@ config.baseUrl = 'http://localhost:8002/'; config.exclude.push( 'dist/js/cjs/examples/e2e_test/sourcemap/sourcemap_spec.js', + //TODO(jeffbcross): remove when http has been implemented for dart + 'dist/js/cjs/examples/e2e_test/http/http_spec.js', // TODO: remove this line when largetable dart has been added 'dist/js/cjs/benchmarks_external/e2e_test/largetable_perf.js', 'dist/js/cjs/benchmarks_external/e2e_test/polymer_tree_perf.js', 'dist/js/cjs/benchmarks_external/e2e_test/react_tree_perf.js' + ); data.createBenchpressRunner({ lang: 'dart' }); diff --git a/scripts/ci/test_e2e_dart.sh b/scripts/ci/test_e2e_dart.sh index 009575d1ba..199dcd824b 100755 --- a/scripts/ci/test_e2e_dart.sh +++ b/scripts/ci/test_e2e_dart.sh @@ -16,6 +16,7 @@ function killServer () { serverPid=$! ./node_modules/.bin/gulp build.css.material& +./node_modules/.bin/gulp build.http.example& trap killServer EXIT diff --git a/scripts/ci/test_e2e_js.sh b/scripts/ci/test_e2e_js.sh index ba0f3b8a70..46a16e04fb 100755 --- a/scripts/ci/test_e2e_js.sh +++ b/scripts/ci/test_e2e_js.sh @@ -16,6 +16,7 @@ function killServer () { serverPid=$! ./node_modules/.bin/gulp build.css.material& +./node_modules/.bin/gulp build.http.example& trap killServer EXIT diff --git a/tools/broccoli/trees/browser_tree.ts b/tools/broccoli/trees/browser_tree.ts index 18485d7b69..c7f264a467 100644 --- a/tools/broccoli/trees/browser_tree.ts +++ b/tools/broccoli/trees/browser_tree.ts @@ -41,6 +41,7 @@ const kServedPaths = [ 'examples/src/forms', 'examples/src/gestures', 'examples/src/hello_world', + 'examples/src/http', 'examples/src/key_events', 'examples/src/sourcemap', 'examples/src/todo', diff --git a/tools/broccoli/trees/dart_tree.ts b/tools/broccoli/trees/dart_tree.ts index f27f5e8da2..ed30bc910b 100644 --- a/tools/broccoli/trees/dart_tree.ts +++ b/tools/broccoli/trees/dart_tree.ts @@ -44,7 +44,16 @@ function stripModulePrefix(relativePath: string): string { function getSourceTree() { // Transpile everything in 'modules' except for rtts_assertions. - var tsInputTree = modulesFunnel(['**/*.js', '**/*.ts', '**/*.dart'], ['rtts_assert/**/*']); + var tsInputTree = modulesFunnel(['**/*.js', '**/*.ts', '**/*.dart'], + // TODO(jeffbcross): add http when lib supports dart + [ + 'rtts_assert/**/*', + 'examples/e2e_test/http/**/*', + 'examples/src/http/**/*', + 'angular2/src/http/**/*', + 'angular2/test/http/**/*', + 'angular2/http.ts' + ]); var transpiled = ts2dart(tsInputTree, {generateLibraryName: true, generateSourceMap: false}); // Native sources, dart only examples, etc. var dartSrcs = modulesFunnel(['**/*.dart', '**/*.ng_meta.json', '**/css/**']);