Tuto
This commit is contained in:
parent
f654108cab
commit
4b75f29e54
5
package-lock.json
generated
5
package-lock.json
generated
@ -473,6 +473,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"angular-in-memory-web-api": {
|
||||
"version": "0.5.3",
|
||||
"resolved": "https://registry.npmjs.org/angular-in-memory-web-api/-/angular-in-memory-web-api-0.5.3.tgz",
|
||||
"integrity": "sha512-1QPwwXG8R/2s7EbHh13HDiJYsk4sdBHNxHJHZHJ/Kxb4T9OG+bb1kGcXzY9UrJkEVxOtUW0ozvL4p/HmeIEszg=="
|
||||
},
|
||||
"ansi-html": {
|
||||
"version": "0.0.7",
|
||||
"resolved": "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.7.tgz",
|
||||
|
@ -21,6 +21,7 @@
|
||||
"@angular/platform-browser": "^5.2.0",
|
||||
"@angular/platform-browser-dynamic": "^5.2.0",
|
||||
"@angular/router": "^5.2.0",
|
||||
"angular-in-memory-web-api": "^0.5.3",
|
||||
"core-js": "^2.4.1",
|
||||
"rxjs": "^5.5.6",
|
||||
"zone.js": "^0.8.19"
|
||||
|
18
src/app/app-routing.module.ts
Normal file
18
src/app/app-routing.module.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { HeroesComponent } from './heroes/heroes.component';
|
||||
import { DashboardComponent } from './dashboard/dashboard.component';
|
||||
import { HeroDetailComponent } from './hero-detail/hero-detail.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: 'heroes', component: HeroesComponent },
|
||||
{ path: 'dashboard', component: DashboardComponent },
|
||||
{ path: 'detail/:id', component: HeroDetailComponent },
|
||||
{ path: '', redirectTo: '/dashboard', pathMatch: 'full' }
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [ RouterModule.forRoot(routes) ],
|
||||
exports: [ RouterModule ]
|
||||
})
|
||||
export class AppRoutingModule { }
|
@ -0,0 +1,29 @@
|
||||
/* AppComponent's private CSS styles */
|
||||
h1 {
|
||||
font-size: 1.2em;
|
||||
color: #999;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
h2 {
|
||||
font-size: 2em;
|
||||
margin-top: 0;
|
||||
padding-top: 0;
|
||||
}
|
||||
nav a {
|
||||
padding: 5px 10px;
|
||||
text-decoration: none;
|
||||
margin-top: 10px;
|
||||
display: inline-block;
|
||||
background-color: #eee;
|
||||
border-radius: 4px;
|
||||
}
|
||||
nav a:visited, a:link {
|
||||
color: #607D8B;
|
||||
}
|
||||
nav a:hover {
|
||||
color: #039be5;
|
||||
background-color: #CFD8DC;
|
||||
}
|
||||
nav a.active {
|
||||
color: #039be5;
|
||||
}
|
@ -1,20 +1,7 @@
|
||||
<!--The content below is only a placeholder and can be replaced.-->
|
||||
<div style="text-align:center">
|
||||
<h1>
|
||||
Welcome to {{ title }}!
|
||||
</h1>
|
||||
<img width="300" alt="Angular Logo" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTAgMjUwIj4KICAgIDxwYXRoIGZpbGw9IiNERDAwMzEiIGQ9Ik0xMjUgMzBMMzEuOSA2My4ybDE0LjIgMTIzLjFMMTI1IDIzMGw3OC45LTQzLjcgMTQuMi0xMjMuMXoiIC8+CiAgICA8cGF0aCBmaWxsPSIjQzMwMDJGIiBkPSJNMTI1IDMwdjIyLjItLjFWMjMwbDc4LjktNDMuNyAxNC4yLTEyMy4xTDEyNSAzMHoiIC8+CiAgICA8cGF0aCAgZmlsbD0iI0ZGRkZGRiIgZD0iTTEyNSA1Mi4xTDY2LjggMTgyLjZoMjEuN2wxMS43LTI5LjJoNDkuNGwxMS43IDI5LjJIMTgzTDEyNSA1Mi4xem0xNyA4My4zaC0zNGwxNy00MC45IDE3IDQwLjl6IiAvPgogIDwvc3ZnPg==">
|
||||
</div>
|
||||
<h2>Here are some links to help you start: </h2>
|
||||
<ul>
|
||||
<li>
|
||||
<h2><a target="_blank" rel="noopener" href="https://angular.io/tutorial">Tour of Heroes</a></h2>
|
||||
</li>
|
||||
<li>
|
||||
<h2><a target="_blank" rel="noopener" href="https://github.com/angular/angular-cli/wiki">CLI Documentation</a></h2>
|
||||
</li>
|
||||
<li>
|
||||
<h2><a target="_blank" rel="noopener" href="https://blog.angular.io/">Angular blog</a></h2>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h1> {{ title }} </h1>
|
||||
<nav>
|
||||
<a routerLink="/dashboard">Dashboard</a>
|
||||
<a routerLink="/heroes">Heroes</a>
|
||||
</nav>
|
||||
<router-outlet></router-outlet>
|
||||
<app-messages></app-messages>
|
||||
|
@ -6,5 +6,5 @@ import { Component } from '@angular/core';
|
||||
styleUrls: ['./app.component.css']
|
||||
})
|
||||
export class AppComponent {
|
||||
title = 'app';
|
||||
title = 'Tour of Heroes';
|
||||
}
|
||||
|
@ -1,18 +1,48 @@
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { NgModule } from '@angular/core';
|
||||
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { HttpClientModule } from '@angular/common/http';
|
||||
|
||||
import { AppComponent } from './app.component';
|
||||
import { HeroesComponent } from './heroes/heroes.component';
|
||||
import { HeroDetailComponent } from './hero-detail/hero-detail.component';
|
||||
import { HeroService } from './hero.service';
|
||||
import { MessagesComponent } from './messages/messages.component';
|
||||
import { MessageService } from './message.service';
|
||||
import { AppRoutingModule } from './/app-routing.module';
|
||||
import { DashboardComponent } from './dashboard/dashboard.component';
|
||||
import { HeroSearchComponent } from './hero-search/hero-search.component';
|
||||
|
||||
import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api';
|
||||
import { InMemoryDataService } from './in-memory-data.service';
|
||||
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent
|
||||
AppComponent,
|
||||
HeroesComponent,
|
||||
HeroDetailComponent,
|
||||
MessagesComponent,
|
||||
DashboardComponent,
|
||||
HeroSearchComponent
|
||||
],
|
||||
imports: [
|
||||
BrowserModule
|
||||
BrowserModule,
|
||||
FormsModule,
|
||||
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: [
|
||||
HeroService,
|
||||
MessageService,
|
||||
],
|
||||
providers: [],
|
||||
bootstrap: [AppComponent]
|
||||
})
|
||||
export class AppModule { }
|
||||
|
62
src/app/dashboard/dashboard.component.css
Normal file
62
src/app/dashboard/dashboard.component.css
Normal file
@ -0,0 +1,62 @@
|
||||
/* DashboardComponent's private CSS styles */
|
||||
[class*='col-'] {
|
||||
float: left;
|
||||
padding-right: 20px;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
[class*='col-']:last-of-type {
|
||||
padding-right: 0;
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
*, *:after, *:before {
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
h3 {
|
||||
text-align: center; margin-bottom: 0;
|
||||
}
|
||||
h4 {
|
||||
position: relative;
|
||||
}
|
||||
.grid {
|
||||
margin: 0;
|
||||
}
|
||||
.col-1-4 {
|
||||
width: 25%;
|
||||
}
|
||||
.module {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: #eee;
|
||||
max-height: 120px;
|
||||
min-width: 120px;
|
||||
background-color: #607D8B;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.module:hover {
|
||||
background-color: #EEE;
|
||||
cursor: pointer;
|
||||
color: #607d8b;
|
||||
}
|
||||
.grid-pad {
|
||||
padding: 10px 0;
|
||||
}
|
||||
.grid-pad > [class*='col-']:last-of-type {
|
||||
padding-right: 20px;
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.module {
|
||||
font-size: 10px;
|
||||
max-height: 75px; }
|
||||
}
|
||||
@media (max-width: 1024px) {
|
||||
.grid {
|
||||
margin: 0;
|
||||
}
|
||||
.module {
|
||||
min-width: 60px;
|
||||
}
|
||||
}
|
11
src/app/dashboard/dashboard.component.html
Normal file
11
src/app/dashboard/dashboard.component.html
Normal file
@ -0,0 +1,11 @@
|
||||
<h3>Top Heroes</h3>
|
||||
<div class="grid grid-pad">
|
||||
<a *ngFor="let hero of heroes" class="col-1-4"
|
||||
routerLink="/detail/{{ hero.id }}">
|
||||
<div class="module hero">
|
||||
<h4>{{hero.name}}</h4>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<app-hero-search></app-hero-search>
|
25
src/app/dashboard/dashboard.component.spec.ts
Normal file
25
src/app/dashboard/dashboard.component.spec.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { DashboardComponent } from './dashboard.component';
|
||||
|
||||
describe('DashboardComponent', () => {
|
||||
let component: DashboardComponent;
|
||||
let fixture: ComponentFixture<DashboardComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ DashboardComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(DashboardComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
24
src/app/dashboard/dashboard.component.ts
Normal file
24
src/app/dashboard/dashboard.component.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Hero } from '../hero';
|
||||
import { HeroService } from '../hero.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-dashboard',
|
||||
templateUrl: './dashboard.component.html',
|
||||
styleUrls: ['./dashboard.component.css']
|
||||
})
|
||||
export class DashboardComponent implements OnInit {
|
||||
heroes: Hero[] = [];
|
||||
|
||||
constructor(private heroService: HeroService) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.getHeroes();
|
||||
}
|
||||
|
||||
getHeroes(): void {
|
||||
this.heroService.getHeroes()
|
||||
.subscribe(heroes => this.heroes = heroes.slice(1, 5));
|
||||
}
|
||||
|
||||
}
|
0
src/app/hero-detail/hero-detail.component.css
Normal file
0
src/app/hero-detail/hero-detail.component.css
Normal file
12
src/app/hero-detail/hero-detail.component.html
Normal file
12
src/app/hero-detail/hero-detail.component.html
Normal file
@ -0,0 +1,12 @@
|
||||
<div *ngIf="hero">
|
||||
<h2> {{ hero.name | uppercase }} Details</h2>
|
||||
<div><span> id: {{ hero.id }} </span></div>
|
||||
<div>
|
||||
<label> name:
|
||||
<input [(ngModel)]="hero.name" placeholder="name">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button (click)="save()"> save </button>
|
||||
<button (click)="goBack()">go back</button>
|
25
src/app/hero-detail/hero-detail.component.spec.ts
Normal file
25
src/app/hero-detail/hero-detail.component.spec.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { HeroDetailComponent } from './hero-detail.component';
|
||||
|
||||
describe('HeroDetailComponent', () => {
|
||||
let component: HeroDetailComponent;
|
||||
let fixture: ComponentFixture<HeroDetailComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ HeroDetailComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(HeroDetailComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
41
src/app/hero-detail/hero-detail.component.ts
Normal file
41
src/app/hero-detail/hero-detail.component.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import { Component, OnInit, Input } from '@angular/core';
|
||||
import { Hero } from '../hero';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { Location } from '@angular/common';
|
||||
|
||||
import { HeroService } from '../hero.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-hero-detail',
|
||||
templateUrl: './hero-detail.component.html',
|
||||
styleUrls: ['./hero-detail.component.css']
|
||||
})
|
||||
export class HeroDetailComponent implements OnInit {
|
||||
|
||||
@Input() hero: Hero;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private heroService: HeroService,
|
||||
private location: Location
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.getHero();
|
||||
}
|
||||
|
||||
getHero(): void {
|
||||
const id = +this.route.snapshot.paramMap.get('id');
|
||||
this.heroService.getHero(id)
|
||||
.subscribe(h => this.hero = h);
|
||||
}
|
||||
|
||||
goBack(): void {
|
||||
this.location.back();
|
||||
}
|
||||
|
||||
save(): void {
|
||||
this.heroService.updateHero(this.hero)
|
||||
.subscribe(() => this.goBack());
|
||||
}
|
||||
}
|
39
src/app/hero-search/hero-search.component.css
Normal file
39
src/app/hero-search/hero-search.component.css
Normal file
@ -0,0 +1,39 @@
|
||||
/* HeroSearch private styles */
|
||||
.search-result li {
|
||||
border-bottom: 1px solid gray;
|
||||
border-left: 1px solid gray;
|
||||
border-right: 1px solid gray;
|
||||
width:195px;
|
||||
height: 16px;
|
||||
padding: 5px;
|
||||
background-color: white;
|
||||
cursor: pointer;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.search-result li:hover {
|
||||
background-color: #607D8B;
|
||||
}
|
||||
|
||||
.search-result li a {
|
||||
color: #888;
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.search-result li a:hover {
|
||||
color: white;
|
||||
}
|
||||
.search-result li a:active {
|
||||
color: white;
|
||||
}
|
||||
#search-box {
|
||||
width: 200px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
|
||||
ul.search-result {
|
||||
margin-top: 0;
|
||||
padding-left: 0;
|
||||
}
|
13
src/app/hero-search/hero-search.component.html
Normal file
13
src/app/hero-search/hero-search.component.html
Normal file
@ -0,0 +1,13 @@
|
||||
<div id="search-component">
|
||||
<h4>Hero Search</h4>
|
||||
|
||||
<input #searchBox id="search-box" (keyup)="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>
|
25
src/app/hero-search/hero-search.component.spec.ts
Normal file
25
src/app/hero-search/hero-search.component.spec.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { HeroSearchComponent } from './hero-search.component';
|
||||
|
||||
describe('HeroSearchComponent', () => {
|
||||
let component: HeroSearchComponent;
|
||||
let fixture: ComponentFixture<HeroSearchComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ HeroSearchComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(HeroSearchComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
43
src/app/hero-search/hero-search.component.ts
Normal file
43
src/app/hero-search/hero-search.component.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { Subject } from 'rxjs/Subject';
|
||||
import { of } from 'rxjs/observable/of';
|
||||
|
||||
import {
|
||||
debounceTime, distinctUntilChanged, switchMap
|
||||
} from 'rxjs/operators';
|
||||
|
||||
import { Hero } from '../hero';
|
||||
import { HeroService } from '../hero.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-hero-search',
|
||||
templateUrl: './hero-search.component.html',
|
||||
styleUrls: [ './hero-search.component.css' ]
|
||||
})
|
||||
|
||||
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)),
|
||||
);
|
||||
}
|
||||
}
|
15
src/app/hero.service.spec.ts
Normal file
15
src/app/hero.service.spec.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { TestBed, inject } from '@angular/core/testing';
|
||||
|
||||
import { HeroService } from './hero.service';
|
||||
|
||||
describe('HeroService', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [HeroService]
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([HeroService], (service: HeroService) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
102
src/app/hero.service.ts
Normal file
102
src/app/hero.service.ts
Normal file
@ -0,0 +1,102 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Hero } from './hero';
|
||||
import { HEROES } from './mock-heroes';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { of } from 'rxjs/observable/of';
|
||||
import { MessageService } from './message.service';
|
||||
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||
import { catchError, map, tap } from 'rxjs/operators';
|
||||
|
||||
const httpOptions = {
|
||||
headers: new HttpHeaders({ 'Content-Type': 'application/json' })
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class HeroService {
|
||||
|
||||
private heroesUrl = 'api/heroes';
|
||||
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
private messageService: MessageService
|
||||
) { }
|
||||
|
||||
getHeroes(): Observable<Hero[]> {
|
||||
return this.http.get<Hero[]>(this.heroesUrl)
|
||||
.pipe(
|
||||
tap(heroes => this.log('fetched heroes')),
|
||||
catchError(this.handleError('getHeroes', []))
|
||||
);
|
||||
}
|
||||
|
||||
getHero(id: number): Observable<Hero> {
|
||||
const heroUrl = `${this.heroesUrl}/${id}`;
|
||||
return this.http.get<Hero>(heroUrl)
|
||||
.pipe(
|
||||
tap(_ => this.log(`fetched hero id=${id}`)),
|
||||
catchError(this.handleError<Hero>(`getHero id=${id}`))
|
||||
);
|
||||
}
|
||||
|
||||
updateHero(hero: Hero): Observable<any> {
|
||||
return this.http.put(this.heroesUrl, hero, httpOptions)
|
||||
.pipe(
|
||||
tap(_ => this.log(`updated hero id=${hero.id}`)),
|
||||
catchError(this.handleError<any>('updateHero'))
|
||||
);
|
||||
}
|
||||
|
||||
addHero(hero: Hero): Observable<Hero> {
|
||||
return this.http.post(this.heroesUrl, hero, httpOptions)
|
||||
.pipe(
|
||||
tap(_ => this.log(`added hero id=${hero.id}`)),
|
||||
catchError(this.handleError<any>('addHero'))
|
||||
);
|
||||
}
|
||||
|
||||
/** DELETE: delete the hero from the server */
|
||||
deleteHero (hero: Hero | number): Observable<Hero> {
|
||||
const id = typeof hero === 'number' ? hero : hero.id;
|
||||
const url = `${this.heroesUrl}/${id}`;
|
||||
|
||||
return this.http.delete<Hero>(url, httpOptions).pipe(
|
||||
tap(_ => this.log(`deleted hero id=${id}`)),
|
||||
catchError(this.handleError<Hero>('deleteHero'))
|
||||
);
|
||||
}
|
||||
|
||||
searchHeroes(term: string): Observable<Hero[]> {
|
||||
term = term.trim();
|
||||
if (!term) { return of([]); }
|
||||
return this.http.get<Hero[]>(`api/heroes/?name=${term}`)
|
||||
.pipe(
|
||||
tap(_ => this.log(`found heroes matching "${term}"`)),
|
||||
catchError(this.handleError<Hero[]>('searchHeroes', []))
|
||||
);
|
||||
}
|
||||
|
||||
private log(message: string) {
|
||||
this.messageService.add('HeroService: ' + message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
};
|
||||
}
|
||||
|
||||
}
|
4
src/app/hero.ts
Normal file
4
src/app/hero.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export class Hero {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
73
src/app/heroes/heroes.component.css
Normal file
73
src/app/heroes/heroes.component.css
Normal file
@ -0,0 +1,73 @@
|
||||
/* HeroesComponent's private CSS styles */
|
||||
.heroes {
|
||||
margin: 0 0 2em 0;
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
width: 15em;
|
||||
}
|
||||
.heroes li {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
background-color: #EEE;
|
||||
margin: .5em;
|
||||
padding: .3em 0;
|
||||
height: 1.6em;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.heroes li:hover {
|
||||
color: #607D8B;
|
||||
background-color: #DDD;
|
||||
left: .1em;
|
||||
}
|
||||
|
||||
.heroes a {
|
||||
color: #888;
|
||||
text-decoration: none;
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
.heroes a:hover {
|
||||
color:#607D8B;
|
||||
}
|
||||
|
||||
.heroes .badge {
|
||||
display: inline-block;
|
||||
font-size: small;
|
||||
color: white;
|
||||
padding: 0.8em 0.7em 0 0.7em;
|
||||
background-color: #607D8B;
|
||||
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;
|
||||
}
|
||||
|
||||
.button {
|
||||
background-color: #eee;
|
||||
border: none;
|
||||
padding: 5px 10px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
cursor: hand;
|
||||
font-family: Arial;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #cfd8dc;
|
||||
}
|
||||
|
||||
button.delete {
|
||||
position: relative;
|
||||
left: 194px;
|
||||
top: -32px;
|
||||
background-color: gray !important;
|
||||
color: white;
|
||||
}
|
21
src/app/heroes/heroes.component.html
Normal file
21
src/app/heroes/heroes.component.html
Normal file
@ -0,0 +1,21 @@
|
||||
<h2> My Heroes </h2>
|
||||
|
||||
<div>
|
||||
<label>Hero name:
|
||||
<input #heroName />
|
||||
</label>
|
||||
<!-- (click) passes input value to add() and then clears the input -->
|
||||
<button (click)="add(heroName.value); heroName.value=''">
|
||||
add
|
||||
</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>
|
||||
<button class="delete" title="delete hero"
|
||||
(click)="delete(hero)">x</button>
|
||||
</li>
|
||||
</ul>
|
25
src/app/heroes/heroes.component.spec.ts
Normal file
25
src/app/heroes/heroes.component.spec.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { HeroesComponent } from './heroes.component';
|
||||
|
||||
describe('HeroesComponent', () => {
|
||||
let component: HeroesComponent;
|
||||
let fixture: ComponentFixture<HeroesComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ HeroesComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(HeroesComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
43
src/app/heroes/heroes.component.ts
Normal file
43
src/app/heroes/heroes.component.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Hero } from '../hero';
|
||||
import { HeroService } from '../hero.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-heroes',
|
||||
templateUrl: './heroes.component.html',
|
||||
styleUrls: ['./heroes.component.css']
|
||||
})
|
||||
export class HeroesComponent implements OnInit {
|
||||
|
||||
heroes: Hero[];
|
||||
|
||||
selectedHero: Hero;
|
||||
|
||||
constructor(private heroService: HeroService) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.getHeroes();
|
||||
}
|
||||
|
||||
onSelect(h: Hero): void {
|
||||
this.selectedHero = h;
|
||||
}
|
||||
|
||||
getHeroes():void {
|
||||
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 => this.heroes.push(hero));
|
||||
}
|
||||
|
||||
delete(hero: Hero): void {
|
||||
this.heroes = this.heroes.filter(h => h !== hero);
|
||||
this.heroService.deleteHero(hero).subscribe();
|
||||
}
|
||||
|
||||
}
|
19
src/app/in-memory-data.service.ts
Normal file
19
src/app/in-memory-data.service.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { InMemoryDbService } from 'angular-in-memory-web-api';
|
||||
|
||||
export class InMemoryDataService implements InMemoryDbService {
|
||||
createDb() {
|
||||
const heroes = [
|
||||
{ id: 11, name: 'Mr. 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};
|
||||
}
|
||||
}
|
15
src/app/message.service.spec.ts
Normal file
15
src/app/message.service.spec.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { TestBed, inject } from '@angular/core/testing';
|
||||
|
||||
import { MessageService } from './message.service';
|
||||
|
||||
describe('MessageService', () => {
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [MessageService]
|
||||
});
|
||||
});
|
||||
|
||||
it('should be created', inject([MessageService], (service: MessageService) => {
|
||||
expect(service).toBeTruthy();
|
||||
}));
|
||||
});
|
16
src/app/message.service.ts
Normal file
16
src/app/message.service.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
@Injectable()
|
||||
export class MessageService {
|
||||
|
||||
messages: string[] = [];
|
||||
|
||||
add(message: string) {
|
||||
this.messages.push(message);
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.messages = [];
|
||||
}
|
||||
|
||||
}
|
35
src/app/messages/messages.component.css
Normal file
35
src/app/messages/messages.component.css
Normal file
@ -0,0 +1,35 @@
|
||||
/* MessagesComponent's private CSS styles */
|
||||
h2 {
|
||||
color: red;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
font-weight: lighter;
|
||||
}
|
||||
body {
|
||||
margin: 2em;
|
||||
}
|
||||
body, input[text], button {
|
||||
color: crimson;
|
||||
font-family: Cambria, Georgia;
|
||||
}
|
||||
|
||||
button.clear {
|
||||
font-family: Arial;
|
||||
background-color: #eee;
|
||||
border: none;
|
||||
padding: 5px 10px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
cursor: hand;
|
||||
}
|
||||
button:hover {
|
||||
background-color: #cfd8dc;
|
||||
}
|
||||
button:disabled {
|
||||
background-color: #eee;
|
||||
color: #aaa;
|
||||
cursor: auto;
|
||||
}
|
||||
button.clear {
|
||||
color: #888;
|
||||
margin-bottom: 12px;
|
||||
}
|
8
src/app/messages/messages.component.html
Normal file
8
src/app/messages/messages.component.html
Normal file
@ -0,0 +1,8 @@
|
||||
<div *ngIf="messageService.messages.length">
|
||||
|
||||
<h2>Messages</h2>
|
||||
<button class="clear"
|
||||
(click)="messageService.clear()">clear</button>
|
||||
<div *ngFor='let message of messageService.messages'> {{message}} </div>
|
||||
|
||||
</div>
|
25
src/app/messages/messages.component.spec.ts
Normal file
25
src/app/messages/messages.component.spec.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { MessagesComponent } from './messages.component';
|
||||
|
||||
describe('MessagesComponent', () => {
|
||||
let component: MessagesComponent;
|
||||
let fixture: ComponentFixture<MessagesComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ MessagesComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(MessagesComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
16
src/app/messages/messages.component.ts
Normal file
16
src/app/messages/messages.component.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { MessageService } from '../message.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-messages',
|
||||
templateUrl: './messages.component.html',
|
||||
styleUrls: ['./messages.component.css']
|
||||
})
|
||||
export class MessagesComponent implements OnInit {
|
||||
|
||||
constructor(public messageService: MessageService) { }
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
}
|
14
src/app/mock-heroes.ts
Normal file
14
src/app/mock-heroes.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { Hero } from './hero';
|
||||
|
||||
export const HEROES: Hero[] = [
|
||||
{ id: 11, name: 'Mr. 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' }
|
||||
];
|
@ -1 +1,23 @@
|
||||
/* You can add global styles to this file, and also import other style files */
|
||||
/* Application-wide Styles */
|
||||
h1 {
|
||||
color: #369;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
font-size: 250%;
|
||||
}
|
||||
h2, h3 {
|
||||
color: #444;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
font-weight: lighter;
|
||||
}
|
||||
body {
|
||||
margin: 2em;
|
||||
}
|
||||
body, input[text], button {
|
||||
color: #888;
|
||||
font-family: Cambria, Georgia;
|
||||
}
|
||||
/* everywhere else */
|
||||
* {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user