From 5596b170a8cf1868c92894a137a7a07729e301cf Mon Sep 17 00:00:00 2001 From: Asim Hussain Date: Mon, 3 Apr 2017 22:44:53 -0600 Subject: [PATCH] Step 8 - Components to Angular --- README.md | 16 ++- package.json | 11 +- src/app/ajs-upgraded-providers.ts | 23 +++ src/app/components/card.component.ts | 78 +++++----- src/app/components/person-create.component.ts | 72 ++++------ src/app/components/person-edit.component.ts | 97 +++++-------- src/app/components/person-list.component.ts | 78 +++++----- .../components/person-modify.component.html | 134 ++++++++++++++++++ src/app/components/search.component.ts | 65 ++++++--- src/app/components/spinner.component.ts | 50 ++++--- src/app/filters/default-image.filter.ts | 15 -- src/app/filters/index.ts | 1 - src/app/main.ts | 50 ++++++- src/app/pipes/default-image.pipe.ts | 14 ++ src/app/pipes/index.ts | 1 + src/app/rxjs-operators.ts | 4 +- src/app/services/contact.service.ts | 1 + 17 files changed, 456 insertions(+), 254 deletions(-) create mode 100644 src/app/components/person-modify.component.html delete mode 100644 src/app/filters/default-image.filter.ts delete mode 100644 src/app/filters/index.ts create mode 100644 src/app/pipes/default-image.pipe.ts create mode 100644 src/app/pipes/index.ts diff --git a/README.md b/README.md index 27c4042..072991a 100644 --- a/README.md +++ b/README.md @@ -96,5 +96,19 @@ The application is a simple contacts application where you can search, create or - It depends on a 3rd party component called Toaster which only works in AngularJS, so we upgrade Toaster to use it in Angular via `ajs-upgraded-providers.ts` - We inject our upgraded Toaster using the `@Inject` annotation. - +### Step 8 - Components to Angular +*Components* +- Convert all the components to Angular components, during this process we will need to deal with a bunch of 3rd party modules. + - For the 3rd party AngularJS `angular-ladda` module we use the Angular version `angular2-ladda` + - For the 3rd party AngularJS `ng-infinite-scroll` module we use the Angular version `angular2-infinite-scroll` + - For the `angular-spinner` 3rd party AngularJS module we re-write from scratch in Angular using the underlying `spin.js` library. + - Since filters can't be upgraded we just need to re-write our `defaultImage` filter as a pipe + - We also update the template HTML to use Angular syntax instead of AngularJS syntax. + - We then add out components to `NgModule`, ensuring we add as both declarations and entry components so we can use them in AngularJS templates. + + +*UI-Router* +- Our component code uses ui-router, we will eventually move to using Angular router so for now we just need a patch to continue letting us use ui-router in this hybrid mode. + - We upgrade the ui-router services so we can use them in Angular, see `ajs-upgraded-providers.ts` + - We stop using ui-router directive such as `ui-sref` and instead hard code URLS in the template. diff --git a/package.json b/package.json index e16a6f9..26bae30 100644 --- a/package.json +++ b/package.json @@ -39,16 +39,21 @@ "jquery": "2.1.3", "ng-infinite-scroll": "1.2.1", "angular-ui-router": "^0.4.2", - "angular-spinner": "^1.0.1" + "angular-spinner": "^1.0.1", + "angular2-infinite-scroll": "^0.3.3", + "angular2-ladda": "^1.1.1", + "spin.js": "^2.3.2" }, "devDependencies": { - "@types/angular": "^1.4.0", + "@types/angular": "^1.6.2", + "@types/spin.js": "^2.3.0", "bower": "^1.8.0", "json-server": "^0.9.6", "serve": "^5.1.2", "rimraf": "^2.6.0", "ts-loader": "^2.0.1", "typescript": "^2.2.1", - "webpack": "^2.2.1" + "webpack": "^2.2.1", + "script-loader": "^0.7.0" } } diff --git a/src/app/ajs-upgraded-providers.ts b/src/app/ajs-upgraded-providers.ts index 0fe1c16..9069249 100644 --- a/src/app/ajs-upgraded-providers.ts +++ b/src/app/ajs-upgraded-providers.ts @@ -8,4 +8,27 @@ export const toasterServiceProvider = { provide: Toaster, useFactory: toasterServiceFactory, deps: ['$injector'] +}; + +export const UIRouterState = new OpaqueToken("UIRouterState"); + +export function uiRouterStateServiceFactory(i: any) { + return i.get('$state'); +} +export const uiRouterStateProvider = { + provide: UIRouterState, + useFactory: uiRouterStateServiceFactory, + deps: ['$injector'] +}; + + +export const UIRouterStateParams = new OpaqueToken("UIRouterStateParams"); + +export function uiRouterStateParamsServiceFactory(i: any) { + return i.get('$stateParams'); +} +export const uiRouterStateParamsProvider = { + provide: UIRouterStateParams, + useFactory: uiRouterStateParamsServiceFactory, + deps: ['$injector'] }; \ No newline at end of file diff --git a/src/app/components/card.component.ts b/src/app/components/card.component.ts index 09a027b..7a836b7 100644 --- a/src/app/components/card.component.ts +++ b/src/app/components/card.component.ts @@ -1,71 +1,63 @@ -import * as angular from 'angular'; +import {Input, Component} from "@angular/core"; +import {ContactService} from "../services/contact.service"; -let CardComponent = { +@Component({ selector: 'ccCard', template: `
- +
-

{{ $ctrl.user.name }} +

{{ user.name }} + [ngClass]="{'fa-female':user.sex == 'F', 'fa-male': user.sex == 'M'}">

- {{ $ctrl.user.city }}, {{ $ctrl.user.country }} + {{ user.city }}, {{ user.country }}

- {{ $ctrl.user.email }} + {{ user.email }}
- {{ $ctrl.user.birthdate | date:"longDate"}} + {{ user.birthdate | date:"longDate"}}

- - + +
-`, - bindings: { - 'user': '=' - }, - controller: class CardController { - private ContactService; - private isDeleting; - private user; +` +}) +export class CardComponent { + @Input() + public user; + public isDeleting = false; - constructor(ContactService) { - this.ContactService = ContactService; - this.isDeleting = false; - } - - deleteUser() { - this.isDeleting = true; - this.ContactService.removeContact(this.user).then(() => { - this.isDeleting = false; - }); - }; + constructor(private contactService: ContactService) { } -}; - -angular - .module('codecraft') - .component(CardComponent.selector, CardComponent); \ No newline at end of file + deleteUser() { + this.isDeleting = true; + this.contactService.removeContact(this.user).then(() => { + this.isDeleting = false; + }); + }; +} diff --git a/src/app/components/person-create.component.ts b/src/app/components/person-create.component.ts index 3066b5f..83393ea 100644 --- a/src/app/components/person-create.component.ts +++ b/src/app/components/person-create.component.ts @@ -1,54 +1,34 @@ -import * as angular from 'angular'; +import * as angular from "angular"; -export let PersonCreateComponent = { - selector: 'personCreate', - template: ` -
-
-
-
- Create -
- -
-
- -
-
- -
-
-
-
-`, - bindings: {}, - controller: class PersonCreateController { - public contacts = null; - public person = {}; +import {Component, Inject} from "@angular/core"; +import {downgradeComponent} from "@angular/upgrade/static"; +import {ContactService} from "../services/contact.service"; +import {UIRouterStateParams, UIRouterState} from "../ajs-upgraded-providers"; - private $state = null; +@Component({ + selector: 'personCreate', + templateUrl: 'app/components/person-modify.component.html' +}) +export class PersonCreateComponent { + public mode: string = 'Create'; + public person = {}; - constructor($state, ContactService) { - this.$state = $state; - this.contacts = ContactService; - this.person = {}; - } + constructor(@Inject(UIRouterStateParams) private $stateParams, + @Inject(UIRouterState) private $state, + private contacts: ContactService) { + this.person = this.contacts.getPerson(this.$stateParams.email); + } - save() { - console.log("createContact"); - this.contacts.createContact(this.person) - .then(() => { - this.$state.go("list"); - }) - } + save() { + this.contacts.createContact(this.person) + .then(() => { + this.$state.go("list"); + }) } -}; +} angular .module('codecraft') - .component(PersonCreateComponent.selector, PersonCreateComponent); \ No newline at end of file + .directive('personCreate', downgradeComponent({ + component: PersonCreateComponent + }) as angular.IDirectiveFactory); \ No newline at end of file diff --git a/src/app/components/person-edit.component.ts b/src/app/components/person-edit.component.ts index f4c5b51..7700262 100644 --- a/src/app/components/person-edit.component.ts +++ b/src/app/components/person-edit.component.ts @@ -1,73 +1,42 @@ -import * as angular from 'angular'; +import * as angular from "angular"; +import {Component, Inject} from "@angular/core"; +import {downgradeComponent} from "@angular/upgrade/static"; +import {ContactService} from "../services/contact.service"; +import {UIRouterStateParams, UIRouterState} from "../ajs-upgraded-providers"; -export let PersonEditComponent = { +@Component({ selector: 'personEdit', - template: ` -
-
-
-
- Edit -
- - - -
-
- -
-
- - - -
-
-
-
-`, - bindings: {}, - controller: class PersonCreateController { - - public contacts = null; - public person = {}; - - private $state = null; - private $stateParams = null; - - constructor($stateParams, $state, ContactService) { - this.$stateParams = $stateParams; - this.$state = $state; - this.contacts = ContactService; - this.person = this.contacts.getPerson(this.$stateParams.email); - } - - save() { - this.contacts.updateContact(this.person) - .then(() => { - this.$state.go("list"); - }) - } + templateUrl: 'app/components/person-modify.component.html' +}) +export class PersonEditComponent { + public mode: string = 'Edit'; + public person: any; + + constructor(@Inject(UIRouterStateParams) private $stateParams, + @Inject(UIRouterState) private $state, + private contacts: ContactService) { + this.person = this.contacts.getPerson(this.$stateParams.email); + } - remove() { - this.contacts.removeContact(this.person) - .then(() => { - this.$state.go("list"); - }) - } + save() { + this.contacts.updateContact(this.person) + .then(() => { + this.$state.go("list"); + }) + } + remove() { + this.contacts.removeContact(this.person) + .then(() => { + this.$state.go("list"); + }) } -}; +} angular .module('codecraft') - .component(PersonEditComponent.selector, PersonEditComponent); \ No newline at end of file + .directive('personEdit', downgradeComponent({ + inputs: ['mode'], + component: PersonEditComponent + }) as angular.IDirectiveFactory); \ No newline at end of file diff --git a/src/app/components/person-list.component.ts b/src/app/components/person-list.component.ts index 1b42806..0a7ba00 100644 --- a/src/app/components/person-list.component.ts +++ b/src/app/components/person-list.component.ts @@ -1,43 +1,49 @@ import * as angular from 'angular'; +import {Component} from "@angular/core"; +import {downgradeComponent} from "@angular/upgrade/static"; +import {ContactService} from "../services/contact.service"; - -export let PersonListComponent = { +@Component({ selector: 'personList', - template: ` -
- -
- - - - -
- -
-
-

No results found for search term '{{ $ctrl.search }}'

-
-
- - -
-`, - bindings: {}, - controller: class PersonListController { - public contacts = null; - - constructor(ContactService) { - this.contacts = ContactService; - } + template: `
+ +
+ + + + +
+ +
+
+

No results found for search term '{{ contacts.search }}'

+
+
+ + +
+` +}) +export class PersonListComponent { + constructor(public contacts: ContactService) { + } + + loadMore() { + console.log("loadMore"); + this.contacts.loadMore(); } -}; +} + angular .module('codecraft') - .component(PersonListComponent.selector, PersonListComponent); \ No newline at end of file + .directive('personList', downgradeComponent({ + component: PersonListComponent + }) as angular.IDirectiveFactory); \ No newline at end of file diff --git a/src/app/components/person-modify.component.html b/src/app/components/person-modify.component.html new file mode 100644 index 0000000..21c0c0a --- /dev/null +++ b/src/app/components/person-modify.component.html @@ -0,0 +1,134 @@ +
+
+ +
+
+ + {{mode}} + +
+ + + +
+
+ +
+
+ +
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ + +
+
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+
+ +
+ +
+ +
+ + +
+
+
+
\ No newline at end of file diff --git a/src/app/components/search.component.ts b/src/app/components/search.component.ts index 0ddc1e2..8e4ea63 100644 --- a/src/app/components/search.component.ts +++ b/src/app/components/search.component.ts @@ -1,26 +1,28 @@ import * as angular from 'angular'; +import {Component} from "@angular/core"; +import {downgradeComponent} from "@angular/upgrade/static"; +import {ContactService} from "../services/contact.service"; +import { + FormGroup, + FormControl +} from '@angular/forms'; -export let SearchComponent = { +@Component({ selector: 'search', template: ` - +` +}) +export class SpinnerComponent implements AfterViewInit { + @Input() + private isLoading: boolean; -angular - .module('codecraft') - .component(SpinnerComponent.selector, SpinnerComponent); \ No newline at end of file + @Input() + private message: string; + + @ViewChild('spinnerEl') + private spinnerEl: ElementRef; + + ngAfterViewInit() { + let spinner = new Spinner({radius: 8, width: 5, length: 3, lines: 9}); + spinner.spin(this.spinnerEl.nativeElement) + } +} \ No newline at end of file diff --git a/src/app/filters/default-image.filter.ts b/src/app/filters/default-image.filter.ts deleted file mode 100644 index e7debc6..0000000 --- a/src/app/filters/default-image.filter.ts +++ /dev/null @@ -1,15 +0,0 @@ -import * as angular from 'angular'; - -angular - .module('codecraft') - .filter('defaultImage', function () { - return function (input, param) { - if (!param) { - param = "/img/avatar.png" - } - if (!input) { - return param - } - return input; - } - }); \ No newline at end of file diff --git a/src/app/filters/index.ts b/src/app/filters/index.ts deleted file mode 100644 index 96a546b..0000000 --- a/src/app/filters/index.ts +++ /dev/null @@ -1 +0,0 @@ -import "./default-image.filter"; diff --git a/src/app/main.ts b/src/app/main.ts index 3aa4ef9..4517ff0 100644 --- a/src/app/main.ts +++ b/src/app/main.ts @@ -13,7 +13,7 @@ import 'reflect-metadata'; import './app.main'; import './services'; -import './filters'; +import './pipes'; import './components'; import './app.routes'; import './polyfills.ts'; @@ -23,24 +23,64 @@ import {NgModule} from '@angular/core'; import {BrowserModule} from '@angular/platform-browser'; import {UpgradeModule} from '@angular/upgrade/static'; import {HttpModule} from '@angular/http'; +import {FormsModule, ReactiveFormsModule} from '@angular/forms'; +import {InfiniteScrollModule} from 'angular2-infinite-scroll'; +import {LaddaModule} from "angular2-ladda/module/module"; import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; +import { + toasterServiceProvider, + uiRouterStateParamsProvider, + uiRouterStateProvider +} from "./ajs-upgraded-providers" -import {toasterServiceProvider} from "./ajs-upgraded-providers" import {Contact} from "./services/contact.resource"; import {ContactService} from "./services/contact.service"; +import {CardComponent} from "./components/card.component"; +import {SpinnerComponent} from "./components/spinner.component" +import {PersonListComponent} from "./components/person-list.component"; +import {PersonEditComponent} from "./components/person-edit.component"; +import {PersonCreateComponent} from "./components/person-create.component"; +import {SearchComponent} from "./components/search.component"; + +import {DefaultImagePipe} from "./pipes/default-image.pipe"; + + @NgModule({ imports: [ BrowserModule, UpgradeModule, - HttpModule + HttpModule, + LaddaModule, + FormsModule, + ReactiveFormsModule, + InfiniteScrollModule + ], + declarations: [ + CardComponent, + SpinnerComponent, + PersonListComponent, + DefaultImagePipe, + PersonEditComponent, + PersonCreateComponent, + SearchComponent + ], + entryComponents: [ + CardComponent, + SpinnerComponent, + PersonListComponent, + PersonEditComponent, + PersonCreateComponent, + SearchComponent ], providers: [ Contact, ContactService, - toasterServiceProvider - ] + toasterServiceProvider, + uiRouterStateParamsProvider, + uiRouterStateProvider + ], }) export class AppModule { // Override Angular bootstrap so it doesn't do anything diff --git a/src/app/pipes/default-image.pipe.ts b/src/app/pipes/default-image.pipe.ts new file mode 100644 index 0000000..be7c3d8 --- /dev/null +++ b/src/app/pipes/default-image.pipe.ts @@ -0,0 +1,14 @@ +import {Pipe, PipeTransform} from '@angular/core'; + +@Pipe({name: 'defaultImage'}) +export class DefaultImagePipe implements PipeTransform { + transform(input, def) { + if (!def) { + def = "/img/avatar.png" + } + if (!input) { + return def + } + return input; + } +} \ No newline at end of file diff --git a/src/app/pipes/index.ts b/src/app/pipes/index.ts new file mode 100644 index 0000000..ceafa13 --- /dev/null +++ b/src/app/pipes/index.ts @@ -0,0 +1 @@ +import "./default-image.pipe"; diff --git a/src/app/rxjs-operators.ts b/src/app/rxjs-operators.ts index 7e4249a..67a144a 100644 --- a/src/app/rxjs-operators.ts +++ b/src/app/rxjs-operators.ts @@ -1,3 +1,5 @@ import 'rxjs/add/operator/toPromise'; import 'rxjs/add/operator/map'; -import 'rxjs/add/operator/do'; \ No newline at end of file +import 'rxjs/add/operator/do'; +import 'rxjs/add/operator/debounceTime'; +import 'rxjs/add/operator/distinctUntilChanged'; \ No newline at end of file diff --git a/src/app/services/contact.service.ts b/src/app/services/contact.service.ts index 025c159..d6e3597 100644 --- a/src/app/services/contact.service.ts +++ b/src/app/services/contact.service.ts @@ -28,6 +28,7 @@ export class ContactService { return person; } } + return {}; } doSearch() {