List all our challenges for this Angular course
Source code at each step is available through branches ====> HERE
Not really a challenge but it's an 'How To' bootstrap the app.
Click here to expand steps
- Install NVM (https://github.com/creationix/nvm#install-script)
- Install a Node Version through NVM
nvm install 7
nvm alias default 7
$ node -v
v8.1.0
$ npm -v
5.0.3
Install @angular/cli globally
`npm install -g @angular/cli
Boostrap an angular cli application (updated guide on official website https://cli.angular.io)
ng new myProjectName
cd myProjectName
# Run the application through http://localhost:4200
ng serve
Proposed solution: step-00
Main idea: use ng generate and be familiar with basic component
Based on step-00
Proposed solution: step-01
Click here to expand steps
- Create a component called
header
$ ng generate component header
- Add the selector element
<app-header></<app-header>
into the main HTMLapp.component.html
- Play with template to see what's going on
- Add ngx-bootstrap or angular2-materialize
- Design a navbar header to display the name of app and links for future routes
- Add code between
<app-header>
and</<app-header>
- be genious :D
Create a product list view by using ngIf & ngFor directives
Based on step-01
Proposed solution: step-02
Click here to expand steps
- Create a component called
product-list
ng generate component product/product-list
- Inject the created component into the
app.component.html
<app-product-list></app-product-list>
- Now, add an default products list into our
ProductListComponent
class (find below a default products array)
products = [
{
"id": 1,
"productName": "Leaf Rake",
"productCode": "GDN-0011",
"releaseDate": "March 19, 2016",
"description": "Leaf rake with 48-inch wooden handle.",
"price": 19.95,
"starRating": 3.2,
"imageUrl": "http://openclipart.org/image/300px/svg_to_png/26215/Anonymous_Leaf_Rake.png"
},
{
"id": 2,
"productName": "Garden Cart",
"productCode": "GDN-0023",
"releaseDate": "March 18, 2016",
"description": "15 gallon capacity rolling garden cart",
"price": 32.99,
"starRating": 4.2,
"imageUrl": "http://openclipart.org/image/300px/svg_to_png/58471/garden_cart.png"
},
{
"id": 5,
"productName": "Hammer",
"productCode": "TBX-0048",
"releaseDate": "May 21, 2016",
"description": "Curved claw steel hammer",
"price": 8.9,
"starRating": 4.8,
"imageUrl": "http://openclipart.org/image/300px/svg_to_png/73/rejon_Hammer.png"
},
{
"id": 8,
"productName": "Saw",
"productCode": "TBX-0022",
"releaseDate": "May 15, 2016",
"description": "15-inch steel blade hand saw",
"price": 11.55,
"starRating": 3.7,
"imageUrl": "http://openclipart.org/image/300px/svg_to_png/27070/egore911_saw.png"
},
{
"id": 10,
"productName": "Video Game Controller",
"productCode": "GMG-0042",
"releaseDate": "October 15, 2015",
"description": "Standard two-button video game controller",
"price": 35.95,
"starRating": 4.6,
"imageUrl": "http://openclipart.org/image/300px/svg_to_png/120337/xbox-controller_01.png"
}
]
-
Work on the product-list component template
-
Add a table to display product (display image url as text)
-
Use
*ngIf
directive to show the table if there is no product in the array -
Use
*ngFor
directive on<tr>
element to repeat this element as many times as products in the array
-
-
Bonus: Create a ProductListDetail component to replace HTML code of
*ngFor
Use property binding, event binding and two-way binding by using [attr]
, (event)
and [(ngModel)]
Based on step-02
Proposed solution: step-03
Click here to expand steps
- Display image as
<img src...
into the table with a property binding toproduct.imageUrl
- Add a button to show/hide all images on the page (you can handle click by using
<button (click)="myPublicMethod()"></button>
) The text should be adapted to the current stage:Show the images
orHide the images
- Set-up using banana in the box
[()]
thengModel
on the filter input text (two-way binding) - Create a custom Pipe to reverse a word & use it to display the filter text value.
Work around component lifecycle
Based on step-03
Proposed solution: step-04
Click here to expand steps
- Develop the
product
pipe and use it into the product-list view (to filter products array). - Use Component lifecycle to
console.log
a message into theonInit
event - Add specific style for the product-list component
- Add pipe to products
*ngFor
(eg: currency, uppercase, etc.)
Proposed solution: step-04-bonus
Click here to expand bonus steps
Filter products without pipe. And add rating sort and so on.
Create a star component
Based on step-04
Proposed solution: step-05
Click here to expand steps
- Create a
starComponent
which display the rating with stars - Use this component into our
productListComponent
and place it next to existingproduct.starRating
- Set-up
rating
input intostarComponent
- Set-up
ratingClicked
output intostarComponent
- Listen
ratingClicked
event fromProductListComponent
Based on step-05
Proposed solution: step-06
Click here to expand steps
- Create a new angular service called
ProductService
$ ng generate service shared/models/product
- Ensure that it will be declared at our appModule level
- Move the IProduct interface and the products array from our
productListComponent
to this new service - Write a public
getProducts
method to access to this products array
BONUS: Start to work with Observable
Move products into a dedicated service
Based on step-06
Proposed solution: step-07
Click here to expand steps
- Instal
json-server
package
npm install --server json-server
- Create a folder
server
- Create a file into created folder called
db.json
with following content
{
"products": [
{
"id": 1,
"productName": "Leaf Rake",
"productCode": "GDN-0011",
"releaseDate": "March 19, 2016",
"description": "Leaf rake with 48-inch wooden handle.",
"price": 19.95,
"starRating": 3.2,
"imageUrl": "http://openclipart.org/image/300px/svg_to_png/26215/Anonymous_Leaf_Rake.png"
},
{
"id": 2,
"productName": "Garden Cart",
"productCode": "GDN-0023",
"releaseDate": "March 18, 2016",
"description": "15 gallon capacity rolling garden cart",
"price": 32.99,
"starRating": 4.2,
"imageUrl": "http://openclipart.org/image/300px/svg_to_png/58471/garden_cart.png"
},
{
"id": 3,
"productName": "Hammer",
"productCode": "TBX-0048",
"releaseDate": "May 21, 2016",
"description": "Curved claw steel hammer",
"price": 8.9,
"starRating": 4.8,
"imageUrl": "http://openclipart.org/image/300px/svg_to_png/73/rejon_Hammer.png"
},
{
"id": 4,
"productName": "Saw",
"productCode": "TBX-0022",
"releaseDate": "May 15, 2016",
"description": "15-inch steel blade hand saw",
"price": 11.55,
"starRating": 3.7,
"imageUrl": "http://openclipart.org/image/300px/svg_to_png/27070/egore911_saw.png"
},
{
"id": 5,
"productName": "Video Game Controller",
"productCode": "GMG-0042",
"releaseDate": "October 15, 2015",
"description": "Standard two-button video game controller",
"price": 35.95,
"starRating": 4.6,
"imageUrl": "http://openclipart.org/image/300px/svg_to_png/120337/xbox-controller_01.png"
}
]
}
- Edit the
package.json
file and add intoscripts
section the following line
"api": "json-server --watch ./server/db.json"
- Then you can run your backend server by type the following command and enjoy http://localhost:3000
npm run api
-
Import the
HttpModule
into theAppModule
(if not already done)- Install the
@angular/http
module - Import the
HttpModule
into ourAppModule
- Install the
-
Inject
Http
into ourProductService
-
Update the
getProducts()
method to make aget
call to our API Servicehttp://localhost:3000/products
-
Use
RxJS
methods:map
to convert the string result into a JSON Objectdo
toconsole.log
the JSON Objectcatch
to attach a method to handle errors- Imports
import { Observable } from 'rxjs/Observable'
import 'rxjs/add/operator/mergeMap';
- Change into
ProductListComponent
the way we retrieve the data from ourProductService
Proposed solution: step-07-bonus
Set-up basic routes to navigate across the application
Based on step-07
Proposed solution: step-08
Click here to expand steps
We'll create 3 main routes: /welcome
, /products
and /products/:id
.
-
Import the
RouterModule
into theAppModule
(if not already there)- Install the
@angular/router
module
$ npm install --save @angular/router
- Import the
RouterModule
into ourAppModule
from installed package - Use the
RouterModule.forRoot([])
syntax to describe the application's routes (Note thatRouterModule.forChild([])
is used in angular sub-module of our application to avoid colision)
- Install the
-
Create a basic
ProductDetailComponent
and aWelcomeComponent
with angular cli
$ ng generate component modules/welcome
$ ng generate component modules/product/product --flat=true
$ ng generate component modules/product/product-detail
- Create manually a ts file
./src/app/app.routes.ts
to centralize application routes and set-up our 3 routes:/welcome
,/products
and/products/:id
. In order to organize routes, split products routes into a separated file./.src/app/modules/product/product.routes.ts
with the same syntax.
import { Routes } from '@angular/router';
export const APP_ROUTES: Routes = [
// Routes
]
- Add the
<router-outlet></router-outlet>
directive into ourapp.component.html
andmodules/product/product.component.html
to place views - Replace
RouterModule.forRoot([])
intoapp.module.ts
to use the routes
import { APP_ROUTES } from './app.routes.ts`
// Some code ...
RouterModule.forRoot(APP_ROUTES)
- Replace links into the top bar by using the directive
routerLink
directive androuterLinkActive
to set style on current active link! - Replace links into our
ProductListComponent
in order to go to the detail page - Add a back button to the
ProductDetailComponent
.
<button routerLink="../">Back to products</button>
Enhance our routes to read parameters, set-up guard and resolve data
Based on step-08
Proposed solution: step-09
Click here to expand steps
- Write a
getProduct(id: number): Observable<IProduct>
method into ourProductService
- This method looks like the existing
getProducts(): Observable<Array<IProduct>>
method - Call the following URL instead:
http://localhost:3000/product/ID
whereID
is the requested product id
- Set-up a resolve called
product
in order to get the product object (IProduct
) into ourProductDetailComponent
- Create a new file injectable called
product.resolve.ts
$ ng generate service shared/resolves/product-resolve
- Refactor the class name from
ProductResolveService
toProductResolve
& the filename fromproduct-resolve.service.ts
intoproduct.resolve.ts
- Import the
ProductResolve
class into theapp.module.ts
file and add it into theproviders: []
array - The class must implement the interface
Resolve<T>
imported from the@angular/router
module - This interface force you to write a method
class ProductResolve implements Resolve<T> {
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<T> | Promise<T> | T {
// Your code can return an Observable, a Promise or T (T is a generic type, can be number, string, IProduct, etc.)
// Here T is IProduct
}
}
-
Now, we'll write the body of this function
- Retrieve id from URL params
- Return the
getProduct(id)
from ourProductService
to get ourObservable<IProduct>
- Add the resolve to the wanted route, here into the
product.routes.ts
{ path: ':id', component: ProductDetailComponent, resolve: {
product: ProductResolve
} }
Here you should see when navigating to this route, a request sent to your server!
- Retrieve the
Observable<IProduct>
into ourProductDetailComponent
- Import & Inject the
ActivatedRoute
from@angular/router
import { ActivatedRoute } from '@angular/router';
// …
class ProductDetailComponent implements OnInit {
constructor(private _route: ActivatedRoute) {}
}
- Retrieve the
data
observable fromActivatedRoute
and assign it to a class variable calledproduct$
this.product$ = this._route.data.map(data => data.product)
- Design the
ProductDetailComponent
HTML in order to display ourproduct$
information. Use the pipeasync
into the*ngIf
<div *ngIf="product$ | async; let product; else noProductTemplate">
ID: {{ product.id }}
</div>
<ng-template #noProductTemplate>
<h4>No product found!</h4>
</ng-template>
-
Set-up into the HTML a button to go to the next product id (
product.id++
) -
Set-up a guard to check if the given id is a number
- Generate a guard by using ng cli
$ ng generate guard --module app.module shared/guards/product-id
// --module will provide automatically the created file into our app.module.ts
- Update the code in order to check that the id param is well a number. Display a
console.error
else and redirect to the product list route
canActivate(
next: ActivatedRouteSnapshot,
state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
const id = Number(next.params.id)
if (isNaN(id) || id > 0) {
// console.error & redirect
}
return true
}
- Add the guard to the wanted route, here into the
product.routes.ts
{ path: ':id', component: ProductDetailComponent, resolve: {
product: ProductResolve
}, canActivate: [ProductIdGuard] }
Here we go! 👌
Resolve: It prepares our data for the targeted component
Guard: It active/deactive a route based on custom check (here that the id is a number)
Edit/Create a product by using forms
Based on step-09
Proposed solution: step-10
Click here to expand steps
- Import @angular modules
FormsModule
&ReactiveFormsModule
- Install npm package
@angular/forms
$ npm install --save @angular/forms
- Import
FormsModule
&ReactiveFormsModule
from@angular/forms
into the app.module file
import { Formsmodule, ReactiveFormsModule } from '@angular/forms';
// …
@ngModule({
//…
imports: […, FormsModule, ReactiveFormsModule]
})
-
Display a form to feed all fields about product in HTML template (in
ProductDetailComponent
)- You can use ngIf/else syntax
<div *ngIf="myTest; else toto">
// Displayed if myTest
</div>
<ng-template #toto>
// Displayed else
</ng-template>
- Create the forms in
model driven
way into the component
- Import useful artefact from
@angular/forms
:FormBuilder
,FormGroup
,Validators
- Inject the
FormBuilder
- Design the form into the constructor
this.myForm = this._formBuilder.group({
id: '',
productName: '',
... etc.
})
- Create and connect the HTML form to the JS FormGroup by using these directives:
FormGroup
,FormControlName
<form [formGroup]="myForm">
//…
<input type="text" formControlName="productName" />
//…
</form>
- Display into the HTML a submit button which is disabled when the form is
INVALID
<input type="submit" [disabled]="myForm.invalid" />
- Change the input style based on validation css rules
- Create a folder
./src/styles
- Create a file
./src/styles/form.css
.ng-valid[required], .ng-valid.required {
border-left: 5px solid #42A948; /* green */
}
.ng-invalid:not(form) {
border-left: 5px solid #a94442; /* red */
}
- Import this new css file into the
styles.css
@import url(styles/form.css);
- Write the submit method in order to save the product through a call to our server
- Create a method
saveProduct(product: IProduct): Observable<IProduct>
into ourProductService
.
return this._http.put(url, payload)
//…
- Create a method
onSubmit()
into ourProductDetailComponent
in order to manage the form submission.
submit() {
if (this.formProduct.valid) {
this._productService.saveProduct(this.formProduct.value)
.subscribe(newProduct => { // Here we subscribe to do the request
this.product$ = Observable.from([product]) // We create a new observable from the server response to update our product
this.toggleMode()
})
}
}
- Into the
ProductDetailTemplate
, write a submit button, disabled if the form is invalid
<button type="submit" [disabled]="productForm.invalid">Save</button>
Here, we update the product into our component. It works like a charm 'cause we don't display information of this product concurrently in an other part of the view.
If for example, we display current product name into the navbar, after a save, data will be different as we refresh only the ProductDetailComponent
product information.
In order to fix that, you cake a look to step-07-bonus which, instead of returns Observables from angular Http
service, will manage a collection in-memory and always returns this Observable, with or without filter.
It will allow all our component to be aware of any change on this collection!
We'll do this fix into 11 - Advanced forms
Based on step-10
Proposed solution: step-11
Click here to expand steps
- Update our
ProductService
to manage a collection in-memory
Use step-07-bonus
class ProductService {
private products$: BehaviorSubject(Array<IProduct>) = new BehaviorSubject<Array<IProduct>>([])
private dataStore = {
products: []
}
constructor(private _http: HttpService) {
this._next() // Emit the dataStore.products event
}
getAll() {
// 1. Do the request call by subscribing
this._http.get(`URL`).map(res => res.json())
.subscribe(products => {
// 2. Sync our dataStore
this.syncProducts(products)
})
// 3. Return the BehaviorSubject as Observable
return this.products$.asObservable()
}
get(id: number) {
// 1. Same as getAll for one product
// 2. call _syncProduct instead of _syncProducts
// 3. Return the BehaviorSubjected as Observable with filter to get the product
return this.products$.asObservable()
.flatMap(products => products) // Flatten the Array<IProduct>
.filter(product => product.id === id)
}
private _syncProducts(products: Array<IProduct>) {
this.dataStore.products = products
this._next()
}
private _syncProduct(product: IProduct) {
this.dataStore.products.map(storeProduct => {
return storeProduct.id === product.id ?
product : storeProduct
})
this._next()
}
private _next() {
this.products$.next(this.dataStore.products)
}
}
- Create a custom Validator
- Create an empty file into
./src/app/shared/validators/product.validators.ts
- Write validator functions into it
export function validProductCode(control:AbstractControl): {[key: string]: any} => {
const productRegex = /[A-Z]{3}-[0-9]{4}/
const productCode = control.value
return productRegex.test(value) ? {'validProductCode': {productCode}} : null;
}
// Function below coming from angular spec
export function forbiddenNameValidator(nameRe: RegExp): ValidatorFn {
return (control: AbstractControl): {[key: string]: any} => {
const name = control.value;
const no = nameRe.test(name);
return no ? {'forbiddenName': {name}} : null;
};
}
- Add
validProductCode
to theproductCode
validators Array into our form
import { validProductCode } from '../../shared/validators/product.validators.ts'
//…
productCode: ['', validProductCode]
- Add
forbiddenNameValidator
to theproductName
to block a regex (eg: blocked)
import { forbiddenNameValidator } from '../../shared/validators/product.validators.ts'
//…
productName: ['', [//…, forbiddenNameValidator(/blocked/)]]
- Display error messages
<div *ngIf="productForm.get('productCode').errors?.validProductCode" class="alert alert-danger">
Invalid product code.
</div>
No challenge here!
No challenge here!
No challenge here!
No challenge here!