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"