Skip to content

Commit

Permalink
Implemented CRUD ops
Browse files Browse the repository at this point in the history
  • Loading branch information
nevskyy committed Jul 15, 2024
1 parent db79e5b commit 94f4b8f
Show file tree
Hide file tree
Showing 18 changed files with 387 additions and 68 deletions.
22 changes: 0 additions & 22 deletions src/app/app-routing/app-routing.module.ts

This file was deleted.

19 changes: 17 additions & 2 deletions src/app/app.config.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,23 @@
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import {
ApplicationConfig,
importProvidersFrom,
provideZoneChangeDetection,
} from '@angular/core';
import { provideRouter } from '@angular/router';

import { routes } from './app.routes';
import { provideHttpClient } from '@angular/common/http';
import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api';
import { InMemoryDataService } from './in-memory-data.service';

export const appConfig: ApplicationConfig = {
providers: [provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes)]
providers: [
provideHttpClient(),
provideRouter(routes),
importProvidersFrom([
HttpClientInMemoryWebApiModule.forRoot(InMemoryDataService, {
dataEncapsulation: false,
}),
]),
],
};
10 changes: 9 additions & 1 deletion src/app/app.routes.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
import { Routes } from '@angular/router';
import { HeroesComponent } from './heroes/heroes.component';
import { DashboardComponent } from './dashboard/dashboard.component';
import { HeroDetailComponent } from './hero-detail/hero-detail.component';

export const routes: Routes = [];
export const routes: Routes = [
{ path: '', redirectTo: '/dashboard', pathMatch: 'full' },
{ path: 'heroes', component: HeroesComponent },
{ path: 'dashboard', component: DashboardComponent },
{ path: 'detail/:id', component: HeroDetailComponent },
];
1 change: 1 addition & 0 deletions src/app/dashboard/dashboard.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ <h2>Top Heroes</h2>
{{hero.name}}
</a>
</div>
<app-hero-search></app-hero-search>
5 changes: 3 additions & 2 deletions src/app/dashboard/dashboard.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import { Hero } from '../hero';
import { HeroService } from '../hero.service';
import { RouterModule } from '@angular/router';
import { HttpClient } from '@angular/common/http';
import { HeroSearchComponent } from '../hero-search/hero-search.component';

@Component({
selector: 'app-dashboard',
standalone: true,
imports: [CommonModule, RouterModule],
imports: [CommonModule, RouterModule, HeroSearchComponent],
templateUrl: './dashboard.component.html',
styleUrl: './dashboard.component.css'
})
Expand All @@ -23,7 +24,7 @@ export class DashboardComponent implements OnInit{

getHeroes(): void {
this.heroService.getHeroes()
.then(heroes => this.heroes = heroes.slice(1, 5));
.subscribe(heroes => this.heroes = heroes.slice(1, 5));
}

}
1 change: 1 addition & 0 deletions src/app/hero-detail/hero-detail.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ <h2>{{hero.name | uppercase}} Details</h2>
</div>
</div>
<button type="button" (click)="goBack()">go back</button>
<button type="button" (click)="save()">save</button>
12 changes: 10 additions & 2 deletions src/app/hero-detail/hero-detail.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@ import { ActivatedRoute } from '@angular/router';
import { Location } from '@angular/common';

import { HeroService } from '../hero.service';
import { RouterModule } from '@angular/router';

// import { HeroesComponent } from '../heroes/heroes.component';

@Component({
selector: 'app-hero-detail',
standalone: true,
imports: [CommonModule, FormsModule],
imports: [CommonModule, FormsModule, RouterModule],
templateUrl: './hero-detail.component.html',
styleUrl: './hero-detail.component.css'
})
Expand All @@ -33,10 +34,17 @@ export class HeroDetailComponent {
getHero(): void {
const id = Number(this.route.snapshot.paramMap.get('id'));
this.heroService.getHero(id)
.then(hero => this.hero = hero);
.subscribe(hero => this.hero = hero);
}

goBack(): void {
this.location.back();
}

save(): void {
if (this.hero) {
this.heroService.updateHero(this.hero)
.subscribe(() => this.goBack());
}
}
}
47 changes: 47 additions & 0 deletions src/app/hero-search/hero-search.component.css
Original file line number Diff line number Diff line change
@@ -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;
}
12 changes: 12 additions & 0 deletions src/app/hero-search/hero-search.component.html
Original file line number Diff line number Diff line change
@@ -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>
23 changes: 23 additions & 0 deletions src/app/hero-search/hero-search.component.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';

import { HeroSearchComponent } from './hero-search.component';

describe('HeroSearchComponent', () => {
let component: HeroSearchComponent;
let fixture: ComponentFixture<HeroSearchComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [HeroSearchComponent]
})
.compileComponents();

fixture = TestBed.createComponent(HeroSearchComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
39 changes: 39 additions & 0 deletions src/app/hero-search/hero-search.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Component, OnInit } from '@angular/core';
import { Observable, Subject } from 'rxjs';
import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';
import { Hero } from '../hero';
import { HeroService } from '../hero.service';
import { CommonModule } from '@angular/common';
import { RouterLink } from '@angular/router';

@Component({
selector: 'app-hero-search',
standalone: true,
imports: [CommonModule, RouterLink],
templateUrl: './hero-search.component.html',
styleUrl: './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))
);
}
}
110 changes: 91 additions & 19 deletions src/app/hero.service.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,49 @@
import { Injectable } from '@angular/core';
import { Hero } from './hero';
import { HEROES } from './mock-heroes';
import { Observable, of } from 'rxjs';
import { Observable, of, catchError, tap } from 'rxjs';
import { MessageService } from './message.service';
import { HttpClient, HttpHeaders } from '@angular/common/http';

@Injectable({
providedIn: 'root',
})
export class HeroService {
constructor(private messageService: MessageService) {}
constructor(
private messageService: MessageService,
private http: HttpClient
) {}

url = 'http://localhost:3000/heroes'
private heroesUrl = 'api/heroes'; // URLL to web api

/** Log a HeroService message with the MessageService */
private log(message: string) {
this.messageService.add(`HeroService: ${message}`);
}

httpOptions = {
headers: new HttpHeaders({ 'Content-Type': 'application/json' }),
};

/**
* 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);
};
}

// getHeroes(): Observable<Hero[]> {
// const heroes = of(HEROES);
Expand All @@ -19,18 +52,14 @@ export class HeroService {
// return heroes;
// }

async getHeroes(): Promise<Hero[]> {
const data = await fetch(this.url);
this.messageService.add('HeroService: fetched heroes');

return await data.json() ?? [];
/** GET heroes from the server */
getHeroes(): Observable<Hero[]> {
return this.http.get<Hero[]>(this.heroesUrl).pipe(
tap((_) => this.log('fetched heroes')),
catchError(this.handleError<Hero[]>('getHeroes', []))
);
}

// /** GET heroes from the server */
// getHeroes(): Observable<Hero[]> {
// return this.http.get<Hero[]>(this.heroesUrl);
// }

// 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.
Expand All @@ -39,12 +68,55 @@ export class HeroService {

// return of(hero);
// }
async getHero(id: number): Promise<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 data = await fetch(`${this.url}/${id}`);
this.messageService.add(`HeroService: fetched hero id=${id}`);

return await data.json() ?? {};
/** GET hero by id. Will 404 if id not found */
getHero(id: number): Observable<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}`))
);
}

/** 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'))
);
}

/** 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'))
);
}

/* 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', []))
);
}
}
Loading

0 comments on commit 94f4b8f

Please sign in to comment.