Intro to
Angular
We expect cooperation from all participants to help ensure a safe environment for everybody.
We treat everyone with respect, we refrain from using offensive language and imagery, and we encourage to report any derogatory or offensive behavior to a member of the JSLeague community.
We provide a fantastic environment for everyone to learn and share skills regardless of gender, gender identity and expression, age, sexual orientation, disability, physical appearance, body size, race, ethnicity, religion (or lack thereof), or technology choices.
We value your attendance and your participation in the JSLeague community and expect everyone to accord to the community Code of Conduct at all JSLeague workshops and other events.
Code of conduct
Whoami
Alexandru Albu
Trainer @JSLeague
frontend engineer @10yo
design: photoshop, illustrator
development: javascript. python, sql, mongo
devops: docker
and gaming and basketball
Overview
Intro to Angular
Intro to Angular
Agenda
Components & Modules
Services
Observables
Requests
Routing
Forms
Testing
Components &
Modules
Intro to Angular
Services
Intro to Angular
Observables
Intro to Angular
Intro to Angular
System complexity
Intro to Angular
Product
Vendor
Cart
Invoice
Sale
Coupon
User profile
Payment
Intro to Angular
Cart
Invoice
Add product...
...update the total
Intro to Angular
Cart
Invoice
Proactive
Passive
import { Invoice } from './invoice';
class Cart {
constructor(invoice: Invoice){ ... }
addProduct(product) {
...
this.invoice.update(product); // needs to be public
...
}
}
Intro to Angular
Cart
Invoice
Listenable
Reactive
import { Cart } from './cart';
class Invoice {
constructor(cart: Cart){ ... }
init() {
...
this.cart.onCartChange( callback )
}
}
Intro to Angular
Cart
Invoice
Sale
Coupon
Payment
Passive programming
- remote setters and updates
- What does it affect? - look inside
- How does it work? - find usage
Intro to Angular
Cart
Invoice
Sale
Coupon
Payment
Reactive programming
- events, observation and self updates
- What does it affect? - find usages of events
- How does it work? - look inside
Intro to Angular
Intro to Angular
Intro to Angular
Observables
- A stream of data ( 0 or more values of any type)
- Pushed over any amount of time (can end, but not necessarily)
- Cancelable ( can be stopped from emitting a value - unsubscribed)
- Lazy - won't emit values until we subscribe to them
Intro to Angular
Iterator pattern
Intro to Angular
Observer pattern
Intro to Angular
Async with callbacks?
getData(
function(successResult) {
// do something with the data
},
function(faliureError) {
// handle the error
},
);
Intro to Angular
Intro to Angular
Request with promise
let result = fetch('api/users.json');
// what we think it is
result
.then(success => {
// handle success
})
.catch(error => {
// handle error
})
// what it actually is
result
.then(...)
.then(...)
.then(...)
Intro to Angular
Async / await
function asyncTask(i) {
return new Promise(resolve => resolve(i + 1));
}
async function runAsyncTasks() {
const res1 = await asyncTask(0);
const res2 = await asyncTask(res1);
const res3 = await asyncTask(res2);
return "Everything done"
}
runAsyncTasks().then(result => console.log(result));
Intro to Angular
Observables
Unifying callbacks, promises and event handlers.
Shape of an observable:
- A function
- accepts an observer ( an object with `next`, `error` and `complete` methods on it)
- returns a cancellation function
Intro to Angular
Observable with rxJS
let observable$ = new Observable(() => {
})
Observable
- read-only - consumer
- plain function
- exposes 3 channels: next, error and complete
Intro to Angular
Observable with rxJS
let observable$ = new Observable(observer => {
observer.next(1);
observer.next(2);
})
Observer
- write-only - producer
- instance passed to observable
- provide next, error and complete methods
- just an interface
Intro to Angular
Observable with rxJS
let observable$ = new Observable(observer => {
observer.next(1);
observer.next(2);
})
observable$.subscribe(value => {
console.log(value)
}
Subscription
- triggers the observable execution
- returns an unsubscribe() method that stops the observable
Intro to Angular
let observable$ = new Observable(observer => {
observer.next(1);
observer.next(2);
return () => {
// cleanup resources when done
};
})
const subscription = observable$.subscribe(value => {
console.log(value)
})
subscription.unsubscribe(); // stop emitting values
Intro to Angular
Other ways to create observables
Creation functions
-
of (value1, value2, value3)
-
from(promise/itterable/observable)
-
fromEvent(target, eventName)
-
interval(time)
-
timer(time)
Intro to Angular
HOT vs COLD observables
COLD is when your observable creates the producer
- producer is created and activated during subscription
- unicast => everyone gets their own instance
- observables are "cold" by default
// COLD
var cold = new Observable((observer) => {
var producer = new Producer();
// have observer listen to producer here
});
Intro to Angular
HOT vs COLD observables
HOT is when your observable closes over the produce
- producer is created and activated outside and independent of subscription
- multicast => shared reference to the producer
// HOT
var producer = new Producer();
var cold = new Observable((observer) => {
// have observer listen to producer here
});
Intro to Angular
Subjects
Observables are unicast - each subscriber manages its own execution context
Subjects are multicast observables - values are multicasted to many Observers
Types of subjects
- BehaviorSubject - has the notion of "current value"
- ReplaySubject - can record part of it's execution
Intro to Angular
Intro to Angular
Operators
Observables are collections of pushed values or events that we can:
- query (filter)
- transform (map)
- accumulate (reduce)
- join
- flatten
- more...
Intro to Angular
let array = [1,2,3,4,5];
array
.map(v => v * v)
.filter(v => v > 5 && v < 20)
.reduce((acc, curr) => acc + curr, 0)
// 25
Array functions
Intro to Angular
let observable$ = from([1,2,3,4,5]);
observable$
.pipe(
map(v => v * v),
filter(v => v > 5 && v < 20),
reduce((acc, curr) => acc + curr, 0)
)
.subscribe(val => console.log(val))
// 25
Observable operators
Intro to Angular
const buttonObs$ = fromEvent(querySelector('button'), 'click');
// anti-pattern
buttonObs$.subscribe(() => {
http$.get('/api/users').subscribe( data => {
// handle loaded data
})
})
// better
buttonObs$.pipe(
concatMap(
event => http$.get('/api/users')
)
).subscribe(data => {
// handle loaded data
})
Higher-order observables
Higher-order operators:
- concatMap
- switchMap
- mergeMap
Intro to Angular
// when stream completes
const obs$ = new Observable(observer => {
observer.closed; // false
observer.next(1); // emit 1
observer.complete();
observer.closed; // true
observer.next(2); // won't emit
});
Error handling
Intro to Angular
// streams error only once
const obs$ = new Observable(observer => {
observer.closed; // false
observer.next(1); // emit 1
observer.error(new Error('Bad!'));
observer.closed; // true
observer.next(2); // won't emit
});
obs$
.subscribe({
next: v => console.log(v),
error: e => console.log(e.message)
});
Error handling
Intro to Angular
// intercepting errors
const obs$ = new Observable(observer => {
observer.next(1);
observer.error(new Error('BANG!'));
}).pipe(
catchError(err => {
console.log('Intercepted error:' + err);
return of('I got this');
})
)
obs$.subscribe(v => console.log(v));
// 1 'I got this'
Error handling
Intro to Angular
// recovering from an error with retry operator
const getData$ = http.get('/api/users')
.pipe(
retry(3),
catchError(() => of('Something went wrong');
)
getData$.subscribe(value => console.log(value));
Error handling
Requests
Intro to Angular
Routing
Intro to Angular
Forms
Intro to Angular
Testing
Intro to Angular
Intro to Angular
Automated testing
Intro to Angular
Why do we (automatically) test?
-
Documented Intentions
-
Improved Design
-
Fewer Bugs into Production
-
No Regressions
-
Safer Refactoring
Intro to Angular
Tools
-
Test frameworks - where you write your test
-
Jasmine, Mocha, Tape
-
-
Test environment - where your tests are executed
-
browsers - Chrome, Firefox, etc
-
headless browsers - JSDom, PhantomJS, Puppeteer
-
-
Test runners - where you run your tests
-
Karma, Jest
-
Intro to Angular
Jasmine
-
A behavior-driven development framework for testing JavaScript code.
-
Create hierarchical suites of test - describe(‘', function)
-
The tests are written as specifications - it('', function)
-
Expectations and Matchers (built-in and custom) - expect(x).toBe(expected)
-
Spies - a test double pattern
-
Asynchronous operations support
Intro to Angular
Jasmine - a basic example
describe("A resource",() => {
const resource;
// runs before each test - good for initializing data
beforeEach(() => {
resource = new Resource();
resource.allocateSpace();
});
// runs after each test - good for cleanup
afterEach(() => {
resource.free();
});
// test
it("should have allocated 100 units of space",() => {
expect(resource.space).toEqual(100);
});
});
Intro to Angular
Jasmine - Matching functions
- not
- toBe
- toEqual
- toMatch
- toBeDefined
- toBeUndefined
- toBeNull
- toBeTruthy
- toBeFalsy
- toContain
- toBeLessThan
- toBeGreaterThan
- toBeCloseTo
- toThrow
Intro to Angular
Jasmine - Spies
// example.ts
class Person {
helloSomeone(toGreet) {
return `${this.sayHello()} ${toGreet}`;
};
sayHello() {
return 'Hello';
};
}
// example.spec.ts
describe('A Person', () => {
let fakePerson;
beforeEach( () => {fakePerson = new Person();})
it('should call the sayHello() function', () => {
spyOn(fakePerson, 'sayHello');
fakePerson.helloSomeone('world');
expect(fakePerson.sayHello).toHaveBeenCalled();
});
it('should greet the world', () => {
spyOn(fakePerson, 'helloSomeone');
fakePerson.helloSomeone('world');
expect(fakePerson.helloSomeone).toHaveBeenCalledWith('world')
});
});
Intro to Angular
Jasmine - Spies
// Actually calling the method
spyOn(fakePerson, 'sayHello').and.callThrough();
// Set a return value for a call
spyOn(fakePerson, 'sayHello').and.returnValue('Hello World');
// Set a return value for a call
spyOn(fakePerson, 'sayHello').and.returnValue('Hello World');
// Call a different function
spyOn(fakePerson, 'sayHello').and.callFake(
(arguments, can, be, received) => ...);
// Get number of calls
spyOn(fakePerson, 'sayHello')
expect(fakePerson.sayHello.calls.count()).toBe(3)
fakePerson.sayHello.calls.reset() // reset the counts
// Create a "bare" spy
spy = jasmine.createSpy('whatAmI');
expect(spy).toHaveBeenCalled();
// Create a spy object
tape = jasmine.createSpyObj('tape', ['play', 'pause', 'stop', 'rewind']);
Intro to Angular
Jasmine - async tests
describe('Asynchronous specs', () => {
let value;
beforeEach(() => {
value = 0;
});
it('should support async execution', done => {
setTimeout(() => {
value++;
expect(value).toBeGreaterThan(0);
done();
}, 1000);
});
});
Intro to Angular
Karma
- JavaScript test runner that integrates with a browser environment
- Created by the AngularJS team
- Configuration file to set:
- browser launchers
- test framework
- reporters
- preprocessors
Intro to Angular
Writing (good) tests
Intro to Angular
-
Arrange all necessary preconditions and inputs.
-
Act on the object or method under test.
-
Assert that the expected results have occurred.
Structuring tests
Intro to Angular
DRY vs DAMP
RULES
- Repeat yourself if necessary to make it easier to read
- A test should be a complete story, all within the it()
- You shouldn’t need to look around much to understand the test
- Minimize logic out of tests (what will test the tests?)
TECHNIQUES
- Remove less interesting setup to beforeEach()
- Keep critical setup within the it()
- Include all of the "Act" and "Assert" test parts are in the it() clause
Intro to Angular
DRY Test
describe("Hero Detail Component", function() {
var heroDetCmp;
beforeEach(function() {
heroDetCmp = createComponent();
heroDetCmp.ngOnInit();
});
describe('ngOninit' function() {
it("should set the hero", function() {
expect(heroDetCmp.hero).toBeDefined()
});
it("should set the heroId", function() {
expect(heroDetCmp.heroId).toBe(3));
});
});
});
Intro to Angular
DAMP Test
describe("Hero Detail Component", function() {
var heroDetCmp;
beforeEach(function() {
heroDetCmp = createComponent();
});
describe('ngOninit' function() {
it("should set the hero", function() {
heroDetCmp.ngOnInit();
expect(heroDetCmp.hero).toBeDefined()
});
it("should set the heroId", function() {
heroDetCmp.ngOnInit();
expect(heroDetCmp.heroId).toBe(3));
});
});
});
Intro to Angular
DRY vs DAMP
Intro to Angular
-
Isolated tests: only the class, mocking everything
-
Integration tests: compiling components and using the injector
-
Shallow: mock out related components
-
Deep: include all components
-
How much testing?
Intro to Angular
Angular Testing
Intro to Angular
Utilities
-
TestBed - a harness for compiling components
-
inject() - provides access to injectables
-
waitForAsync() & fakeAsync() - async Zone control
Intro to Angular
TestBed
// component.spec
describe('Testing GreetComponent', () => {
let component: GreetComponent;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ HeroComponent ],
imports: [ ... ],
providers: [ ... ],
schemas: [ ... ]
});
});
});
TestBed configures a temporary NgModule for testing
Intro to Angular
TestBed
// component.spec
describe('Testing GreetComponent', () => {
let component: GreetComponent;
beforeEach(() => {
TestBed.configureTestingModule({...});
TestBed.overrideComponent(GreetComponent, {
set: {
template: '<div>Overridden template here</div>'
// ...
}
});
});
});
TestBed configurations can be overriden
Intro to Angular
Component fixture
// component.spec
describe('Testing GreetComponent', () => {
let component: GreetComponent;
let fixture: ComponentFixture<GreetComponent>;
beforeEach(() => {
TestBed.configureTestingModule({...});
fixture = TestBed.createComponent(GreetComponent);
});
});
- creates an instance of the component to test
-
returns a component fixture
- access to the component instance
- access to Native DOM Element
- control Change Detection
- closes current TestBed configurations
Intro to Angular
Component fixture methods
Access to the component, its DOM and change detection
-
componentInstance - the instance of the component created by TestBed
-
debugElement - provides insight into the component and its DOM element
-
nativeElement - the native DOM element at the root of the component
-
detectChanges() - trigger a change detection cycle for the component
-
whenStable() - returns a promise that resolves when the fixture is stable
Intro to Angular
Change detection
describe('Testing message state in greet.component', () => {
beforeEach(...)
it('should display original greet', () => {
fixture.detectChanges();
expect(element.textContent).toContain(component.message);
});
})
- tells Angular to perform change detection
- TestBed.createComponent() does not trigger change detection
Intro to Angular
Change detection - automatically
import { ComponentFixtureAutoDetect } from '@angular/core/testing';
TestBed.configureTestingModule({
providers: [
{ provide: ComponentFixtureAutoDetect, useValue: true }
]
})
Configure automatic change detection
Intro to Angular
Debug Element
Insights into the component's DOM representation
-
parent / children - the immediate parent or children of this DebugElement
-
query(predicate) - search for one descendant that matches
-
queryAll(predicate) - search for many descendants that match
-
injector - this component's injector
-
listeners - this callback handlers for this component's events and @Outputs
-
triggerEventHandler(listener) - trigger an event or @Output
Intro to Angular
Query the DOM
-
nativeElement provides:
-
querySelector(cssSelector)
-
-
debugElement provides:
-
query(predicate)
-
queryAll(predicate)
-
-
predicates can be created by helpers:
-
By.css(selector)
-
By.directive(DirectiveType)
-
Intro to Angular
Interacting with the DOM
-
nativeElement - can't use outside the browser
- dispatchEvent
- textContent
-
debugElement - doesn't have access to textContent
- triggerEventHandler
- properties
- attributes
- classes
- styles
Intro to Angular
Dependency Injection
let heroService;
beforeEach(() => {
heroService = TestBed.inject(HeroService);
}));
- Gets services from the root injector
- Can be placed in beforeEach or it blocks:
Intro to Angular
Isolated Tests
Intro to Angular
// example.component
@Component({
template: `
<h1>{{message}}</h1>
<button (click)="clearMessage">Clear</button>
`
})
export class GreetComponent {
public message = '';
constructor() {}
setMessage(newMessage: string) {
this.message = newMessage;
}
clearMessage() {
this.message = '';
}
}
// Component
Intro to Angular
// example.sec
import {GreetComponent} from './greet.component';
describe('Testing message state in greet.component', () => {
let greetComponent: GreetComponent;
beforeEach(() => {
greetComponent = new GreetComponent();
});
it('should set new message', () => {
greetComponent.setMessage('Testing');
expect(greetComponent.message).toBe('Testing');
});
it('should clear message', () => {
greetComponent.clearMessage();
expect(greetComponent.message).toBe('');
});
});
// Test
Intro to Angular
Deep Integration Tests
Intro to Angular
Deep component testing
- Nested Components need to be tested too
- Shallow testing (mocking all children) is not enough
- Deep tests check that
- the parent is rendering the children correctly
- the child is receiving the correct values in its inputs
- the parent handles output events correctly
Intro to Angular
Accessing child components
// Search for instances of the child component
movieElements = fixture.debugElement.queryAll(By.directive(MovieItemComponent));
// Check the value of @Input properties on the child component
expect(movieElements[0].componentInstance.movie).toBe(MOVIES[0]);
// Trigger @Output bindings
movieElements[0].triggerEventHandler('delete', null);
Intro to Angular
Testing @Input and @Output
@Component({
selector: 'greet-message',
template: `<div class="greet">
{{message}}
<button (click)="handleClick()">LIKE</button>
</div>`
})
export class GreetComponent {
@Input() message: string;
@Output() onLiked = new EventEmitter<string>();
handleClick() {
this.onLiked.emit(this.message);
}
}
-
Goal - Test if inputs and outputs work correctly
-
Approaches
-
test as a standalone component
-
test inside a container component
-
Intro to Angular
Testing @Input
it('should display greeting', () => {
expect(greetElementText.nativeElement.textContent).toBe(expectedMessage);
});
set a value to the input property on the component object
Intro to Angular
Testing @Output
it('should raise selected event when clicked', () => {
let likedMessage: string;
component.onLiked.subscribe((message: string) => {
likedMessage = message;
});
greetElementButton.triggerEventHandler('click', null);
expect(likedMessage).toBe(expectedMessage);
});
subscribe to EventEmitter, trigger click event
Intro to Angular
Testing in Host Component
@Component({
template: `
<greet-message [message]="greet" (onLiked)="handleLike($event)">
</greet-message>
`,
})
class TestHostComponent {
greet = 'Wassuuuup?!?';
handleLike(message: string) {
this.greet = 'New greet';
}
}
create an on the fly component to test the target component
Intro to Angular
Testing in Host Component
describe('Test input/output for components', () => {
let fixture: ComponentFixture<TestHostComponent>;
let testHost: TestHostComponent;
let greetElementText: DebugElement;
let greetElementButton: DebugElement;
beforeEach(
waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [GreetComponent, TestHostComponent],
}).compileComponents();
}),
);
beforeEach(() => {
fixture = TestBed.createComponent(TestHostComponent);
testHost = fixture.componentInstance;
greetElementText = fixture.debugElement.query(By.css('.greet span'));
greetElementButton = fixture.debugElement.query(By.css('.greet button'));
fixture.detectChanges();
});
...
});
Intro to Angular
Testing with Dependencies
Intro to Angular
Testing a service
import {Component} from '@angular/core';
import {UserService} from './user.service';
@Component({
selector: 'greet-message',
template: '<h1>{{message}}</h1>'
})
export class GreetComponent {
public message = 'Hello';
constructor(private userService: UserService) { }
ngOnInit(): void {
this.message = this.userService.isLoggedIn ?
'Welcome, ' + this.userService.user.name :
'Please log in.';
}
}
Intro to Angular
Testing a service
// service stub
userServiceStub = {
isLoggedIn: true,
user: { name: 'Test User'}
};
// configure stub
TestBed.configureTestingModule({
declarations: [ WelcomeComponent ],
// providers: [ UserService ]
providers: [{
provide: UserService,
useValue: userServiceStub
}]
});
// use service from injector
userService = TestBed.inject(UserService);
Intro to Angular
Mocking HTTP
Intro to Angular
Mocking HTTP
describe('HttpClient testing', () => {
let httpTestingController: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ HttpClientTestingModule ]
});
httpTestingController = TestBed.inject(HttpTestingController);
});
});
-
HttpClientTestingModule - don't use the regular HttpClientModule
-
HttpTestingController - used to control the HTTP calls
Intro to Angular
Mocking HTTP
it('should get the proper todo\'s', () => {
const testData: Data = [{todo: 'Test Data'}];
// when call is made, observable emits
component.todos$.subscribe(data =>
expect(data).toEqual(testData)
);
// Match request URL's
const req = httpTestingController.expectOne('/data');
// Assert request method
expect(req.request.method).toEqual('GET');
// respond with mock data
req.flush(testData);
// make sure no outstanding calls
httpTestingController.verify();
});
Intro to Angular
Async Unit Tests
Intro to Angular
Zone.js
Intercepts and tracks asynchronous callbacks
- Intercept asynchronous task scheduling
- Wrap callbacks for error-handling and zone tracking across async operations.
- Provide a way to attach data to zones
- Provide a context specific last frame error handling
Configured by rules (or specs)
- AsyncTestZoneSpec - rules for async test zones
- FakeAsyncTestZoneSpec - rules for fake async test zones
Intro to Angular
Component with Async depedencies
@Component({
selector: 'greet-message',
template: '<h1>{{message}}</h1>'
})
export class GreetComponent {
public message = 'Hello';
constructor(private greetingsService: GreetingsService) { }
ngOnInit() {
this.greetingsService.getGreets()
.then(greets => this.message = greets[0]);
}
}
Intro to Angular
Component with Async dependencies
it('should show message (async)',
waitForAsync(() => {
fixture.detectChanges();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(element.textContent)
.toBe(testGreetings[0]);
});
}),
);
it('should show message (fakeAsync)',
fakeAsync(() => {
fixture.detectChanges();
tick();
fixture.detectChanges();
expect(element.textContent)
.toBe(testGreetings[0]);
}),
);
Q&A
Intro to Angular
Thank you!
Intro to Angular
By Alex Albu
Intro to Angular
- 477