- Open Style Guide
- If you are using VS Code then make sure the following settings are put in place
"editor.formatOnSave": true,
"editor.insertSpaces": true,
Optionally
"files.autoSave": "onFocusChange"
Also, if you have not, install the following extensions
-
Bootstrapping your app
- Angular CLI
-
Preferred editor
- Visual Studio Code
- Sublime Text
-
Finally,
- What do we have?
- what are we going to do?
- What is a component?
- How does Angular use components?
- Hierarchy of components
- Send data down, emit events up
- No scopes, no two-way binding, no more MVC
- Update
app.component.html
<div class="container">
<nav class="navbar navbar-expand-lg navbar-dark bg-dark navbar-toggleable-sm justify-content-center">
<button class="navbar-toggler navbar-toggler-right" type="button" data-toggle="collapse" data-target=".navbar-collapse">
<span class="navbar-toggler-icon"></span>
</button>
<a href="/" class="navbar-brand d-flex w-50 mr-auto">Friends HQ</a>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav ml-auto w-100 justify-content-end">
<li class="nav-item">
<a class="nav-link" href="#">Dashboard</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">People</a>
</li>
</ul>
</div>
</nav>
</div>
<div class="container">
<!-- Insert your code here -->
</div>
<footer class="footer text-center">
<nav class="navbar fixed-bottom navbar-light bg-faded">
<a class="navbar-brand" href="#">Created by Looselytyped</a>
</nav>
</footer>- See how
app.component.tsis being used inapp.module.ts - Introduce variables in
app.component.tsand use them in the template
- Component hierarchy
- Just as
index.htmlusesapp-rootwe can create another component, sayapp-child-componentand use it inapp.component.html - What does it take to create a new component?
- Create the necessary files in the right location (Refer to Style Guide)
- Create an
index.tsfile and export the component declarethe component inapp.module.ts- Use it somewhere in the DOM
- Just as
-
Create
PeopleComponent- The
peopledirectory should be undersrc/app/ - The
selectorneeds to beapp-people - The
templateUrlshould bepeople.component.html
- The
-
Update
app/people/people.component.html
<div>
<h1>People Component</h1>
</div>- Be sure to update
app.module.ts!! - Update
app.component.htmland use the selector where it says<!-- Insert your code here -->
- Now we need to display a "list" of people
- While it seems that
PeopleComponentis an "empty" component, it is usual for "feature root" components to be just like the "root" component - they act as the place where you hang all the features off of
-
Create a
PersonListComponentunderpeople/person-listdirectory- Use the snippets functionality in VS code if you have it installed
- The snippet is
Ctrl-spacefollowed bya-componentthen "tab" key - The
selectorshould beapp-person-list - Note that here we are using the
person-list.component.cssfile, so be sure to introduce astyleUrlsarray in your@Componentdescriptors
-
Update
person-list.component.html
<div class="person-list">
<div class="row">
<div class="col-xs-12 col-md-9">
<div class="list-group">
<a href="#" class="list-group-item list-group-item-action flex-column align-items-start">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">
<!--Display first friend here-->
</h5>
</div>
</a>
</div>
</div>
<div class="col-xs-12 col-md-3 sidebar">
<a href="#" class="btn btn-success sidebar-cta disabled">
Add someone
</a>
</div>
</div>
</div>- Update
person-list.component.css
.person-list .list-group, .sidebar {
margin-top: 20px;
}
.person-list .list-group {
border: 1px solid #eee;
border-radius: 3px;
}
.person-list .sidebar .btn {
padding: 15px;
width: 100%;
}- Be sure to update
app.module.ts! - Use the
PersonListComponentinpeople.component.html
- Models, and mapping to entities
- Using models in your components
- Define a interface called
Friendinshared/friend.model.tsdirectory to map entities inserver/api/db.json - Declare an array of friends on
PersonListComponentand initialize it to an array- Copy the array found in
server/api/db.jsontoperson-list.component.tsand assign it to a local variable if it makes it easier
- Copy the array found in
- Display the first and last name of the first friend in that array in
person-list.component.html
- Do we need another component?
- Whats the reuse potential?
- Is there enough state to manage?
- How do we loop over a list of items?
- If we are looping over a child component, how do we supply it with what it needs?
-
Create a
ShowPersonComponentunderpeople/show-personselectormust beapp-show-person- Make sure you have the
stylesUrlattribute set in@Component
-
Ensure it has an
@Input() friend: Friendattribute -
Update
show-person.component.html
<a href="#" class="list-group-item list-group-item-action flex-column align-items-start">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">
{{ friend.firstName }} {{ friend.lastName }}
</h5>
<small>
<div class="heart-rating">
<span class="fa fa-heart" data-rating="1"></span>
</div>
</small>
</div>
</a>- Update
show-person.component.css
.heart-rating {
line-height:32px;
}
.heart-rating .fa-heart, .heart-rating .fa-heart-o {
font-size: 2em;
}
.heart-rating .fa-heart {
color: red;
}- Use
*ngForinperson-list.component.htmlinvokingapp-show-personsetting thefriendattribute for each friend in thefriendsarray
- How do we attach events to components?
- We can use the browser events like
clickand invoke a method on the component
- We can use the browser events like
- Given that the state of the component changes how do we conditionally apply a
classto the component?- We can use the
[ngClass]directive - Notice that this is a "setter" just like
[friend]=friendis
- We can use the
- Modify
show-person.component.htmlto attach aclickhandler toheart-rating- Introduce a method called 'like' in
ShowPersonComponentthat that toggles thefavflag on thefriendattribute - Invoke it by attaching a
(click)handler in the template so that you invokelikeon a click - To avoid event propogation you can simply do something like
like(); false;in the template
- Introduce a method called 'like' in
- Once you do that, use
ngClassto flit theclassof thespanbetweenfa-heartandfa-heart-o- You can use tertiary statements in HTML like so
(friend.fav)?'fa-heart':'fa-heart-o'
- You can use tertiary statements in HTML like so
- How do we notify the parent of an event?
- We can use the
EventEmitterand wrap anything in the event that is to be propagated
- We can use the
- The parent can then listen for the name of that "event" and attach a callback in the template
-
Introduce an
Outputevent emitter inShowPersonComponentthat emits an event of typeFriend -
When a friend is "liked" go ahead and emit the event wrapping the friend in it
-
In
PersonListComponent's template "listen" for that event, and attach a handler to set an attribute nameddisplayBannerto true- NOTE that you HAVE to declare the attribute first (initialize to
false) and then set it to true on receiving an event - Make sure that you switch
displayBannerto false eventually (You can usesetTimeoutfor this
- NOTE that you HAVE to declare the attribute first (initialize to
-
Here is the HTML you will need to add to
person-list.component.html
<div class="col-xs-12 col-md-9">
<div *ngIf="displayBanner" class="alert alert-success box-msg" role="alert">
<strong>List Saved!</strong> Your changes has been saved.
</div>
</div>- Here is what
displayBannershould look like inPersonListComponent
showBanner(friend: Friend) {
this.displayBanner = true;
setTimeout(() => {
this.displayBanner = false;
}, 3000);
};-
How do services work in Angular?
- If they too have dependencies (for e.g. a service might need
HttpClient) then you need to mark the service as@Injectable - They need to be "provided"
- Once provided they can be injected into other components
- If they too have dependencies (for e.g. a service might need
-
AJAX Calls
- Will need
HttpClientinjected HttpClienthas methods (likegetandput) returnObservable-s
- Will need
- Open a new terminal and run
npm run server(So now you have two terminals, one runningng serveand now this)
-
Import
HttpClientModulefrom@angular/common/httpin our module -
Create a
FriendsServiceunderapp/shared(Use the command lineng g service shared/Friends --dry-run trueand see what it does- Make sure it is
@Injectable!!
- Make sure it is
-
Inject
HttpClientin it's constructor -
You will need a BUNCH of imports
import {
HttpClient,
HttpHeaders,
} from '@angular/common/http';
import {
Observable,
} from 'rxjs';- Implement a
getFriendsmethod thatgetshttp://localhost:3000/friendsand returnsObservable<Array<Friend>>
getFriends(): Observable<Friend[]> {
return this.http.get<Friend[]>('http://localhost:3000/friends');
}- Now that we have a service, how do we use it in our components?
- This is the same as how we used
HttpClientin ourFriendService!
- This is the same as how we used
- We also need to discuss the lifecycle of components, especially what
OnInitoffers us, and why it is useful
- Inject the
FriendServiceinPersonListComponent - Instead of hard-coding the array of friends, use
onInitto populate the friends array like so
this.friendService.getFriends()
.subscribe(friends => this.friends = friends);- Much like
HttpClient.getwe also haveHttpClient.putbut the signature is a tad more elaborate.- Specifically in our situation, our backend which is the
json-serverthat expects us to supply the rightHttpHeaderselse it won't do anything putalso requires a payload
- Specifically in our situation, our backend which is the
- Implement
saveFriendwhich takes aFriendas an argument, does aputon the backend with the suppliedFriendas a payload, and with the rightHttpHeaders. Here is what the headers look like
const headers: HttpHeaders = new HttpHeaders({
'Content-Type': 'application/json',
'Accept': 'application/json',
});- Note that the backend sends you back the updated friend record!
- Go ahead and use that method in
PersonListComponent'slikemethod to save when you like/unlike a friend- First, inject the service in the constructor
- Then update the
likemethod to usesaveFriend
- Smart vs. dumb components
- Smart components
- Usually leverage services
- Know how to get/load/update data
- Dumb components
- Fully defined via their API
- Everything is an
@Inputor an@Ouput
- Smart components
But! Where do we save our friend? - In the PersonListComponent of course!
- Instead, when the parent
PersonListComponentreceives thenotifyParent(which in turn invokesshowBanner) it will now need to "save" the fact that a friend was "liked". ModifyshowBannerto first save the friend, and thenshowBanner
- How does routing work in Angular?
- I like to use a separate file called
app.routes.ts - This declares a
Routesobject which is essentially an array ofRouteobjects - Each
Routeobject introduces apathand acomponentto use for that path - We need to then install the routes as part of our
importsinapp.module.ts - Finally we need to use
router-outletin the DOM to signify which portions of the DOM get managed by the router
- I like to use a separate file called
- First you need to
importRouterModuleinto yourAppModule - Create a new file called
app.routes.tsnext toapp.module.tsfile so that we- route
peopleto use thePeopleComponent - trap
**toredirectTopeople - Be sure to
importRoutescoz you will need it
- route
- Import that file into
app.module.tsand import the routes usingRouter.forRoot - Update
app.component.htmlto userouter-outlet
- Usage of
routerLinkandrouterLinkActivedirectives in the DOM
- Update the navbar links in
app.component.htmlto use these two directives.- Remember that I have already supplied an
activeclass to highlight which link is active
- Remember that I have already supplied an
- Use the
angular-clito generate aDashboardComponent- In your terminal you will need to do
ng generate component <component-name>
- In your terminal you will need to do
- Update
dashboard.component.htmlto look like this
<div class="container">
<div class="dashboard">
<div class="dashboard-box dashboard-stat">
<h2>Statistics about your account</h2>
<ul class="list-inline">
<li class="list-inline-item">
<span class="stat-number">1</span>
<span class="stat-description">Contacts</span>
</li>
<li class="list-inline-item">
<span class="stat-number">2</span>
<span class="stat-description">Kids</span>
</li>
</ul>
</div>
</div>
</div>- Update
dashboard.component.cssto look like this
.dashboard {
background-color: #fff;
padding-top: 50px;
}
.dashboard .dashboard-box.dashboard-stat {
margin-bottom: 40px;
text-align: center;
}
.dashboard .dashboard-box {
background-color: #fff;
border: 1px solid #dfdfdf;
border-radius: 3px;
padding: 15px;
}
.dashboard .dashboard-box h2 {
border-bottom: 1px solid #eee;
font-size: 14px;
font-weight: 600;
padding-bottom: 5px;
}
.dashboard .dashboard-box.dashboard-stat li:not(:last-child) {
margin-right: 25px;
}
.dashboard .dashboard-box.dashboard-stat li {
display: inline-block;
}
.dashboard .dashboard-box.dashboard-stat .stat-number {
display: block;
font-size: 25px;
}
.dashboard .dashboard-box.dashboard-stat .stat-description {
font-size: 13px;
}- Introduce a new route that uses the
DashboardComponentwhen the route is/dashboard - Update the
Dashboardlink inapp.component.htmlso that it links correctly and displays the right class (active) when clicked
- What are pipes?
- Think of Unix pipes
- They are only meant for view transformation!!!
- Use the angular cli to create a new pipe called
FullNameunderapp/people/shared - The first argument to the
transformmethod should be of typeFriend - The return value should be the
Friend-sfirstNameandlastNameconcatenated - Use template strings here - Use the pipe in
show-person.component.html - Can you think of how you could pass in a delimiter, like
,as an argument? How will the pipe use that?
- How does one add a
/people/addroute? - How does one define a nested route?
-
First, create a
PersonFormComponentunderapp/people/ -
Modify the
app.routes.tsto have two routes underpeople—''that usesPeopleComponent, andaddthat usesPersonFormComponent -
Add the necessary
router-outletinpeople.component.html -
Update
person-form.component.html
<div class="col-xs-12 col-md-12">
<h1>Add a new person</h1>
</div>- Update
person-form.component.css
.ng-valid[required], .ng-valid.required {
border-left: 5px solid #42A948; /* green */
}
.ng-invalid:not(form) {
border-left: 5px solid #a94442; /* red */
}- Familiar / Simple use cases / Harder to test
- For our project
- Is our model sufficient?
- Maybe we should make an enum for
Gender
-
Introduce a enum
Genderundershareddirectory using the angular cli- This should have three values, one for
Male, like so —Male = "male", and similarly forFemale, andUndisclosed - Use this enum in
Friend
- This should have three values, one for
-
Create a new pipe using the cli called
EnumToArrayPipeundershared
Update enum-to-array.pipe.ts to look like so
@Pipe({
name: 'enumToArray'
})
export class EnumToArrayPipe implements PipeTransform {
transform(data) {
const keys = [];
for (const enumMember in data) {
keys.push({
key: enumMember,
value: data[enumMember]
});
}
return keys;
}
}- Import
FormsModuleinapp.module.ts - Create a instance variable (the name of the varible should be
model) of typeFriendinPersonFormComponentwith appropriate values - Update
person-form.component.htmlto look like so
<form #addNewPersonForm="ngForm"
(ngSubmit)="onSubmit()">
<div class="form-group">
<label for="first-name">First name</label>
<input type="text"
class="form-control"
id="first-name"
[(ngModel)]="model.firstName"
name="firstName"
#firstName="ngModel"
required>
<div [hidden]="firstName.valid || firstName.pristine"
class="alert alert-danger">
First name is required
</div>
</div>
<button [disabled]="!addNewPersonForm.valid"
type="submit"
class="btn btn-success">Submit</button>
</form>
<p>Form value: {{ addNewPersonForm.value | json }}</p>
<p>Model value: {{ model | json }}</p>- What is
#addNewPersonForm="ngForm"? - What is
[(ngModel)]="model.firstName"? - What is
name="firstName"&#firstName="ngModel"?
- Fill in the form to accomodate for
lastName, andfav(Note — this is a checkbox input) - Use a
selectandoptionwith theenumToArraypipe to correctly display and update themodel.gender
- Leverages programmatic API / Easier to test / Simplifies templates
- Add
FormsModulewithReactiveFormsModuleinapp.module.ts - Modify
person-form.component.tsto have aaddNewPersonFormof typeFormGrouplike so
addNewPersonForm = new FormGroup({
firstName: new FormControl(this.model.firstName),
// ...
});- Add the fields for other attributes in your model
- Clean up the template to use the
FormGrouplike so
<form [formGroup]=addNewPersonForm>
<div class="form-group">
<label for="first-name">First name</label>
<input type="text"
formControlName="firstName">
</div>
<button type="submit"
class="btn btn-success">Submit</button>
</form>
<p>Form value: {{ addNewPersonForm.value | json }}</p>
<p>Model value: {{ model | json }}</p>- Make sure you add the other fields
- Why do we need the
FormBuilder?- Reduces verbosity
- Allows for symmetry between the domain and the form
- Particularly useful if you have a nested domain
- Inject
FormBuilderinto the constructor forPersonFormComponent - Use
FormBuilder.groupto create the form
- Add
addFriendmethod toFriendServicethat takes aFriendandPOST-s it- Remember, you need the
HttpHeaders here!!
- Remember, you need the
- Inject
FriendServiceinto thePersonFormComponentonSubmitinvoke theaddFriendmethod to save a friend to the backend
- What kinds of testing is available?
- Unit
- Integration
- e2e
- What is the setup in Angular for testing?
- Update
friends.service.spec.tsto look like
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { TestBed } from '@angular/core/testing';
import { Friend, Gender } from '.';
import { BASE_URL, FriendsService } from './friends.service';
fdescribe('FriendsService', () => {
let httpClient: HttpClient;
let httpTestingController: HttpTestingController;
let friendsService: FriendsService;
let friend;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
HttpClientTestingModule,
],
providers: [
],
});
httpClient = TestBed.get(HttpClient);
httpTestingController = TestBed.get(HttpTestingController);
friendsService = TestBed.get(FriendsService);
friend = {
'id': 1,
'firstName': 'Michelle',
'lastName': 'Mulroy',
'gender': Gender.Female,
'fav': true
};
});
afterEach(() => {
// After every test, assert that there are no more pending requests.
httpTestingController.verify();
});
describe('#construction', () => {
it('should be created', () => {
expect(friendsService).toBeDefined();
});
});
describe('#getFriends', () => {
it('should get all friends', () => {
const expectedFriends: Friend[] = [friend];
friendsService.getFriends()
.subscribe(data => {
expect(data).toEqual(expectedFriends);
});
const req = httpTestingController.expectOne(`${BASE_URL}/friends`);
expect(req.request.method).toEqual('GET');
req.flush(expectedFriends);
});
});
});
- Following this model add a unit test to test
saveFriendandaddFriend
- Update
show-person.component.spec.tsto be
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ShowPersonComponent } from './show-person.component';
import { FullNamePipe } from '..';
import { DebugElement } from '@angular/core';
import { By } from '@angular/platform-browser';
import { Friend, Gender } from '../../shared';
fdescribe('ShowPersonComponent', () => {
let component: ShowPersonComponent;
let fixture: ComponentFixture<ShowPersonComponent>;
let nameDisplayEl: DebugElement;
let favEl: DebugElement;
let friend: Friend;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [
ShowPersonComponent,
FullNamePipe,
]
});
friend = {
'id': 1,
'firstName': 'Michelle',
'lastName': 'Mulroy',
'gender': Gender.Female,
'fav': true
};
fixture = TestBed.createComponent(ShowPersonComponent);
component = fixture.componentInstance;
nameDisplayEl = fixture.debugElement.query(By.css("h5.mb-1"));
favEl = fixture.debugElement.query(By.css("span.fa"));
});
it('should be created', () => {
expect(component).toBeDefined();
});
});
- We will write the tests for this together
- Organizing
- Use
index.tsfiles
- Use
- Angular Forms/Router/DI
- Routing example
- Augury
- Peformance Tips
- Use pipes (with primitives) more than functions on components
- Think about Lazy loading