From 0fb191a129b0eda80096320bf49e8d62574ed44c Mon Sep 17 00:00:00 2001 From: nikurasu Date: Fri, 30 Sep 2022 11:30:08 +0200 Subject: [PATCH] Completed Angular Tour of Heroes --- package.json | 1 + src/app/app.module.ts | 13 ++- src/app/dashboard/dashboard.component.html | 2 + src/app/dashboard/dashboard.component.scss | 94 ++++++++++--------- .../hero-detail/hero-detail.component.html | 1 + .../hero-detail/hero-detail.component.scss | 45 ++++----- src/app/hero-detail/hero-detail.component.ts | 6 ++ .../hero-search/hero-search.component.html | 13 +++ .../hero-search/hero-search.component.scss | 47 ++++++++++ .../hero-search/hero-search.component.spec.ts | 23 +++++ src/app/hero-search/hero-search.component.ts | 31 ++++++ src/app/hero.service.ts | 72 ++++++++++++-- src/app/heroes/heroes.component.html | 10 ++ src/app/heroes/heroes.component.scss | 36 +++++++ src/app/heroes/heroes.component.ts | 11 +++ src/app/in-memory-data.service.spec.ts | 16 ++++ src/app/in-memory-data.service.ts | 29 ++++++ yarn.lock | 7 ++ 18 files changed, 380 insertions(+), 77 deletions(-) create mode 100644 src/app/hero-search/hero-search.component.html create mode 100644 src/app/hero-search/hero-search.component.scss create mode 100644 src/app/hero-search/hero-search.component.spec.ts create mode 100644 src/app/hero-search/hero-search.component.ts create mode 100644 src/app/in-memory-data.service.spec.ts create mode 100644 src/app/in-memory-data.service.ts diff --git a/package.json b/package.json index 90e048b..df19979 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "@angular/cli": "~14.2.3", "@angular/compiler-cli": "^14.2.0", "@types/jasmine": "~4.0.0", + "angular-in-memory-web-api": "^0.14.0", "jasmine-core": "~4.3.0", "karma": "~6.4.0", "karma-chrome-launcher": "~3.1.0", diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 4ddf6f4..db6aa81 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,6 +1,9 @@ import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; +import { HttpClientModule } from '@angular/common/http'; +import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api'; +import { InMemoryDataService } from './in-memory-data.service'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; @@ -8,6 +11,7 @@ import { HeroesComponent } from './heroes/heroes.component'; import { HeroDetailComponent } from './hero-detail/hero-detail.component'; import { MessagesComponent } from './messages/messages.component'; import { DashboardComponent } from './dashboard/dashboard.component'; +import { HeroSearchComponent } from './hero-search/hero-search.component'; @NgModule({ declarations: [ @@ -15,12 +19,17 @@ import { DashboardComponent } from './dashboard/dashboard.component'; HeroesComponent, HeroDetailComponent, MessagesComponent, - DashboardComponent + DashboardComponent, + HeroSearchComponent ], imports: [ BrowserModule, FormsModule, - AppRoutingModule + AppRoutingModule, + HttpClientModule, + HttpClientInMemoryWebApiModule.forRoot( + InMemoryDataService, {dataEncapsulation: false}, + ), ], providers: [], bootstrap: [AppComponent] diff --git a/src/app/dashboard/dashboard.component.html b/src/app/dashboard/dashboard.component.html index f9f58dd..7c2352a 100644 --- a/src/app/dashboard/dashboard.component.html +++ b/src/app/dashboard/dashboard.component.html @@ -4,3 +4,5 @@ {{hero.name}} + + diff --git a/src/app/dashboard/dashboard.component.scss b/src/app/dashboard/dashboard.component.scss index e9d534d..f3036dd 100644 --- a/src/app/dashboard/dashboard.component.scss +++ b/src/app/dashboard/dashboard.component.scss @@ -1,50 +1,54 @@ /* DashboardComponent's private CSS styles */ h2 { - text-align: center; - } - - .heroes-menu { - padding: 0; - margin: auto; - max-width: 1000px; - - /* flexbox */ - display: flex; - flex-direction: row; - flex-wrap: wrap; - justify-content: space-around; - align-content: flex-start; - align-items: flex-start; - } - + text-align: center; +} + +.heroes-menu { + padding: 0; + margin: auto; + max-width: 1000px; + + /* flexbox */ + display: -webkit-box; + display: -moz-box; + display: -ms-flexbox; + display: -webkit-flex; + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: space-around; + align-content: flex-start; + align-items: flex-start; +} + +a { + background-color: #3f525c; + border-radius: 2px; + padding: 1rem; + font-size: 1.2rem; + text-decoration: none; + display: inline-block; + color: #fff; + text-align: center; + width: 100%; + min-width: 70px; + margin: .5rem auto; + box-sizing: border-box; + + /* flexbox */ + order: 0; + flex: 0 1 auto; + align-self: auto; +} + +@media (min-width: 600px) { a { - background-color: #3f525c; - border-radius: 2px; - padding: 1rem; - font-size: 1.2rem; - text-decoration: none; - display: inline-block; - color: #fff; - text-align: center; - width: 100%; - min-width: 70px; - margin: .5rem auto; - box-sizing: border-box; - - /* flexbox */ - order: 0; - flex: 0 1 auto; - align-self: auto; + width: 18%; + box-sizing: content-box; } - - @media (min-width: 600px) { - a { - width: 18%; - box-sizing: content-box; - } - } - - a:hover { - background-color: #000; - } \ No newline at end of file +} + +a:hover { + background-color: black; +} \ No newline at end of file diff --git a/src/app/hero-detail/hero-detail.component.html b/src/app/hero-detail/hero-detail.component.html index 6af9e5b..57f51ee 100644 --- a/src/app/hero-detail/hero-detail.component.html +++ b/src/app/hero-detail/hero-detail.component.html @@ -6,4 +6,5 @@ + \ No newline at end of file diff --git a/src/app/hero-detail/hero-detail.component.scss b/src/app/hero-detail/hero-detail.component.scss index 7509fb2..ea759bd 100644 --- a/src/app/hero-detail/hero-detail.component.scss +++ b/src/app/hero-detail/hero-detail.component.scss @@ -1,24 +1,25 @@ /* HeroDetailComponent's private CSS styles */ label { - color: #435960; - font-weight: bold; - } - input { - font-size: 1em; - padding: .5rem; - } - button { - margin-top: 20px; - background-color: #eee; - padding: 1rem; - border-radius: 4px; - font-size: 1rem; - } - button:hover { - background-color: #cfd8dc; - } - button:disabled { - background-color: #eee; - color: #ccc; - cursor: auto; - } \ No newline at end of file + color: #435960; + font-weight: bold; +} +input { + font-size: 1em; + padding: .5rem; +} +button { + margin-top: 20px; + margin-right: .5rem; + background-color: #eee; + padding: 1rem; + border-radius: 4px; + font-size: 1rem; +} +button:hover { + background-color: #cfd8dc; +} +button:disabled { + background-color: #eee; + color: #ccc; + cursor: auto; +} \ No newline at end of file diff --git a/src/app/hero-detail/hero-detail.component.ts b/src/app/hero-detail/hero-detail.component.ts index 166d61e..89e6cd9 100644 --- a/src/app/hero-detail/hero-detail.component.ts +++ b/src/app/hero-detail/hero-detail.component.ts @@ -32,4 +32,10 @@ export class HeroDetailComponent implements OnInit { this.location.back() } + save(): void { + if(this.hero){ + this.heroService.updateHero(this.hero).subscribe(() => this.goBack()) + } + } + } diff --git a/src/app/hero-search/hero-search.component.html b/src/app/hero-search/hero-search.component.html new file mode 100644 index 0000000..d658001 --- /dev/null +++ b/src/app/hero-search/hero-search.component.html @@ -0,0 +1,13 @@ +
+ + + + +
diff --git a/src/app/hero-search/hero-search.component.scss b/src/app/hero-search/hero-search.component.scss new file mode 100644 index 0000000..e420c33 --- /dev/null +++ b/src/app/hero-search/hero-search.component.scss @@ -0,0 +1,47 @@ +/* HeroSearch private styles */ + +label { + display: block; + font-weight: bold; + font-size: 1.2rem; + margin-top: 1rem; + margin-bottom: .5rem; + + } + input { + padding: .5rem; + width: 100%; + max-width: 600px; + box-sizing: border-box; + display: block; + } + + input:focus { + outline: #336699 auto 1px; + } + + li { + list-style-type: none; + } + .search-result li a { + border-bottom: 1px solid gray; + border-left: 1px solid gray; + border-right: 1px solid gray; + display: inline-block; + width: 100%; + max-width: 600px; + padding: .5rem; + box-sizing: border-box; + text-decoration: none; + color: black; + } + + .search-result li a:hover { + background-color: #435A60; + color: white; + } + + ul.search-result { + margin-top: 0; + padding-left: 0; + } \ No newline at end of file diff --git a/src/app/hero-search/hero-search.component.spec.ts b/src/app/hero-search/hero-search.component.spec.ts new file mode 100644 index 0000000..574eeda --- /dev/null +++ b/src/app/hero-search/hero-search.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { HeroSearchComponent } from './hero-search.component'; + +describe('HeroSearchComponent', () => { + let component: HeroSearchComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ HeroSearchComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(HeroSearchComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/hero-search/hero-search.component.ts b/src/app/hero-search/hero-search.component.ts new file mode 100644 index 0000000..056f043 --- /dev/null +++ b/src/app/hero-search/hero-search.component.ts @@ -0,0 +1,31 @@ +import { Component, OnInit } from '@angular/core'; +import { debounceTime, distinctUntilChanged, Observable, Subject, switchMap } from 'rxjs'; +import { Hero } from '../hero'; +import { HeroService } from '../hero.service'; + +@Component({ + selector: 'app-hero-search', + templateUrl: './hero-search.component.html', + styleUrls: ['./hero-search.component.scss'] +}) +export class HeroSearchComponent implements OnInit { + heroes$!: Observable + private searchTerms = new Subject() + + constructor( + private heroService: HeroService + ) { } + + search(term: string): void { + this.searchTerms.next(term) + } + + ngOnInit(): void { + this.heroes$ = this.searchTerms.pipe( + debounceTime(300), + distinctUntilChanged(), + switchMap((term: string) => this.heroService.searchHeroes(term)) + ) + } + +} diff --git a/src/app/hero.service.ts b/src/app/hero.service.ts index eece890..60dcf2c 100644 --- a/src/app/hero.service.ts +++ b/src/app/hero.service.ts @@ -1,5 +1,6 @@ +import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { Observable, of } from 'rxjs'; +import { catchError, Observable, of, pipe, tap } from 'rxjs'; import { Hero } from './hero'; import { MessageService } from './messages.service'; import { HEROES } from './mock-heroes'; @@ -8,18 +9,73 @@ import { HEROES } from './mock-heroes'; providedIn: 'root' }) export class HeroService { + httpOptions = { + headers: new HttpHeaders({ 'Content-Type': 'application/json' }) + }; - constructor(private messageService: MessageService) { } + private heroesUrl = 'api/heroes' + + constructor(private messageService: MessageService, private http: HttpClient) { } getHeroes(): Observable { - const heroes = of(HEROES) - this.messageService.add('HeroService: Heroes fetched!') - return heroes + return this.http.get(this.heroesUrl).pipe( + tap(_ => this.log('fetched Heroes')), + catchError(this.handleError('getHeroes', [])) + ) } getHero(id: number): Observable { - const hero = HEROES.find(h => h.id === id)! - this.messageService.add(`HeroService: fetched hero id=${id}`) - return of(hero) + const url = `${this.heroesUrl}/${id}` + return this.http.get(url).pipe( + tap(_ => this.log(`fetched hero id=${id}`)), + catchError(this.handleError(`getHero id=${id}`)) + ) + } + + updateHero(hero: Hero): Observable { + return this.http.put(this.heroesUrl, hero, this.httpOptions).pipe( + tap(_ => this.log(`updated hero id=${hero.id}`)), + catchError(this.handleError(`updatedHero`)) + ) + } + + addHero(hero: Hero): Observable { + return this.http.post(this.heroesUrl, hero, this.httpOptions).pipe( + tap((newHero: Hero) => this.log(`added hero w/ id=${newHero.id}`)), + catchError(this.handleError('addHero')) + ) + } + + deleteHero(id: number): Observable { + const url = `${this.heroesUrl}/${id}` + + return this.http.delete(url, this.httpOptions).pipe( + tap(_ => this.log(`deleted hero id=${id}`)), + catchError(this.handleError('deleteHero')) + ) + } + + searchHeroes(term: string): Observable { + if (!term.trim()) { + return of([]) + } + return this.http.get(`${this.heroesUrl}/?name=${term}`).pipe( + tap(x => x.length ? + this.log(`found heroes matching "${term}"`) : + this.log(`no heroes matching "${term}"`)), + catchError(this.handleError('searchHeroes', [])) + ) + } + + private log(message: string) { + this.messageService.add(`HeroService: ${message}`) + } + + private handleError(operation = 'operation', result?: T) { + return (error: any): Observable => { + console.log(error); + this.log(`${operation} failed: ${error} message`) + return of(result as T) + } } } diff --git a/src/app/heroes/heroes.component.html b/src/app/heroes/heroes.component.html index 6ad5eee..8775abc 100644 --- a/src/app/heroes/heroes.component.html +++ b/src/app/heroes/heroes.component.html @@ -1,8 +1,18 @@

Heroes

+
+ + + + + +
\ No newline at end of file diff --git a/src/app/heroes/heroes.component.scss b/src/app/heroes/heroes.component.scss index 4bf53ff..5f7c543 100644 --- a/src/app/heroes/heroes.component.scss +++ b/src/app/heroes/heroes.component.scss @@ -5,6 +5,15 @@ padding: 0; width: 15em; } + +input { + display: block; + width: 100%; + padding: .5rem; + margin: 1rem 0; + box-sizing: border-box; +} + .heroes li { position: relative; cursor: pointer; @@ -51,4 +60,31 @@ text-align: right; margin-right: .8em; border-radius: 4px 0 0 4px; +} + +.add-button { + padding: .5rem 1.5rem; + font-size: 1rem; + margin-bottom: 2rem; +} + +.add-button:hover { + color: white; + background-color: #42545C; +} + +button.delete { + position: absolute; + left: 210px; + top: 5px; + background-color: white; + color: #525252; + font-size: 1.1rem; + margin: 0; + padding: 1px 10px 3px 10px; +} + +button.delete:hover { + background-color: #525252; + color: white; } \ No newline at end of file diff --git a/src/app/heroes/heroes.component.ts b/src/app/heroes/heroes.component.ts index 35da9a0..4869bb9 100644 --- a/src/app/heroes/heroes.component.ts +++ b/src/app/heroes/heroes.component.ts @@ -22,4 +22,15 @@ export class HeroesComponent implements OnInit { this.heroService.getHeroes().subscribe(heroes => this.heroes = heroes) } + add(name: string): void { + name = name.trim() + if (!name) { return } + this.heroService.addHero({ name } as Hero).subscribe((hero: Hero) => {this.heroes.push(hero)}) + } + + delete(hero: Hero): void { + this.heroes = this.heroes.filter(h => h !== hero) + this.heroService.deleteHero(hero.id).subscribe() + } + } diff --git a/src/app/in-memory-data.service.spec.ts b/src/app/in-memory-data.service.spec.ts new file mode 100644 index 0000000..eefd761 --- /dev/null +++ b/src/app/in-memory-data.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { InMemoryDataService } from './in-memory-data.service'; + +describe('InMemoryDataService', () => { + let service: InMemoryDataService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(InMemoryDataService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/in-memory-data.service.ts b/src/app/in-memory-data.service.ts new file mode 100644 index 0000000..028a3a0 --- /dev/null +++ b/src/app/in-memory-data.service.ts @@ -0,0 +1,29 @@ +import { Injectable } from '@angular/core'; +import { InMemoryDbService } from 'angular-in-memory-web-api'; +import { Hero } from './hero'; + +@Injectable({ + providedIn: 'root' +}) +export class InMemoryDataService implements InMemoryDbService { + createDb() { + const heroes = [ + { id: 12, name: 'Dr. Nice' }, + { id: 13, name: 'Bombasto' }, + { id: 14, name: 'Celeritas' }, + { id: 15, name: 'Magneta' }, + { id: 16, name: 'RubberMan' }, + { id: 17, name: 'Dynama' }, + { id: 18, name: 'Dr. IQ' }, + { id: 19, name: 'Magma' }, + { id: 20, name: 'Tornado' } + ] + return {heroes} + } + + genId(heroes: Hero[]): number { + return heroes.length > 0 ? Math.max(...heroes.map(hero => hero.id)) + 1 : 11 + } + + constructor() { } +} diff --git a/yarn.lock b/yarn.lock index 5f440a1..9267728 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1876,6 +1876,13 @@ ajv@^6.12.4, ajv@^6.12.5: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +angular-in-memory-web-api@^0.14.0: + version "0.14.0" + resolved "https://registry.yarnpkg.com/angular-in-memory-web-api/-/angular-in-memory-web-api-0.14.0.tgz#4f8aae27d5e59ecb7acbf17ceefb3393c30085a3" + integrity sha512-8RLFBpXZONDQxYGKiheaYQQl3iydesCrhWLuzDD6AsQDcOF+HEvIuOfBdJaTWKfqyNZNWjHvXzIyT0bUIunb/A== + dependencies: + tslib "^2.3.0" + ansi-colors@4.1.3: version "4.1.3" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b"