From a858b6d31817e40a4b7b55200ef1a8d6e823765a Mon Sep 17 00:00:00 2001 From: Nathan Walker Date: Fri, 10 Jun 2016 20:13:44 -0700 Subject: [PATCH] feat(Http): option to fetch local resources with Http using '~/' semantics like Image component closes https://github.com/NativeScript/nativescript-angular/issues/249 --- .gitignore | 1 + nativescript-angular/application.ts | 23 +++++-- .../file-system/ns-file-system.ts | 12 ++++ nativescript-angular/http/ns-http.ts | 56 +++++++++++++++++ nativescript-angular/{ => http}/xhr.ts | 0 nativescript-angular/package.json | 3 +- ng-sample/app/app.ts | 5 +- ng-sample/app/examples/http/data.json | 8 +++ ng-sample/app/examples/http/http-test.ts | 50 +++++++++++++++ ng-sample/package.json | 1 + tests/app/tests/http.ts | 62 +++++++++++++++++++ tests/app/tests/mocks/ns-file-system.mock.ts | 35 +++++++++++ tests/app/tests/xhr-paths.ts | 2 +- tests/package.json | 1 + 14 files changed, 252 insertions(+), 7 deletions(-) create mode 100644 nativescript-angular/file-system/ns-file-system.ts create mode 100644 nativescript-angular/http/ns-http.ts rename nativescript-angular/{ => http}/xhr.ts (100%) create mode 100644 ng-sample/app/examples/http/data.json create mode 100644 ng-sample/app/examples/http/http-test.ts create mode 100644 tests/app/tests/http.ts create mode 100644 tests/app/tests/mocks/ns-file-system.mock.ts diff --git a/.gitignore b/.gitignore index c51171b48..33742d38b 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,7 @@ ng-sample/app/global.d.ts ng-sample/platforms ng-sample/lib ng-sample/node_modules +ng-sample/app/nativescript-angular startup-test/platforms startup-test/lib diff --git a/nativescript-angular/application.ts b/nativescript-angular/application.ts index 03e29cb08..c2e2978a3 100644 --- a/nativescript-angular/application.ts +++ b/nativescript-angular/application.ts @@ -1,5 +1,5 @@ import 'globals'; -import "zone.js/dist/zone-node" +import "zone.js/dist/zone-node"; import 'reflect-metadata'; import './polyfills/array'; @@ -14,7 +14,10 @@ import {RootRenderer, Renderer} from '@angular/core/src/render/api'; import {NativeScriptRootRenderer, NativeScriptRenderer} from './renderer'; import {NativeScriptDomAdapter, NativeScriptElementSchemaRegistry, NativeScriptSanitizationService} from './dom-adapter'; import {ElementSchemaRegistry, XHR, COMPILER_PROVIDERS, CompilerConfig} from '@angular/compiler'; -import {FileSystemXHR} from './xhr'; +import {Http, XHRBackend, BrowserXhr, RequestOptions, ResponseOptions, XSRFStrategy} from '@angular/http'; +import {FileSystemXHR} from './http/xhr'; +import {NSXSRFStrategy, NSHttp} from './http/ns-http'; +import {NSFileSystem} from './file-system/ns-file-system'; import {Parse5DomAdapter} from '@angular/platform-server/src/parse5_adapter'; import {ExceptionHandler} from '@angular/core/src/facade/exception_handler'; import {APPLICATION_COMMON_PROVIDERS} from '@angular/core/src/application_common_providers'; @@ -87,7 +90,7 @@ export function bootstrap(appComponentType: any, provide(ElementSchemaRegistry, { useClass: NativeScriptElementSchemaRegistry }), NS_COMPILER_PROVIDERS, provide(ElementSchemaRegistry, { useClass: NativeScriptElementSchemaRegistry }), - provide(XHR, { useClass: FileSystemXHR }), + provide(XHR, { useClass: FileSystemXHR }) ] var appProviders = [defaultAppProviders]; @@ -95,7 +98,19 @@ export function bootstrap(appComponentType: any, appProviders.push(customProviders); } - var platform = getPlatform(); + // Http Setup + // Since HTTP_PROVIDERS can be added with customProviders above, this must come after + appProviders.push([ + provide(XSRFStrategy, { useValue: new NSXSRFStrategy()}), + NSFileSystem, + provide(Http, { + useFactory: (backend, options, nsFileSystem) => { + return new NSHttp(backend, options, nsFileSystem); + }, deps: [XHRBackend, RequestOptions, NSFileSystem] + }) + ]); + + var platform = getPlatform(); if (!isPresent(platform)) { platform = createPlatform(ReflectiveInjector.resolveAndCreate(platformProviders)); } diff --git a/nativescript-angular/file-system/ns-file-system.ts b/nativescript-angular/file-system/ns-file-system.ts new file mode 100644 index 000000000..704ae45e4 --- /dev/null +++ b/nativescript-angular/file-system/ns-file-system.ts @@ -0,0 +1,12 @@ +import {Injectable} from '@angular/core'; +import {knownFolders, Folder} from 'file-system'; + +// Allows greater flexibility with `file-system` and Angular +// Also provides a way for `file-system` to be mocked for testing + +@Injectable() +export class NSFileSystem { + public currentApp(): Folder { + return knownFolders.currentApp(); + } +} \ No newline at end of file diff --git a/nativescript-angular/http/ns-http.ts b/nativescript-angular/http/ns-http.ts new file mode 100644 index 000000000..242025fde --- /dev/null +++ b/nativescript-angular/http/ns-http.ts @@ -0,0 +1,56 @@ +import {Injectable} from '@angular/core'; +import {Http, XHRConnection, ConnectionBackend, RequestOptions, RequestOptionsArgs, ResponseOptions, ResponseType, Response, Request, BrowserXhr} from '@angular/http'; +import {Observable} from 'rxjs/Observable'; +import 'rxjs/add/observable/fromPromise'; +import {NSFileSystem} from '../file-system/ns-file-system'; + +export class NSXSRFStrategy { + public configureRequest(req: any) { + // noop + } +} + +@Injectable() +export class NSHttp extends Http { + constructor(backend: ConnectionBackend, defaultOptions: RequestOptions, private nsFileSystem: NSFileSystem) { + super(backend, defaultOptions); + } + + /** + * Performs a request with `get` http method. + * Uses a local file if `~/` resource is requested. + */ + get(url: string, options?: RequestOptionsArgs): Observable { + if (url.indexOf('~') === 0 || url.indexOf('/') === 0) { + // normalize url + url = url.replace('~', '').replace('/', ''); + // request from local app resources + return Observable.fromPromise(new Promise((resolve, reject) => { + let app = this.nsFileSystem.currentApp(); + let localFile = app.getFile(url); + if (localFile) { + localFile.readText().then((data) => { + resolve(responseOptions(data, 200, url)); + }, (err: Object) => { + reject(responseOptions(err, 400, url)); + }); + } else { + reject(responseOptions('Not Found', 404, url)); + } + })); + } else { + return super.get(url, options); + } + } +} + +function responseOptions(body: string | Object, status: number, url: string): Response { + return new Response(new ResponseOptions({ + body: body, + status: status, + statusText: 'OK', + type: status === 200 ? ResponseType.Default : ResponseType.Error, + url: url + })); +} + diff --git a/nativescript-angular/xhr.ts b/nativescript-angular/http/xhr.ts similarity index 100% rename from nativescript-angular/xhr.ts rename to nativescript-angular/http/xhr.ts diff --git a/nativescript-angular/package.json b/nativescript-angular/package.json index a24c60b3f..0e91f6c46 100644 --- a/nativescript-angular/package.json +++ b/nativescript-angular/package.json @@ -21,6 +21,7 @@ "@angular/common": "2.0.0-rc.3", "@angular/compiler": "2.0.0-rc.3", "@angular/core": "2.0.0-rc.3", + "@angular/http": "2.0.0-rc.3", "@angular/platform-browser": "2.0.0-rc.3", "@angular/platform-browser-dynamic": "2.0.0-rc.3", "@angular/platform-server": "2.0.0-rc.3", @@ -39,4 +40,4 @@ "typescript": "^1.8.10" }, "nativescript": {} -} +} \ No newline at end of file diff --git a/ng-sample/app/app.ts b/ng-sample/app/app.ts index 096be6a84..e05e677ad 100644 --- a/ng-sample/app/app.ts +++ b/ng-sample/app/app.ts @@ -9,6 +9,7 @@ import { nativeScriptBootstrap } from "nativescript-angular/application"; import { NS_ROUTER_PROVIDERS as NS_ROUTER_PROVIDERS_DEPRECATED } from "nativescript-angular/router-deprecated"; import { NS_ROUTER_PROVIDERS } from "nativescript-angular/router"; +import { HTTP_PROVIDERS } from "@angular/http"; import { rendererTraceCategory, routerTraceCategory, listViewTraceCategory } from "nativescript-angular/trace"; import trace = require("trace"); @@ -23,6 +24,7 @@ import {Benchmark} from './performance/benchmark'; import {ListTest} from './examples/list/list-test'; import {ListTestAsync, ListTestFilterAsync} from "./examples/list/list-test-async"; import {ImageTest} from "./examples/image/image-test"; +import {HttpTest} from "./examples/http/http-test"; import {ActionBarTest} from "./examples/action-bar/action-bar-test"; import {ModalTest} from "./examples/modal/modal-test"; import {PlatfromDirectivesTest} from "./examples/platform-directives/platform-directives-test"; @@ -43,6 +45,7 @@ import { PageRouterOutletNestedAppComponent, PageRouterOutletNestedRouterProvide // nativeScriptBootstrap(ListTest); // nativeScriptBootstrap(ListTestAsync); //nativeScriptBootstrap(ImageTest); +nativeScriptBootstrap(HttpTest, [HTTP_PROVIDERS]); //nativeScriptBootstrap(ActionBarTest, [NS_ROUTER_PROVIDERS_DEPRECATED], { startPageActionBarHidden: false }); //nativeScriptBootstrap(ActionBarTest, [NS_ROUTER_PROVIDERS_DEPRECATED]); //nativeScriptBootstrap(ModalTest); @@ -51,7 +54,7 @@ import { PageRouterOutletNestedAppComponent, PageRouterOutletNestedRouterProvide // new router // nativeScriptBootstrap(RouterOutletAppComponent, [RouterOutletRouterProviders]); // nativeScriptBootstrap(PageRouterOutletAppComponent, [PageRouterOutletRouterProviders]); -nativeScriptBootstrap(PageRouterOutletNestedAppComponent, [PageRouterOutletNestedRouterProviders]); +// nativeScriptBootstrap(PageRouterOutletNestedAppComponent, [PageRouterOutletNestedRouterProviders]); // router-deprecated // nativeScriptBootstrap(NavigationTest, [NS_ROUTER_PROVIDERS_DEPRECATED]); diff --git a/ng-sample/app/examples/http/data.json b/ng-sample/app/examples/http/data.json new file mode 100644 index 000000000..52d4169d4 --- /dev/null +++ b/ng-sample/app/examples/http/data.json @@ -0,0 +1,8 @@ +{ + "results": [ + { + "title": "Test", + "description": "Testing Http local and remote." + } + ] +} diff --git a/ng-sample/app/examples/http/http-test.ts b/ng-sample/app/examples/http/http-test.ts new file mode 100644 index 000000000..3a56c0120 --- /dev/null +++ b/ng-sample/app/examples/http/http-test.ts @@ -0,0 +1,50 @@ +import {Component} from '@angular/core'; +import {Http} from '@angular/http'; +import 'rxjs/add/operator/map'; + +/* IMPORTANT +In order to test out the full image example, to fix the App Transport Security error in iOS 9, you will need to follow this after adding the iOS platform: + +https://blog.nraboy.com/2015/12/fix-ios-9-app-transport-security-issues-in-nativescript/ +*/ + +@Component({ + selector: 'http-test', + template: ` + + + + + + + `, + styles: [ + `Button { + margin-bottom:20; + }` + ] +}) +export class HttpTest { + public title: string; + public description: string; + + constructor(private http: Http) { + + } + + public loadLocal() { + this.http.get('~/examples/http/data.json').map(res => res.json()).subscribe((response: any) => { + let user = response.results[0]; + this.title = user.title; + this.description = user.description; + }); + } + + public loadRemote() { + this.http.get(`https://randomuser.me/api/?results=1&nat=us`).map(res => res.json()).subscribe((response: any) => { + let user = response.results[0]; + this.title = user.name.first; + this.description = user.email; + }); + } +} diff --git a/ng-sample/package.json b/ng-sample/package.json index f1433712b..1c74490c9 100644 --- a/ng-sample/package.json +++ b/ng-sample/package.json @@ -29,6 +29,7 @@ "@angular/common": "2.0.0-rc.3", "@angular/compiler": "2.0.0-rc.3", "@angular/core": "2.0.0-rc.3", + "@angular/http": "2.0.0-rc.3", "@angular/platform-browser": "2.0.0-rc.3", "@angular/platform-browser-dynamic": "2.0.0-rc.3", "@angular/platform-server": "2.0.0-rc.3", diff --git a/tests/app/tests/http.ts b/tests/app/tests/http.ts new file mode 100644 index 000000000..fe8bcea75 --- /dev/null +++ b/tests/app/tests/http.ts @@ -0,0 +1,62 @@ +//make sure you import mocha-config before @angular/core +import {assert} from "./test-config"; +import { + async, + inject, + beforeEach, + beforeEachProviders +} from '@angular/core/testing'; +import {provide, ReflectiveInjector} from '@angular/core'; +import {BaseRequestOptions, ConnectionBackend, Http, HTTP_PROVIDERS, Response, ResponseOptions} from '@angular/http'; +import 'rxjs/add/operator/map'; +import {MockBackend} from '@angular/http/testing'; +import {NSHttp} from "nativescript-angular/http/ns-http"; +import {NSFileSystem} from "nativescript-angular/file-system/ns-file-system"; +import {NSFileSystemMock, FileResponses} from './mocks/ns-file-system.mock'; + +describe("Http", () => { + let http: Http; + let backend: MockBackend; + + beforeEach(() => { + let injector = ReflectiveInjector.resolveAndCreate([ + HTTP_PROVIDERS, + BaseRequestOptions, + MockBackend, + provide(NSFileSystem, { useClass: NSFileSystemMock }), + provide(Http, { + useFactory: function (backend: ConnectionBackend, defaultOptions: BaseRequestOptions, nsFileSystem: NSFileSystem) { + return new NSHttp(backend, defaultOptions, nsFileSystem); + }, + deps: [MockBackend, BaseRequestOptions, NSFileSystem] + }) + ]); + + backend = injector.get(MockBackend); + http = injector.get(Http); + }); + + it("should work with local files prefixed with '~'", () => { + http.get('~/test.json').map(res => res.json()).subscribe((response: any) => { + assert.strictEqual(3, response.length); + assert.strictEqual('Alex', response[0].name); + }); + }); + + it("should work with local files prefixed with '/'", () => { + http.get('/test.json').map(res => res.json()).subscribe((response: any) => { + assert.strictEqual(3, response.length); + assert.strictEqual('Panayot', response[2].name); + }); + }); + + it("should work with remote files", () => { + let connection: any; + backend.connections.subscribe((c: any) => connection = c); + http.get('http://www.nativescript.org/test.json').map(res => res.json()).subscribe((response: any) => { + assert.strictEqual(3, response.length); + assert.strictEqual('Rosen', response[1].name); + }); + connection.mockRespond(new Response(new ResponseOptions({ body: FileResponses.AWESOME_TEAM }))); + }); +}); diff --git a/tests/app/tests/mocks/ns-file-system.mock.ts b/tests/app/tests/mocks/ns-file-system.mock.ts new file mode 100644 index 000000000..809936f16 --- /dev/null +++ b/tests/app/tests/mocks/ns-file-system.mock.ts @@ -0,0 +1,35 @@ +import {Injectable} from '@angular/core'; +import {ResponseType, Response, ResponseOptions} from '@angular/http'; + +export class FileResponses { + public static AWESOME_TEAM: string = '[{"name":"Alex"}, {"name":"Rosen"}, {"name":"Panayot"}]'; +} + +// Folder mock +class Folder { + public getFile(url: string): any { + let data; + switch (url) { + case 'test.json': + data = FileResponses.AWESOME_TEAM; + break; + default: + throw (new Error('Unsupported file for the testing mock - ns-file-system-mock')); + } + return { + readText: () => { + return new Promise((resolve) => { + resolve(data); + }); + } + } + } +} + +// Filesystem mock +@Injectable() +export class NSFileSystemMock { + public currentApp(): Folder { + return new Folder(); + } +} \ No newline at end of file diff --git a/tests/app/tests/xhr-paths.ts b/tests/app/tests/xhr-paths.ts index 0bc08a69f..66a2e4995 100644 --- a/tests/app/tests/xhr-paths.ts +++ b/tests/app/tests/xhr-paths.ts @@ -1,6 +1,6 @@ //make sure you import mocha-config before @angular/core import {assert} from "./test-config"; -import {FileSystemXHR} from "nativescript-angular/xhr"; +import {FileSystemXHR} from "nativescript-angular/http/xhr"; describe("XHR name resolution", () => { it("resolves relative paths from app root", () => { diff --git a/tests/package.json b/tests/package.json index e710b7045..7e1a96d33 100644 --- a/tests/package.json +++ b/tests/package.json @@ -32,6 +32,7 @@ "@angular/common": "2.0.0-rc.3", "@angular/compiler": "2.0.0-rc.3", "@angular/core": "2.0.0-rc.3", + "@angular/http": "2.0.0-rc.3", "@angular/platform-browser": "2.0.0-rc.3", "@angular/platform-browser-dynamic": "2.0.0-rc.3", "@angular/platform-server": "2.0.0-rc.3",