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": {
|
"ansi-html": {
|
||||||
"version": "0.0.7",
|
"version": "0.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.7.tgz",
|
"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": "^5.2.0",
|
||||||
"@angular/platform-browser-dynamic": "^5.2.0",
|
"@angular/platform-browser-dynamic": "^5.2.0",
|
||||||
"@angular/router": "^5.2.0",
|
"@angular/router": "^5.2.0",
|
||||||
|
"angular-in-memory-web-api": "^0.5.3",
|
||||||
"core-js": "^2.4.1",
|
"core-js": "^2.4.1",
|
||||||
"rxjs": "^5.5.6",
|
"rxjs": "^5.5.6",
|
||||||
"zone.js": "^0.8.19"
|
"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.-->
|
<h1> {{ title }} </h1>
|
||||||
<div style="text-align:center">
|
<nav>
|
||||||
<h1>
|
<a routerLink="/dashboard">Dashboard</a>
|
||||||
Welcome to {{ title }}!
|
<a routerLink="/heroes">Heroes</a>
|
||||||
</h1>
|
</nav>
|
||||||
<img width="300" alt="Angular Logo" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTAgMjUwIj4KICAgIDxwYXRoIGZpbGw9IiNERDAwMzEiIGQ9Ik0xMjUgMzBMMzEuOSA2My4ybDE0LjIgMTIzLjFMMTI1IDIzMGw3OC45LTQzLjcgMTQuMi0xMjMuMXoiIC8+CiAgICA8cGF0aCBmaWxsPSIjQzMwMDJGIiBkPSJNMTI1IDMwdjIyLjItLjFWMjMwbDc4LjktNDMuNyAxNC4yLTEyMy4xTDEyNSAzMHoiIC8+CiAgICA8cGF0aCAgZmlsbD0iI0ZGRkZGRiIgZD0iTTEyNSA1Mi4xTDY2LjggMTgyLjZoMjEuN2wxMS43LTI5LjJoNDkuNGwxMS43IDI5LjJIMTgzTDEyNSA1Mi4xem0xNyA4My4zaC0zNGwxNy00MC45IDE3IDQwLjl6IiAvPgogIDwvc3ZnPg==">
|
<router-outlet></router-outlet>
|
||||||
</div>
|
<app-messages></app-messages>
|
||||||
<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>
|
|
||||||
|
|
||||||
|
@ -6,5 +6,5 @@ import { Component } from '@angular/core';
|
|||||||
styleUrls: ['./app.component.css']
|
styleUrls: ['./app.component.css']
|
||||||
})
|
})
|
||||||
export class AppComponent {
|
export class AppComponent {
|
||||||
title = 'app';
|
title = 'Tour of Heroes';
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,48 @@
|
|||||||
import { BrowserModule } from '@angular/platform-browser';
|
import { BrowserModule } from '@angular/platform-browser';
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { HttpClientModule } from '@angular/common/http';
|
||||||
|
|
||||||
import { AppComponent } from './app.component';
|
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({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
AppComponent
|
AppComponent,
|
||||||
|
HeroesComponent,
|
||||||
|
HeroDetailComponent,
|
||||||
|
MessagesComponent,
|
||||||
|
DashboardComponent,
|
||||||
|
HeroSearchComponent
|
||||||
],
|
],
|
||||||
imports: [
|
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]
|
bootstrap: [AppComponent]
|
||||||
})
|
})
|
||||||
export class AppModule { }
|
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 */
|
/* 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