This commit is contained in:
Dat 2021-04-29 19:14:11 +08:00
parent 21a6ff53d8
commit cfc3c0a9e4
15 changed files with 9314 additions and 110 deletions

View File

@ -2,35 +2,41 @@ import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
import { AppComponent } from './app.component';
import { ComponentOverviewComponent } from './components/component-overview/component-overview.component';
import { HeroesComponent } from './components/heroes/heroes.component';
import { MessagesComponent } from './components/messages/messages.component';
import { HeroDetailComponent } from './components/hero-detail/hero-detail.component';
import { AppRoutingModule } from './app-routing.module';
import { DashboardComponent } from './components/dashboard/dashboard.component';
import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api';
import { InMemoryDataService } from './in-memory-data.service';
import { InMemoryDataService } from './services/in-memory-data.service';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { DashboardComponent } from './components/dashboard/dashboard.component';
import { HeroDetailComponent } from './components/hero-detail/hero-detail.component';
import { HeroesComponent } from './components/heroes/heroes.component';
import { HeroSearchComponent } from './components/hero-search/hero-search.component';
import { MessagesComponent } from './components/messages/messages.component';
@NgModule({
declarations: [
AppComponent,
ComponentOverviewComponent,
HeroesComponent,
MessagesComponent,
HeroDetailComponent,
DashboardComponent
],
imports: [
BrowserModule,
FormsModule,
HttpClientModule,
AppRoutingModule,
HttpClientModule,
// The HttpClientInMemoryWebApiModule module intercepts HTTP requests
// and returns simulated server responses.
// Remove it when a real server is ready to receive requests.
HttpClientInMemoryWebApiModule.forRoot(
InMemoryDataService, { dataEncapsulation: false }
)
],
providers: [],
bootstrap: [AppComponent]
declarations: [
AppComponent,
DashboardComponent,
HeroesComponent,
HeroDetailComponent,
MessagesComponent,
HeroSearchComponent
],
bootstrap: [ AppComponent ]
})
export class AppModule { }

View File

@ -1,4 +1,9 @@
<a *ngFor="let hero of heroes"
routerLink="/detail/{{hero.id}}">
{{hero.name}}
</a>
<h2>Top Heroes</h2>
<div class="heroes-menu">
<a *ngFor="let hero of heroes"
routerLink="/detail/{{hero.id}}">
{{hero.name}}
</a>
</div>
<app-hero-search></app-hero-search>

View File

@ -6,4 +6,5 @@
<input id="hero-name" [(ngModel)]="hero.name" placeholder="Hero name"/>
</div>
<button (click)="goBack()">go back</button>
<button (click)="save()">save</button>
</div>

View File

@ -1,4 +1,4 @@
import { Component, OnInit, Input } from '@angular/core';
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Location } from '@angular/common';
@ -11,7 +11,7 @@ import { HeroService } from '../../services/hero.service';
styleUrls: [ './hero-detail.component.scss' ]
})
export class HeroDetailComponent implements OnInit {
@Input() hero?: Hero;
hero: Hero;
constructor(
private route: ActivatedRoute,
@ -24,12 +24,17 @@ export class HeroDetailComponent implements OnInit {
}
getHero(): void {
const id = Number(this.route.snapshot.paramMap.get('id'));
this.heroService.getHero(id)
const id = this.route.snapshot.paramMap.get('id');
this.heroService.getHero(Number(id))
.subscribe(hero => this.hero = hero);
}
goBack(): void {
this.location.back();
}
save(): void {
this.heroService.updateHero(this.hero)
.subscribe(() => this.goBack());
}
}

View File

@ -0,0 +1,12 @@
<div id="search-component">
<label for="search-box">Hero Search</label>
<input #searchBox id="search-box" (input)="search(searchBox.value)" />
<ul class="search-result">
<li *ngFor="let hero of heroes$ | async" >
<a routerLink="/detail/{{hero.id}}">
{{hero.name}}
</a>
</li>
</ul>
</div>

View File

@ -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;
}

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HeroSearchComponent } from './hero-search.component';
describe('HeroSearchComponent', () => {
let component: HeroSearchComponent;
let fixture: ComponentFixture<HeroSearchComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ HeroSearchComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(HeroSearchComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,40 @@
import { Component, OnInit } from '@angular/core';
import { Observable, Subject } from 'rxjs';
import {
debounceTime, distinctUntilChanged, switchMap
} from 'rxjs/operators';
import { Hero } from '../../interface/hero.interface';
import { HeroService } from '../../services/hero.service';
@Component({
selector: 'app-hero-search',
templateUrl: './hero-search.component.html',
styleUrls: [ './hero-search.component.scss' ]
})
export class HeroSearchComponent implements OnInit {
heroes$: Observable<Hero[]>;
private searchTerms = new Subject<string>();
constructor(private heroService: HeroService) {}
// Push a search term into the observable stream.
search(term: string): void {
this.searchTerms.next(term);
}
ngOnInit(): void {
this.heroes$ = this.searchTerms.pipe(
// wait 300ms after each keystroke before considering the term
debounceTime(300),
// ignore new term if same as previous term
distinctUntilChanged(),
// switch to new search observable each time the term changes
switchMap((term: string) => this.heroService.searchHeroes(term)),
);
}
}

View File

@ -1,8 +1,21 @@
<h2>My Heroes</h2>
<div>
<label id="new-hero">Hero name: </label>
<input for="new-hero" #heroName />
<!-- (click) passes input value to add() and then clears the input -->
<button class="add-button" (click)="add(heroName.value); heroName.value=''">
Add hero
</button>
</div>
<ul class="heroes">
<li *ngFor="let hero of heroes">
<a routerLink="/detail/{{hero.id}}">
<span class="badge">{{hero.id}}</span> {{hero.name}}
</a>
</li>
</ul>
<app-hero-detail [hero]="selectedHero"></app-hero-detail>
<li *ngFor="let hero of heroes">
<a routerLink="/detail/{{hero.id}}">
<span class="badge">{{hero.id}}</span> {{hero.name}}
</a>
<button class="delete" title="delete hero"
(click)="delete(hero)">x</button>
</li>
</ul>

View File

@ -1,52 +1,89 @@
/* HeroesComponent's private CSS styles */
.heroes {
margin: 0 0 2em 0;
list-style-type: none;
padding: 0;
width: 15em;
}
.heroes li {
cursor: pointer;
position: relative;
left: 0;
background-color: #EEE;
margin: .5em;
padding: .3em 0;
height: 1.6em;
border-radius: 4px;
}
.heroes li:hover {
color: #2c3a41;
background-color: #e6e6e6;
left: .1em;
}
.heroes li.selected {
background-color: black;
color: white;
}
.heroes li.selected:hover {
background-color: #505050;
color: white;
}
.heroes li.selected:active {
background-color: black;
color: white;
}
.heroes .badge {
display: inline-block;
font-size: small;
color: white;
padding: 0.8em 0.7em 0 0.7em;
background-color:#405061;
line-height: 1em;
position: relative;
left: -1px;
top: -4px;
height: 1.8em;
margin-right: .8em;
border-radius: 4px 0 0 4px;
}
margin: 0 0 2em 0;
list-style-type: none;
padding: 0;
width: 15em;
}
input {
padding: .5rem;
}
input {
display: block;
width: 100%;
padding: .5rem;
margin: 1rem 0;
box-sizing: border-box;
}
.heroes li {
position: relative;
cursor: pointer;
}
.heroes li:hover {
left: .1em;
}
.heroes a {
color: #333;
text-decoration: none;
background-color: #EEE;
margin: .5em;
padding: .3em 0;
height: 1.6em;
border-radius: 4px;
display: block;
width: 100%;
}
.heroes a:hover {
color: #2c3a41;
background-color: #e6e6e6;
}
.heroes a:active {
background-color: #525252;
color: #fafafa;
}
.heroes .badge {
display: inline-block;
font-size: small;
color: white;
padding: 0.8em 0.7em 0 0.7em;
background-color:#405061;
line-height: 1em;
position: relative;
left: -1px;
top: -4px;
height: 1.8em;
min-width: 16px;
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;
padding: 1px 10px 3px 10px;
}
button.delete:hover {
background-color: #525252;
color: white;
}

View File

@ -1,7 +1,7 @@
import { Component, OnInit } from '@angular/core';
import { Hero } from '../../interface/hero.interface';
import { HeroService } from '../../services/hero.service';
import { MessageService } from '../../services/message.service';
@Component({
selector: 'app-heroes',
@ -9,23 +9,31 @@ import { MessageService } from '../../services/message.service';
styleUrls: ['./heroes.component.scss']
})
export class HeroesComponent implements OnInit {
heroes: Hero[];
selectedHero?: Hero;
heroes: Hero[] = [];
constructor(private heroService: HeroService, private messageService: MessageService) { }
constructor(private heroService: HeroService) { }
ngOnInit() {
this.getHeroes();
}
onSelect(hero: Hero): void {
console.log("heroes.component.ts excite onSelect")
this.selectedHero = hero;
this.messageService.add(`HeroesComponent: Selected hero id=${hero.id}`);
}
getHeroes(): void {
this.heroService.getHeroes()
.subscribe(heroes => this.heroes = heroes); }
.subscribe(heroes => this.heroes = heroes);
}
add(name: string): void {
name = name.trim();
if (!name) { return; }
this.heroService.addHero({ name } as Hero)
.subscribe(hero => {
this.heroes.push(hero);
});
}
delete(hero: Hero): void {
this.heroes = this.heroes.filter(h => h !== hero);
this.heroService.deleteHero(hero.id).subscribe();
}
}

View File

@ -1,27 +1,122 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { catchError, map, tap } from 'rxjs/operators';
import { Hero } from '../interface/hero.interface';
import { HEROES } from '../interface/mock-heroes';
import { MessageService } from './message.service';
@Injectable({
providedIn: 'root',
})
@Injectable({ providedIn: 'root' })
export class HeroService {
constructor(private messageService: MessageService) { }
private heroesUrl = 'api/heroes'; // URL to web api
httpOptions = {
headers: new HttpHeaders({ 'Content-Type': 'application/json' })
};
constructor(
private http: HttpClient,
private messageService: MessageService) { }
/** GET heroes from the server */
getHeroes(): Observable<Hero[]> {
const heroes = of(HEROES);
this.messageService.add('HeroService: fetched heroes');
return heroes;
return this.http.get<Hero[]>(this.heroesUrl)
.pipe(
tap(_ => this.log('fetched heroes')),
catchError(this.handleError<Hero[]>('getHeroes', []))
);
}
/** GET hero by id. Return `undefined` when id not found */
getHeroNo404<Data>(id: number): Observable<Hero> {
const url = `${this.heroesUrl}/?id=${id}`;
return this.http.get<Hero[]>(url)
.pipe(
map(heroes => heroes[0]), // returns a {0|1} element array
tap(h => {
const outcome = h ? `fetched` : `did not find`;
this.log(`${outcome} hero id=${id}`);
}),
catchError(this.handleError<Hero>(`getHero id=${id}`))
);
}
/** GET hero by id. Will 404 if id not found */
getHero(id: number): Observable<Hero> {
// For now, assume that a hero with the specified `id` always exists.
// Error handling will be added in the next step of the tutorial.
const hero = HEROES.find(h => h.id === id) as Hero;
this.messageService.add(`HeroService: fetched hero id=${id}`);
return of(hero);
const url = `${this.heroesUrl}/${id}`;
return this.http.get<Hero>(url).pipe(
tap(_ => this.log(`fetched hero id=${id}`)),
catchError(this.handleError<Hero>(`getHero id=${id}`))
);
}
/* GET heroes whose name contains search term */
searchHeroes(term: string): Observable<Hero[]> {
if (!term.trim()) {
// if not search term, return empty hero array.
return of([]);
}
return this.http.get<Hero[]>(`${this.heroesUrl}/?name=${term}`).pipe(
tap(x => x.length ?
this.log(`found heroes matching "${term}"`) :
this.log(`no heroes matching "${term}"`)),
catchError(this.handleError<Hero[]>('searchHeroes', []))
);
}
//////// Save methods //////////
/** POST: add a new hero to the server */
addHero(hero: Hero): Observable<Hero> {
return this.http.post<Hero>(this.heroesUrl, hero, this.httpOptions).pipe(
tap((newHero: Hero) => this.log(`added hero w/ id=${newHero.id}`)),
catchError(this.handleError<Hero>('addHero'))
);
}
/** DELETE: delete the hero from the server */
deleteHero(id: number): Observable<Hero> {
const url = `${this.heroesUrl}/${id}`;
return this.http.delete<Hero>(url, this.httpOptions).pipe(
tap(_ => this.log(`deleted hero id=${id}`)),
catchError(this.handleError<Hero>('deleteHero'))
);
}
/** PUT: update the hero on the server */
updateHero(hero: Hero): Observable<any> {
return this.http.put(this.heroesUrl, hero, this.httpOptions).pipe(
tap(_ => this.log(`updated hero id=${hero.id}`)),
catchError(this.handleError<any>('updateHero'))
);
}
/**
* Handle Http operation that failed.
* Let the app continue.
* @param operation - name of the operation that failed
* @param result - optional value to return as the observable result
*/
private handleError<T>(operation = 'operation', result?: T) {
return (error: any): Observable<T> => {
// TODO: send the error to remote logging infrastructure
console.error(error); // log to console instead
// TODO: better job of transforming error for user consumption
this.log(`${operation} failed: ${error.message}`);
// Let the app keep running by returning an empty result.
return of(result as T);
};
}
/** Log a HeroService message with the MessageService */
private log(message: string) {
this.messageService.add(`HeroService: ${message}`);
}
}

View File

@ -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();
});
});

View File

@ -0,0 +1,33 @@
import { Injectable } from '@angular/core';
import { InMemoryDbService } from 'angular-in-memory-web-api';
import { Hero } from '../interface/hero.interface';
@Injectable({
providedIn: 'root',
})
export class InMemoryDataService implements InMemoryDbService {
createDb() {
const heroes = [
{ id: 11, name: 'Dr Nice' },
{ id: 12, name: 'Narco' },
{ 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};
}
// Overrides the genId method to ensure that a hero always has an id.
// If the heroes array is empty,
// the method below returns the initial number (11).
// if the heroes array is not empty, the method below returns the highest
// hero id + 1.
genId(heroes: Hero[]): number {
return heroes.length > 0 ? Math.max(...heroes.map(hero => hero.id)) + 1 : 11;
}
}

8861
yarn.lock Normal file

File diff suppressed because it is too large Load Diff