diff --git a/app/app-routing.module.ts b/app/app-routing.module.ts new file mode 100644 index 0000000..73f447f --- /dev/null +++ b/app/app-routing.module.ts @@ -0,0 +1,20 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +import { DashboardComponent } from './dashboard.component'; +import { HeroesComponent } from './heroes.component'; +import { HeroDetailComponent } from './hero-detail.component'; + +const routes: Routes = [ + { path: '', redirectTo: '/dashboard', pathMatch: 'full' }, + { path: 'dashboard', component: DashboardComponent }, + { path: 'detail/:id', component: HeroDetailComponent }, + { path: 'heroes', component: HeroesComponent } +]; + +@NgModule({ + imports: [ RouterModule.forRoot(routes) ], + exports: [ RouterModule ] +}) + +export class AppRoutingModule {} diff --git a/app/app.component.css b/app/app.component.css new file mode 100644 index 0000000..40e1aba --- /dev/null +++ b/app/app.component.css @@ -0,0 +1,28 @@ +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; +} diff --git a/app/app.component.ts b/app/app.component.ts index 4c06d2f..cb4c452 100644 --- a/app/app.component.ts +++ b/app/app.component.ts @@ -1,6 +1,19 @@ import { Component } from '@angular/core'; + @Component({ + moduleId: module.id, selector: 'my-app', - template: '

My First Angular App

' + template:` +

{{title}}

+ + + `, + styleUrls: [ 'app.component.css' ] }) -export class AppComponent { } + +export class AppComponent { + title = 'Tour of Heroes'; +} diff --git a/app/app.module.ts b/app/app.module.ts index 5117901..f9e9c8a 100644 --- a/app/app.module.ts +++ b/app/app.module.ts @@ -1,12 +1,41 @@ import { NgModule } from '@angular/core'; -// ブラウザで表示させるために必要 import { BrowserModule } from '@angular/platform-browser'; +import { FormsModule } from '@angular/forms'; +import { HttpModule } from '@angular/http'; + +import { AppComponent } from './app.component'; +import { DashboardComponent } from './dashboard.component'; +import { HeroesComponent } from './heroes.component'; +import { HeroDetailComponent } from './hero-detail.component'; +import { HeroSearchComponent } from './hero-search.component' + +import { HeroService } from './hero.service'; + +import { AppRoutingModule } from './app-routing.module'; + +import { InMemoryWebApiModule } from 'angular-in-memory-web-api'; +import { InMemoryDataService } from './in-memory-data.service'; + -// AppComponent を読みこんでる -import { AppComponent } from './app.component'; @NgModule({ - imports: [ BrowserModule ], - declarations: [ AppComponent ], - bootstrap: [ AppComponent ] + imports: [ + BrowserModule, + FormsModule, + HttpModule, + InMemoryWebApiModule.forRoot(InMemoryDataService), + AppRoutingModule + ], + declarations: [ + AppComponent, + DashboardComponent, + HeroesComponent, + HeroSearchComponent, + HeroDetailComponent + ], + providers: [ + HeroService + ], + bootstrap: [ AppComponent ] }) + export class AppModule { } diff --git a/app/dashboard.component.css b/app/dashboard.component.css new file mode 100644 index 0000000..096cec7 --- /dev/null +++ b/app/dashboard.component.css @@ -0,0 +1,61 @@ +[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; + } +} diff --git a/app/dashboard.component.html b/app/dashboard.component.html new file mode 100644 index 0000000..937931a --- /dev/null +++ b/app/dashboard.component.html @@ -0,0 +1,9 @@ +

Top Heroes

+
+ +
+

{{hero.name}}

+
+
+
+ diff --git a/app/dashboard.component.ts b/app/dashboard.component.ts new file mode 100644 index 0000000..cb45b10 --- /dev/null +++ b/app/dashboard.component.ts @@ -0,0 +1,22 @@ +import { Component, OnInit } from '@angular/core'; + +import { Hero } from './hero'; +import { HeroService } from './hero.service'; + +@Component({ + moduleId: module.id, + selector: 'my-dashboard', + templateUrl: 'dashboard.component.html', + styleUrls: [ 'dashboard.component.css' ] +}) + +export class DashboardComponent { + heroes: Hero[] = []; + + constructor(private heroService: HeroService) {} + + ngOnInit(): void { + this.heroService.getHeroes() + .then(heroes => this.heroes = heroes.slice(1, 5)); + } +} diff --git a/app/hero-detail.component.css b/app/hero-detail.component.css new file mode 100644 index 0000000..f6139ba --- /dev/null +++ b/app/hero-detail.component.css @@ -0,0 +1,29 @@ +label { + display: inline-block; + width: 3em; + margin: .5em 0; + color: #607D8B; + font-weight: bold; +} +input { + height: 2em; + font-size: 1em; + padding-left: .4em; +} +button { + margin-top: 20px; + 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: #ccc; + cursor: auto; +} diff --git a/app/hero-detail.component.html b/app/hero-detail.component.html new file mode 100644 index 0000000..9ef390c --- /dev/null +++ b/app/hero-detail.component.html @@ -0,0 +1,11 @@ +
+

{{hero.name}} details!

+
+ {{hero.id}}
+
+ + +
+ + +
diff --git a/app/hero-detail.component.ts b/app/hero-detail.component.ts new file mode 100644 index 0000000..a8ed5bb --- /dev/null +++ b/app/hero-detail.component.ts @@ -0,0 +1,40 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, Params } from '@angular/router'; +import { Location } from '@angular/common'; + +import 'rxjs/add/operator/switchMap'; + +import { Hero } from './hero'; +import { HeroService } from './hero.service'; + +@Component({ + moduleId: module.id, + selector: 'my-hero-detail', + templateUrl: 'hero-detail.component.html', + styleUrls: [ 'hero-detail.component.css' ] +}) + +export class HeroDetailComponent implements OnInit { + hero: Hero; + + ngOnInit(): void { + this.route.params + .switchMap((params: Params) => this.heroService.getHero(+params['id'])) + .subscribe(hero => this.hero = hero); + } + + constructor( + private heroService: HeroService, + private route: ActivatedRoute, + private location: Location + ) {} + + save(): void { + this.heroService.update(this.hero) + .then(() => this.goBack()); + } + + goBack(): void { + this.location.back(); + } +} diff --git a/app/hero-search.component.css b/app/hero-search.component.css new file mode 100644 index 0000000..f6b5c59 --- /dev/null +++ b/app/hero-search.component.css @@ -0,0 +1,14 @@ +.search-result{ + border-bottom: 1px solid gray; + border-left: 1px solid gray; + border-right: 1px solid gray; + width:195px; + height: 20px; + padding: 5px; + background-color: white; + cursor: pointer; +} +#search-box{ + width: 200px; + height: 20px; +} diff --git a/app/hero-search.component.html b/app/hero-search.component.html new file mode 100644 index 0000000..15d7c6c --- /dev/null +++ b/app/hero-search.component.html @@ -0,0 +1,8 @@ +
+

Hero Search

+ +
+
+ {{hero.name}} +
+
diff --git a/app/hero-search.component.ts b/app/hero-search.component.ts new file mode 100644 index 0000000..fc827be --- /dev/null +++ b/app/hero-search.component.ts @@ -0,0 +1,58 @@ +import { Component, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; + +import { Observable } from 'rxjs/Observable'; +import { Subject } from 'rxjs/Subject'; + +// Observable class extensions +import 'rxjs/add/observable/of'; + +// Observable operators +import 'rxjs/add/operator/catch'; +import 'rxjs/add/operator/debounceTime'; +import 'rxjs/add/operator/distinctUntilChanged'; + +import { HeroSearchService } from './hero-search.service'; +import { Hero } from './hero'; + +@Component({ + moduleId: module.id, + selector: 'hero-search', + templateUrl: './hero-search.component.html', + styleUrls: [ './hero-search.component.css' ], + providers: [HeroSearchService] +}) +export class HeroSearchComponent implements OnInit { + heroes: Observable; + private searchTerms = new Subject(); + + constructor( + private heroSearchService: HeroSearchService, + private router: Router) {} + + // Push a search term into the observable stream. + search(term: string): void { + this.searchTerms.next(term); + } + + ngOnInit(): void { + this.heroes = this.searchTerms + .debounceTime(300) // wait 300ms after each keystroke before considering the term + .distinctUntilChanged() // ignore if next search term is same as previous + .switchMap(term => term // switch to new observable each time the term changes + // return the http search observable + ? this.heroSearchService.search(term) + // or the observable of empty heroes if there was no search term + : Observable.of([])) + .catch(error => { + // TODO: add real error handling + console.log(error); + return Observable.of([]); + }); + } + + gotoDetail(hero: Hero): void { + let link = ['/detail', hero.id]; + this.router.navigate(link); + } +} diff --git a/app/hero-search.service.ts b/app/hero-search.service.ts new file mode 100644 index 0000000..23fbffa --- /dev/null +++ b/app/hero-search.service.ts @@ -0,0 +1,17 @@ +import { Injectable } from '@angular/core'; +import { Http } from '@angular/http'; + +import { Observable } from 'rxjs/Observable'; +import 'rxjs/add/operator/map'; + +import { Hero } from './hero'; + +@Injectable() +export class HeroSearchService { + constructor(private http: Http) {} + + search(term: string): Observable { + return this.http.get(`app/heroes/?name=${term}`) + .map(response => response.json().data as Hero[]); + } +} diff --git a/app/hero.service.ts b/app/hero.service.ts new file mode 100644 index 0000000..8de8c76 --- /dev/null +++ b/app/hero.service.ts @@ -0,0 +1,53 @@ +import { Injectable } from '@angular/core'; +import { Headers, Http } from '@angular/http'; + +import 'rxjs/add/operator/toPromise'; + +import { Hero } from './hero'; + +@Injectable() +export class HeroService { + private heroesUrl = 'api/heroes'; // URL to web api + private headers = new Headers({'Content-Type': 'application/json'}); + + constructor(private http: Http) { } + + getHeroes(): Promise { + return this.http.get(this.heroesUrl) + .toPromise() + .then(response => response.json().data as Hero[]) + .catch(this.handleError); + } + getHero(id: number): Promise { + const url = `${this.heroesUrl}/${id}`; + return this.http.get(url) + .toPromise() + .then(response => response.json().data as Hero) + .catch(this.handleError); + } + create(name: string): Promise { + return this.http.post(this.heroesUrl, JSON.stringify({name: name}), {headers: this.headers}) + .toPromise() + .then(res => res.json().data) + .catch(this.handleError); + } + update(hero: Hero): Promise { + const url = `${this.heroesUrl}/${hero.id}`; + return this.http.put(url, JSON.stringify(hero), {headers: this.headers}) + .toPromise() + .then(() => hero) + .catch(this.handleError); + } + delete(id: number): Promise { + const url = `${this.heroesUrl}/${id}`; + return this.http.delete(url, {headers: this.headers}) + .toPromise() + .then(() => null) + .catch(this.handleError); + } + + private handleError(error: any): Promise { + console.error('An error occurred', error); // for demo purposes only + return Promise.reject(error.message || error); + } +} diff --git a/app/hero.ts b/app/hero.ts new file mode 100644 index 0000000..e3eac51 --- /dev/null +++ b/app/hero.ts @@ -0,0 +1,4 @@ +export class Hero { + id: number; + name: string; +} diff --git a/app/heroes.component.css b/app/heroes.component.css new file mode 100644 index 0000000..cacc340 --- /dev/null +++ b/app/heroes.component.css @@ -0,0 +1,55 @@ +.selected { + background-color: #CFD8DC !important; + color: white; +} +.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.selected:hover { + background-color: #BBD8DC !important; + color: white; +} +.heroes li:hover { + color: #607D8B; + background-color: #DDD; + left: .1em; +} +.heroes .text { + position: relative; + top: -3px; +} +.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; + margin-right: .8em; + border-radius: 4px 0 0 4px; +} + +button.delete { + float:right; + margin-top: 2px; + margin-right: .8em; + background-color: gray !important; + color:white; +} diff --git a/app/heroes.component.html b/app/heroes.component.html new file mode 100644 index 0000000..cf0b311 --- /dev/null +++ b/app/heroes.component.html @@ -0,0 +1,20 @@ +

My Heroes

+
+ + +
+
    +
  • + {{hero.id}} + {{hero.name}} + +
  • +
+
+

+ {{selectedHero.name | uppercase}} is my hero +

+ +
diff --git a/app/heroes.component.ts b/app/heroes.component.ts new file mode 100644 index 0000000..d0b14a3 --- /dev/null +++ b/app/heroes.component.ts @@ -0,0 +1,56 @@ +import { Component, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; +import { Hero } from './hero'; +import { HeroService } from './hero.service'; + +@Component({ + moduleId: module.id, + selector: 'my-heroes', + templateUrl: 'heroes.component.html', + styleUrls: [ 'heroes.component.css' ] +}) + + +export class HeroesComponent implements OnInit { + heroes: Hero[]; + selectedHero: Hero; + + constructor( + private router: Router, + private heroService: HeroService + ) { } + + getHeroes(): void { + this.heroService.getHeroes().then(heroes => this.heroes = heroes); + } + + add(name: string): void { + name = name.trim(); + if(!name) { return; } + this.heroService.create(name) + .then(hero => { + this.heroes.push(hero); + this.selectedHero = null; + }); + } + + delete(hero: Hero): void { + this.heroService.delete(hero.id) + .then(() => { + this.heroes = this.heroes.filter(h => h !== hero); + if (this.selectedHero === hero) { this.selectedHero = null; } + }); + } + + ngOnInit(): void { + this.getHeroes(); + } + + onSelect(hero: Hero): void { + this.selectedHero = hero; + } + + gotoDetail(): void { + this.router.navigate(['/detail', this.selectedHero.id]); + } +} diff --git a/app/in-memory-data.service.ts b/app/in-memory-data.service.ts new file mode 100644 index 0000000..0f50f26 --- /dev/null +++ b/app/in-memory-data.service.ts @@ -0,0 +1,18 @@ +import { InMemoryDbService } from 'angular-in-memory-web-api'; +export class InMemoryDataService implements InMemoryDbService { + createDb() { + let 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}; + } +} diff --git a/index.html b/index.html index 4c01792..616804a 100644 --- a/index.html +++ b/index.html @@ -1,5 +1,6 @@ + Angular QuickStart