- 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.ts
is being used inapp.module.ts
- Introduce variables in
app.component.ts
and use them in the template
- Component hierarchy
- Just as
index.html
usesapp-root
we can create another component, sayapp-child-component
and 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.ts
file and export the component declare
the component inapp.module.ts
- Use it somewhere in the DOM
- Just as
-
Create
PeopleComponent
- The
people
directory should be undersrc/app/
- The
selector
needs to beapp-people
- The
templateUrl
should 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.html
and use the selector where it says<!-- Insert your code here -->
- Now we need to display a "list" of people
- While it seems that
PeopleComponent
is 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
PersonListComponent
underpeople/person-list
directory- Use the snippets functionality in VS code if you have it installed
- The snippet is
Ctrl-space
followed bya-component
then "tab" key - The
selector
should beapp-person-list
- Note that here we are using the
person-list.component.css
file, so be sure to introduce astyleUrls
array in your@Component
descriptors
-
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
PersonListComponent
inpeople.component.html
- Models, and mapping to entities
- Using models in your components
- Define a interface called
Friend
inshared/friend.model.ts
directory to map entities inserver/api/db.json
- Declare an array of friends on
PersonListComponent
and initialize it to an array- Copy the array found in
server/api/db.json
toperson-list.component.ts
and 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
ShowPersonComponent
underpeople/show-person
selector
must beapp-show-person
- Make sure you have the
stylesUrl
attribute set in@Component
-
Ensure it has an
@Input() friend: Friend
attribute -
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
*ngFor
inperson-list.component.html
invokingapp-show-person
setting thefriend
attribute for each friend in thefriends
array
- How do we attach events to components?
- We can use the browser events like
click
and 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
class
to the component?- We can use the
[ngClass]
directive - Notice that this is a "setter" just like
[friend]=friend
is
- We can use the
- Modify
show-person.component.html
to attach aclick
handler toheart-rating
- Introduce a method called 'like' in
ShowPersonComponent
that that toggles thefav
flag on thefriend
attribute - Invoke it by attaching a
(click)
handler in the template so that you invokelike
on 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
ngClass
to flit theclass
of thespan
betweenfa-heart
andfa-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
EventEmitter
and 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
Output
event emitter inShowPersonComponent
that 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 nameddisplayBanner
to 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
displayBanner
to false eventually (You can usesetTimeout
for 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
displayBanner
should 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
HttpClient
injected HttpClient
has methods (likeget
andput
) returnObservable
-s
- Will need
- Open a new terminal and run
npm run server
(So now you have two terminals, one runningng serve
and now this)
-
Import
HttpClientModule
from@angular/common/http
in our module -
Create a
FriendsService
underapp/shared
(Use the command lineng g service shared/Friends --dry-run true
and see what it does- Make sure it is
@Injectable
!!
- Make sure it is
-
Inject
HttpClient
in it's constructor -
You will need a BUNCH of imports
import {
HttpClient,
HttpHeaders,
} from '@angular/common/http';
import {
Observable,
} from 'rxjs';
- Implement a
getFriends
method thatget
shttp://localhost:3000/friends
and 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
HttpClient
in ourFriendService
!
- This is the same as how we used
- We also need to discuss the lifecycle of components, especially what
OnInit
offers us, and why it is useful
- Inject the
FriendService
inPersonListComponent
- Instead of hard-coding the array of friends, use
onInit
to populate the friends array like so
this.friendService.getFriends()
.subscribe(friends => this.friends = friends);
- Much like
HttpClient.get
we also haveHttpClient.put
but the signature is a tad more elaborate.- Specifically in our situation, our backend which is the
json-server
that expects us to supply the rightHttpHeaders
else it won't do anything put
also requires a payload
- Specifically in our situation, our backend which is the
- Implement
saveFriend
which takes aFriend
as an argument, does aput
on the backend with the suppliedFriend
as 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
'slike
method to save when you like/unlike a friend- First, inject the service in the constructor
- Then update the
like
method 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
@Input
or an@Ouput
- Smart components
But! Where do we save our friend? - In the PersonListComponent
of course!
- Instead, when the parent
PersonListComponent
receives thenotifyParent
(which in turn invokesshowBanner
) it will now need to "save" the fact that a friend was "liked". ModifyshowBanner
to 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
Routes
object which is essentially an array ofRoute
objects - Each
Route
object introduces apath
and acomponent
to use for that path - We need to then install the routes as part of our
imports
inapp.module.ts
- Finally we need to use
router-outlet
in 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
import
RouterModule
into yourAppModule
- Create a new file called
app.routes.ts
next toapp.module.ts
file so that we- route
people
to use thePeopleComponent
- trap
**
toredirectTo
people
- Be sure to
import
Routes
coz you will need it
- route
- Import that file into
app.module.ts
and import the routes usingRouter.forRoot
- Update
app.component.html
to userouter-outlet
- Usage of
routerLink
androuterLinkActive
directives in the DOM
- Update the navbar links in
app.component.html
to use these two directives.- Remember that I have already supplied an
active
class to highlight which link is active
- Remember that I have already supplied an
- Use the
angular-cli
to 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.html
to 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.css
to 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
DashboardComponent
when the route is/dashboard
- Update the
Dashboard
link inapp.component.html
so 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
FullName
underapp/people/shared
- The first argument to the
transform
method should be of typeFriend
- The return value should be the
Friend
-sfirstName
andlastName
concatenated - 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/add
route? - How does one define a nested route?
-
First, create a
PersonFormComponent
underapp/people/
-
Modify the
app.routes.ts
to have two routes underpeople
—''
that usesPeopleComponent
, andadd
that usesPersonFormComponent
-
Add the necessary
router-outlet
inpeople.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
Gender
undershared
directory 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
EnumToArrayPipe
undershared
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
FormsModule
inapp.module.ts
- Create a instance variable (the name of the varible should be
model
) of typeFriend
inPersonFormComponent
with appropriate values - Update
person-form.component.html
to 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
select
andoption
with theenumToArray
pipe to correctly display and update themodel.gender
- Leverages programmatic API / Easier to test / Simplifies templates
- Add
FormsModule
withReactiveFormsModule
inapp.module.ts
- Modify
person-form.component.ts
to have aaddNewPersonForm
of typeFormGroup
like 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
FormGroup
like 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
FormBuilder
into the constructor forPersonFormComponent
- Use
FormBuilder.group
to create the form
- Add
addFriend
method toFriendService
that takes aFriend
andPOST
-s it- Remember, you need the
HttpHeader
s here!!
- Remember, you need the
- Inject
FriendService
into thePersonFormComponent
onSubmit
invoke theaddFriend
method 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.ts
to 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
saveFriend
andaddFriend
- Update
show-person.component.spec.ts
to 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.ts
files
- Use
- Angular Forms/Router/DI
- Routing example
- Augury
- Peformance Tips
- Use pipes (with primitives) more than functions on components
- Think about Lazy loading