SlideShare ist ein Scribd-Unternehmen logo
1 von 88
Lessons Learned
2 years of
Dirk Luijk
@dirkluijk
#frontend
#angular
#fullstack
About Bingel
● Full lesson support
● Instruction, exercises & testing
● Maths & language courses
● Adaptive learning
● Modern & cross-platform
Architecture
How it started…
ng new bingel-app$ ng new bingel-student-app
Architecture
student app
Architecture
student app
tasks module
lessons module
results module
Architecture
student app
tasks module
lessons module
results module
shared module
Architecture
student app
tasks module
lessons module
results module
shared module
teacher app
Architecture
student app
tasks module
lessons module
results module
shared module
teacher app
instruction module
analysis module
shared module
Architecture
student app
tasks module
lessons module
results module
shared module
teacher app
"shared code"
instruction module
analysis module
shared module
Architecture
student app
tasks module
lessons module
results module
shared module
teacher app
ui library
interactions
library
api library common library
instruction module
analysis module
shared module
Architecture
student app
tasks module
lessons module
results module
shared module
teacher app
@p1/ui @p1/interactions @p1/api @p1/common
preview app admin app
instruction module
analysis module
shared module
preview module licensing module
content graph module
@p1/features
Architecture
student app
tasks module
lessons module
results module
shared module
teacher app
@p1/ui @p1/interactions @p1/api @p1/common
preview app admin app
instruction module
analysis module
shared module
preview module licensing module
content graph module
@p1/features
single repository (monorepo)
How?
Nx workspaces
● Builts on top of Angular CLI
● Supports monorepo approach
● Provides "affected builds" option:
○ build only what has been changed
● Better separation of packages
Architecture: lessons learned
1. Consider a monorepo
○ simple versioning & release process
○ easier refactoring
○ well supported by Angular CLI + Nx Workspaces
2. Think in packages and their responsibilities
Routing
Routing
[
{
path: 'overview',
loadChildren: 'overview/overview.module#OverviewModule'
},
{
path: 'lessons',
loadChildren: 'lessons/lessons.module#LessonsModule'
},
{
path: 'tasks',
loadChildren: 'tasks/tasks.module#TasksModule'
}
];
Routing guards
{
path: 'tasks',
resolve: {
blocks: BlocksResolver
},
children: [
{
path: '',
canActivate: [TasksRedirectGuard]
},
{
path: 'blocks',
component: TasksBlocksComponent
},
{
path: 'blocks/:blockId/weeks/:weekId',
component: TasksSelectionComponent
}
]
}
Routing guards
{
path: 'tasks',
resolve: {
blocks: BlocksResolver
},
children: [
{
path: '',
canActivate: [TasksRedirectGuard]
},
{
path: 'blocks',
component: TasksBlocksComponent
},
{
path: 'blocks/:blockId/weeks/:weekId',
component: TasksSelectionComponent
}
]
}
Routing in Angular is reactive.
Routing in Angular is reactive.
But not necessarily.
ActivatedRoute
class WeekComponent implements OnInit {
week: Week;
constructor(private route: ActivatedRoute, private weekService: WeekService) {}
ngOnInit(): void {
this.route.paramMap
.pipe(
map(params => params.get('weekId')),
switchMap(weekId => this.weekService.getWeek(weekId))
)
.subscribe(week => {
this.week = week;
});
}
}
ActivatedRoute
class WeekComponent implements OnInit {
week: Week;
constructor(private route: ActivatedRoute, private weekService: WeekService) {}
ngOnInit(): void {
this.route.paramMap
.pipe(
map(params => params.get('weekId')),
switchMap(weekId => this.weekService.getWeek(weekId))
)
.subscribe(week => {
this.week = week;
});
}
}
reactive!
ActivatedRoute vs. ActivatedRouteSnapShot
class WeekComponent {
week: Week;
constructor(private route: ActivatedRoute, private weekService: WeekService) {}
ngOnInit(): void {
this.weekService
.getWeek(this.route.snapshot.paramMap.get('weekId'))
.subscribe(week => this.week = week);
}
}
ActivatedRoute vs. ActivatedRouteSnapShot
class WeekComponent {
week: Week;
constructor(private route: ActivatedRoute, private weekService: WeekService) {}
ngOnInit(): void {
this.weekService
.getWeek(this.route.snapshot.paramMap.get('weekId'))
.subscribe(week => this.week = week);
}
}
not reactive (static)!
Routing: lessons learned
1. Make use of lazy-loaded feature modules
○ Breaks down bundles into smaller chunks
2. Make smart use of guards!
3. Get used to the (reactive) API of Angular Router
Components
Different types of components
Page Components
Routed, fetches data, (almost) no presentation logic1
Feature Components
Specific presentation logic (bound to domain model)2
UI Components
Generic presentation logic (not bound to domain model)3
Different types of components
TasksModule
TaskSelectionComponent
/tasks/blocks/week/:id
TaskDetailsComponent
/tasks/task/:id
LessonsModule
LessonsComponent
/lessons
Different types of components
TasksModule
TaskSelectionComponent
/tasks/blocks/week/:id
TaskDetailsComponent
/tasks/task/:id
SidebarModule
SidebarComponent
AccordionModule CardModule
AccordionComponent CardComponent
LessonsModule
LessonsComponent
/lessons
Different types of components
TasksModule
TaskSelectionComponent
/tasks/blocks/week/:id
TaskDetailsComponent
/tasks/task/:id
SidebarModule
SidebarComponent
AccordionModule CardModule
AccordionComponent CardComponent
TaskPreviewComponent
TaskMenuComponent
LessonsModule
LessonsComponent
/lessons
Different types of components
TasksModule
TaskSelectionComponent
/tasks/blocks/week/:id
TaskDetailsComponent
/tasks/task/:id
SidebarModule
SidebarComponent
AccordionModule CardModule
AccordionComponent CardComponent
TaskPreviewComponent
TaskMenuComponent
LessonsModule
LessonsComponent
/lessons
Only be generic when needed
Page components:
Try to be specific. Duplicate page variants, avoid pages that become fuzzy.
UI components:
Be generic. Don't couple to domain model.
Styling
Style encapsulation! 😍
Concept from Web Components
● A component has its own "shadow DOM"
● A component can only style its own elements
● Prevents "leaking" styling and unwanted side-effects
● No conventies like BEM, SMACSS, …, needed anymore
Example
@Component({
selector: 'p1-panel',
template: `
<header>...</header>
`,
styles: [`
header {
margin-bottom: 1em;
}
`]
})
class PanelComponent {}
Don't forget the :host element!
Don't forget the :host element!
@Component({
selector: 'p1-panel',
template: `
<div class="wrapper">
<header>...</header>
<footer>...</footer>
</div>
`,
styles: [`
.wrapper {
display: flex;
}
`]
})
class PanelComponent {}
Don't forget the :host element!
@Component({
selector: 'p1-panel',
template: `
<div class="wrapper">
<header>...</header>
<footer>...</footer>
</div>
`,
styles: [`
.wrapper {
display: flex;
}
`]
})
class PanelComponent {}
Don't forget the :host element!
@Component({
selector: 'p1-panel',
template: `
<header>...</header>
<footer>...</footer>
`,
styles: [`
:host {
display: flex;
}
`]
})
class PanelComponent {}
Don't forget the :host element!
@Component({
selector: 'p1-panel',
template: `
<header>...</header>
<footer>...</footer>
`,
styles: [`
:host {
display: flex;
}
`]
})
class PanelComponent {}
Robust default styling
Robust default styling
@Component({
selector: 'p1-panel',
template: `
<header>...</header>
<footer>...</footer>`,
styles: [`
:host {
/* ... */
}
`]
})
class PanelComponent {}
Robust default styling
@Component({
selector: 'p1-some-page',
template: `
<p1-panel *ngFor="week of weeks"></p1-panel>
`,
styles: [`
p1-panel {
/* ... */
}
`]
})
class SomePageComponent {}
@Component({
selector: 'p1-panel',
template: `
<header>...</header>
<footer>...</footer>`,
styles: [`
:host {
/* ... */
}
`]
})
class PanelComponent {}
Robust default styling
@Component({
selector: 'p1-some-page',
template: `
<p1-panel *ngFor="week of weeks"></p1-panel>
`,
styles: [`
p1-panel {
/* ... */
}
`]
})
class SomePageComponent {}
@Component({
selector: 'p1-panel',
template: `
<header>...</header>
<footer>...</footer>`,
styles: [`
:host {
/* ... */
}
`]
})
class PanelComponent {}
More
default styling!
Less
contextual styling!
Component styling vs. global styling 🤔
● Global styling
○ Try to avoid as much as possible!
● Component styling
○ Makes use of style encapsulation.
Awesome feature, :host-context()
@Component({
selector: 'p1-panel',
template: `
<header>...</header>
`,
styles: [`
header { margin-bottom: 1em; }
:host-context(p1-sidebar) header {
margin-bottom: 0;
}
`]
})
export class PanelComponent {}
Styling: lessons learned
1. Prefer component styling over global styling
2. Prevent using "cheats" like ::ng-deep. It's a smell! 💩
3. Don't forget the :host element!
4. Go for robust and flexible default styling
5. Make use of CSS inherit keyword
6. Use EM/REM instead of pixels
Testing! 🤮
Testing! 😍
Unit testing
Angular provides the following tools out-of-the-box:
➔ Karma runner with Jasmine as test framework 🤔
➔ TestBed as test API for Angular Components 🤔
TestBed API
describe('ButtonComponent', () => {
let fixture: ComponentFixture<ButtonComponent>;
let instance: ButtonComponent;
let debugElement: DebugElement;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ ButtonComponent ]
})
.compileComponents();
fixture = TestBed.createComponent(ButtonComponent);
instance = fixture.componentInstance;
debugElement = fixture.debugElement;
}));
it('should set the class name according to the [className] input', () => {
instance.className = 'danger';
fixture.detectChanges();
const button = debugElement.query(By.css('button')).nativeElement as HTMLButtonElement;
expect(button.classList.contains('danger')).toBeTruthy();
expect(button.classList.contains('success')).toBeFalsy();
});
});
TestBed API
describe('ButtonComponent', () => {
let fixture: ComponentFixture<ButtonComponent>;
let instance: ButtonComponent;
let debugElement: DebugElement;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ ButtonComponent ]
})
.compileComponents();
fixture = TestBed.createComponent(ButtonComponent);
instance = fixture.componentInstance;
debugElement = fixture.debugElement;
}));
it('should set the class name according to the [className] input', () => {
instance.className = 'danger';
fixture.detectChanges();
const button = debugElement.query(By.css('button')).nativeElement as HTMLButtonElement;
expect(button.classList.contains('danger')).toBeTruthy();
expect(button.classList.contains('success')).toBeFalsy();
});
});
TL;DR 🤔
Hello @netbasal/spectator! 😎
AWESOME library for component testing in Angular
➔ Simple API
➔ Better typings
➔ Custom matchers
➔ Mocking integration
➔ Simple way of querying
Spectator API
describe('ButtonComponent', () => {
const createComponent = createTestComponentFactory(ButtonComponent);
let spectator: Spectator<ButtonComponent>;
beforeEach(() => {
spectator = createComponent();
});
it('should set the class name according to the [className] input', () => {
spectator.component.className = 'danger';
spectator.detectChanges();
expect('button').toHaveClass('danger');
expect('button').not.toHaveClass('success');
});
});
Example: p1-keyboard
Go for readable test code!
it('should show the pressed key while pointing down', () => {
const keys = spectator.queryAll('.key');
const key1 = keys[0]; // q-key
const key2 = keys[1]; // w-key
spectator.dispatchMouseEvent(key1, 'pointerdown');
expect(key1.classList.contains('key-pressed')).toBe(true);
expect(key2.classList.contains('key-pressed')).toBe(false);
expect(spectator.component.activeKey).toBe('q');
spectator.dispatchMouseEvent(key1, 'pointerup');
expect(key1.classList.contains('key-pressed')).toBe(false);
expect(key2.classList.contains('key-pressed')).toBe(false);
expect(spectator.component.activeKey).toBeFalsy();
spectator.dispatchMouseEvent(key2, 'pointerdown');
expect(key1.classList.contains('key-pressed')).toBe(false);
expect(key2.classList.contains('key-pressed')).toBe(true);
expect(spectator.component.activeKey).toBe('w');
});
Go for readable test code!
it('should show the pressed key while pointing down', () => {
const keys = spectator.queryAll('.key');
const key1 = keys[0]; // q-key
const key2 = keys[1]; // w-key
spectator.dispatchMouseEvent(key1, 'pointerdown');
expect(key1.classList.contains('key-pressed')).toBe(true);
expect(key2.classList.contains('key-pressed')).toBe(false);
expect(spectator.component.activeKey).toBe('q');
spectator.dispatchMouseEvent(key1, 'pointerup');
expect(key1.classList.contains('key-pressed')).toBe(false);
expect(key2.classList.contains('key-pressed')).toBe(false);
expect(spectator.component.activeKey).toBeFalsy();
spectator.dispatchMouseEvent(key2, 'pointerdown');
expect(key1.classList.contains('key-pressed')).toBe(false);
expect(key2.classList.contains('key-pressed')).toBe(true);
expect(spectator.component.activeKey).toBe('w');
});
Go for readable test code!
it('should show the pressed key while pointing down', () => {
const keys = spectator.queryAll('.key');
const keyQ = keys[0];
const keyW = keys[1];
spectator.dispatchMouseEvent(keyQ, 'pointerdown');
expect(keyQ.classList.contains('key-pressed')).toBe(true);
expect(keyW.classList.contains('key-pressed')).toBe(false);
expect(spectator.component.activeKey).toBe('q');
spectator.dispatchMouseEvent(keyQ, 'pointerup');
expect(keyQ.classList.contains('key-pressed')).toBe(false);
expect(keyW.classList.contains('key-pressed')).toBe(false);
expect(spectator.component.activeKey).toBeFalsy();
spectator.dispatchMouseEvent(keyW, 'pointerdown');
expect(keyQ.classList.contains('key-pressed')).toBe(false);
expect(keyW.classList.contains('key-pressed')).toBe(true);
expect(spectator.component.activeKey).toBe('w');
});
Go for readable test code!
it('should show the pressed key while pointing down', () => {
const keys = spectator.queryAll('.key');
const keyQ = keys[0];
const keyW = keys[1];
spectator.dispatchMouseEvent(keyQ, 'pointerdown');
expect(keyQ.classList.contains('key-pressed')).toBe(true);
expect(keyW.classList.contains('key-pressed')).toBe(false);
expect(spectator.component.activeKey).toBe('q');
spectator.dispatchMouseEvent(keyQ, 'pointerup');
expect(keyQ.classList.contains('key-pressed')).toBe(false);
expect(keyW.classList.contains('key-pressed')).toBe(false);
expect(spectator.component.activeKey).toBeFalsy();
spectator.dispatchMouseEvent(keyW, 'pointerdown');
expect(keyQ.classList.contains('key-pressed')).toBe(false);
expect(keyW.classList.contains('key-pressed')).toBe(true);
expect(spectator.component.activeKey).toBe('w');
});
Go for readable test code!
it('should show the pressed key while pointing down', () => {
const [keyQ, keyW] = spectator.queryAll('.key');
spectator.dispatchMouseEvent(keyQ, 'pointerdown');
expect(keyQ.classList.contains('key-pressed')).toBe(true);
expect(keyW.classList.contains('key-pressed')).toBe(false);
expect(spectator.component.activeKey).toBe('q');
spectator.dispatchMouseEvent(keyQ, 'pointerup');
expect(keyQ.classList.contains('key-pressed')).toBe(false);
expect(keyW.classList.contains('key-pressed')).toBe(false);
expect(spectator.component.activeKey).toBeFalsy();
spectator.dispatchMouseEvent(keyW, 'pointerdown');
expect(keyQ.classList.contains('key-pressed')).toBe(false);
expect(keyW.classList.contains('key-pressed')).toBe(true);
expect(spectator.component.activeKey).toBe('w');
});
Go for readable test code!
it('should show the pressed key while pointing down', () => {
const [keyQ, keyW] = spectator.queryAll('.key');
spectator.dispatchMouseEvent(keyQ, 'pointerdown');
expect(keyQ.classList.contains('key-pressed')).toBe(true);
expect(keyW.classList.contains('key-pressed')).toBe(false);
expect(spectator.component.activeKey).toBe('q');
spectator.dispatchMouseEvent(keyQ, 'pointerup');
expect(keyQ.classList.contains('key-pressed')).toBe(false);
expect(keyW.classList.contains('key-pressed')).toBe(false);
expect(spectator.component.activeKey).toBeFalsy();
spectator.dispatchMouseEvent(keyW, 'pointerdown');
expect(keyQ.classList.contains('key-pressed')).toBe(false);
expect(keyW.classList.contains('key-pressed')).toBe(true);
expect(spectator.component.activeKey).toBe('w');
});
Go for readable test code!
const pointerDown = element => spectator.dispatchMouseEvent(element, 'pointerdown');
const pointerUp = element => spectator.dispatchMouseEvent(element, 'pointerdown');
it('should show the pressed key while pointing down', () => {
const [keyQ, keyW] = spectator.queryAll('.key');
pointerDown(keyQ);
expect(keyQ.classList.contains('key-pressed')).toBe(true);
expect(keyW.classList.contains('key-pressed')).toBe(false);
expect(spectator.component.activeKey).toBe('q');
pointerUp(keyQ);
expect(keyQ.classList.contains('key-pressed')).toBe(false);
expect(keyW.classList.contains('key-pressed')).toBe(false);
expect(spectator.component.activeKey).toBeFalsy();
pointerDown(keyW);
expect(keyQ.classList.contains('key-pressed')).toBe(false);
expect(keyW.classList.contains('key-pressed')).toBe(true);
expect(spectator.component.activeKey).toBe('w');
});
Go for readable test code!
const pointerDown = element => spectator.dispatchMouseEvent(element, 'pointerdown');
const pointerUp = element => spectator.dispatchMouseEvent(element, 'pointerdown');
it('should show the pressed key while pointing down', () => {
const [keyQ, keyW] = spectator.queryAll('.key');
pointerDown(keyQ);
expect(keyQ.classList.contains('key-pressed')).toBe(true);
expect(keyW.classList.contains('key-pressed')).toBe(false);
expect(spectator.component.activeKey).toBe('q');
pointerUp(keyQ);
expect(keyQ.classList.contains('key-pressed')).toBe(false);
expect(keyW.classList.contains('key-pressed')).toBe(false);
expect(spectator.component.activeKey).toBeFalsy();
pointerDown(keyW);
expect(keyQ.classList.contains('key-pressed')).toBe(false);
expect(keyW.classList.contains('key-pressed')).toBe(true);
expect(spectator.component.activeKey).toBe('w');
});
Go for readable test code!
const pointerDown = element => spectator.dispatchMouseEvent(element, 'pointerdown');
const pointerUp = element => spectator.dispatchMouseEvent(element, 'pointerdown');
it('should show the pressed key while pointing down', () => {
const [keyQ, keyW] = spectator.queryAll('.key');
pointerDown(keyQ);
expect(keyQ).toHaveClass('key-pressed');
expect(keyW).not.toHaveClass('key-pressed');
expect(spectator.component.activeKey).toBe('q');
pointerUp(keyQ);
expect(keyQ).not.toHaveClass('key-pressed');
expect(keyW).not.toHaveClass('key-pressed');
expect(spectator.component.activeKey).toBeFalsy();
pointerDown(keyW);
expect(keyQ).not.toHaveClass('key-pressed');
expect(keyW).toHaveClass('key-pressed');
expect(spectator.component.activeKey).toBe('w');
});
Class testing or component testing?
➔ Discuss it with your team
➔ What means the "unit" in "unit testing"?
➔ What does a class test prove about the quality of a component?
➔ Technical tests vs. functional tests
To mock... or not to mock?
Pure unit tests are run in isolation.
➔ Want to mock components/directives/pipes? Hello ng-mocks!
To mock... or not to mock?
➔ Pure unit tests are run in isolation.
➔ Want to mock components/directives/pipes? Hello ng-mocks!
describe('AudioPlayerComponent', () => {
const createHost = createHostComponentFactory({
component: AudioPlayerComponent,
declarations: [
MockComponent(EbMediaControlsComponent)
],
providers: [
{ provide: AUTOPLAY_DELAY, useValue: 1000 }
]
});
To mock... or not to mock?
➔ Pure unit tests are run in isolation.
➔ Want to mock components/directives/pipes? Hello ng-mocks!
But: nothing wrong with a bit of integration testing!
➔ Discuss with your team how you implement a test pyramid
Organize your testdata
● Consider complete and type-safe testdata
● Organize fixture data in one place
● Useful when using large & complex domain models
● Don't repeat yourself
Hello Jest!
● Fast, robust and powerful test framework
● Replaces Karma/Jasmine
● Simple migration path: compatible with Jasmine syntax (describe/it)
● Integrates very well with Angular (thymikee/jest-preset-angular)
● Integrates very well with Angular CLI (@angular-builders/jest)
Unit testing: lessons learned!
1. Consider Spectator instead of Angular TestBed
2. Consider Jest instead of Karma/Jasmine
3. Make unit testing FUN!
4. Go for functional component testing
- Don't think in methods, but in user events.
- Don't test from component class, but from DOM
Wrapping up...
Code quality
- Make your TypeScript compiler as strict as possible
- Make your TypeScript linting as strict as possible
- Take a look at Prettier
- Use modern JavaScript APIs and standards
- Don't fear old browsers!
- Embrace polyfills!
Complex stuff
- Take a look at @angular/cdk!
- Look at @angular/material for inspiration, to learn 'the Angular way'
- Use the power of MutationObserver, ResizeObserver, IntersectionObserver
- Don't be afraid to refactor!
Spend time on open-source development
We use open-source stuff every day. We should care about it.
1. Report your issue, and take time to investigate!
○ If you try hard, others will as well!
2. Try to fix it yourself, be a contributor!
○ Open-source projects are for everyone!
3. Improve yourself, read blogs. Know what's going on.
Reduce technical debt
1. Don't go for the best solution the first time!
○ Keep it simple so that you can obtain insights.
○ Prevent "over-engineering".
2. … but always improve things the second time!
○ Don't create refactor user stories, but take your refactor time for every user story!
○ This prevents postponing things and makes it more fun.
3. Too much technical debt? Bring it on the agenda for the upcoming sprint.
Thanks! 😁

Weitere ähnliche Inhalte

Was ist angesagt?

Was ist angesagt? (20)

Introduction to Angular 2
Introduction to Angular 2Introduction to Angular 2
Introduction to Angular 2
 
Angular 2... so can I use it now??
Angular 2... so can I use it now??Angular 2... so can I use it now??
Angular 2... so can I use it now??
 
Паразитируем на React-экосистеме (Angular 4+) / Алексей Охрименко (IPONWEB)
Паразитируем на React-экосистеме (Angular 4+) / Алексей Охрименко (IPONWEB)Паразитируем на React-экосистеме (Angular 4+) / Алексей Охрименко (IPONWEB)
Паразитируем на React-экосистеме (Angular 4+) / Алексей Охрименко (IPONWEB)
 
Angular Dependency Injection
Angular Dependency InjectionAngular Dependency Injection
Angular Dependency Injection
 
Introduction to Angular 2
Introduction to Angular 2Introduction to Angular 2
Introduction to Angular 2
 
Angular 2 : le réveil de la force
Angular 2 : le réveil de la forceAngular 2 : le réveil de la force
Angular 2 : le réveil de la force
 
Exploring Angular 2 - Episode 2
Exploring Angular 2 - Episode 2Exploring Angular 2 - Episode 2
Exploring Angular 2 - Episode 2
 
Angular Weekend
Angular WeekendAngular Weekend
Angular Weekend
 
Angular2 for Beginners
Angular2 for BeginnersAngular2 for Beginners
Angular2 for Beginners
 
Using hilt in a modularized project
Using hilt in a modularized projectUsing hilt in a modularized project
Using hilt in a modularized project
 
Building maintainable app
Building maintainable appBuilding maintainable app
Building maintainable app
 
Angular 2 - Better or worse
Angular 2 - Better or worseAngular 2 - Better or worse
Angular 2 - Better or worse
 
Reactive Programming with JavaScript
Reactive Programming with JavaScriptReactive Programming with JavaScript
Reactive Programming with JavaScript
 
Understanding react hooks
Understanding react hooksUnderstanding react hooks
Understanding react hooks
 
How Angular2 Can Improve Your AngularJS Apps Today!
How Angular2 Can Improve Your AngularJS Apps Today!How Angular2 Can Improve Your AngularJS Apps Today!
How Angular2 Can Improve Your AngularJS Apps Today!
 
Angular 1.x in action now
Angular 1.x in action nowAngular 1.x in action now
Angular 1.x in action now
 
Workshop 23: ReactJS, React & Redux testing
Workshop 23: ReactJS, React & Redux testingWorkshop 23: ReactJS, React & Redux testing
Workshop 23: ReactJS, React & Redux testing
 
Flutter hooks tutorial (part 1) flutter animation using hooks (use effect and...
Flutter hooks tutorial (part 1) flutter animation using hooks (use effect and...Flutter hooks tutorial (part 1) flutter animation using hooks (use effect and...
Flutter hooks tutorial (part 1) flutter animation using hooks (use effect and...
 
React on es6+
React on es6+React on es6+
React on es6+
 
Tech Webinar: Angular 2, Introduction to a new framework
Tech Webinar: Angular 2, Introduction to a new frameworkTech Webinar: Angular 2, Introduction to a new framework
Tech Webinar: Angular 2, Introduction to a new framework
 

Ähnlich wie 2 years of angular: lessons learned

Ähnlich wie 2 years of angular: lessons learned (20)

Stencil the time for vanilla web components has arrived
Stencil the time for vanilla web components has arrivedStencil the time for vanilla web components has arrived
Stencil the time for vanilla web components has arrived
 
Angular 2 - The Next Framework
Angular 2 - The Next FrameworkAngular 2 - The Next Framework
Angular 2 - The Next Framework
 
Stencil: The Time for Vanilla Web Components has Arrived
Stencil: The Time for Vanilla Web Components has ArrivedStencil: The Time for Vanilla Web Components has Arrived
Stencil: The Time for Vanilla Web Components has Arrived
 
angular fundamentals.pdf angular fundamentals.pdf
angular fundamentals.pdf angular fundamentals.pdfangular fundamentals.pdf angular fundamentals.pdf
angular fundamentals.pdf angular fundamentals.pdf
 
Angular 2 Migration - JHipster Meetup 6
Angular 2 Migration - JHipster Meetup 6Angular 2 Migration - JHipster Meetup 6
Angular 2 Migration - JHipster Meetup 6
 
Webinar: AngularJS and the WordPress REST API
Webinar: AngularJS and the WordPress REST APIWebinar: AngularJS and the WordPress REST API
Webinar: AngularJS and the WordPress REST API
 
Webinar: AngularJS and the WordPress REST API
Webinar: AngularJS and the WordPress REST APIWebinar: AngularJS and the WordPress REST API
Webinar: AngularJS and the WordPress REST API
 
Angular2 + rxjs
Angular2 + rxjsAngular2 + rxjs
Angular2 + rxjs
 
Angular JS2 Training Session #2
Angular JS2 Training Session #2Angular JS2 Training Session #2
Angular JS2 Training Session #2
 
Angular or React
Angular or ReactAngular or React
Angular or React
 
Voorhoede - Front-end architecture
Voorhoede - Front-end architectureVoorhoede - Front-end architecture
Voorhoede - Front-end architecture
 
Angular 2 at solutions.hamburg
Angular 2 at solutions.hamburgAngular 2 at solutions.hamburg
Angular 2 at solutions.hamburg
 
How to implement multiple layouts using React router V4.pptx
How to implement multiple layouts using React router V4.pptxHow to implement multiple layouts using React router V4.pptx
How to implement multiple layouts using React router V4.pptx
 
EWD 3 Training Course Part 38: Building a React.js application with QEWD, Part 4
EWD 3 Training Course Part 38: Building a React.js application with QEWD, Part 4EWD 3 Training Course Part 38: Building a React.js application with QEWD, Part 4
EWD 3 Training Course Part 38: Building a React.js application with QEWD, Part 4
 
Building Universal Web Apps with React ForwardJS 2017
Building Universal Web Apps with React ForwardJS 2017Building Universal Web Apps with React ForwardJS 2017
Building Universal Web Apps with React ForwardJS 2017
 
Building an Angular 2 App
Building an Angular 2 AppBuilding an Angular 2 App
Building an Angular 2 App
 
Speed up the site building with Drupal's Bootstrap Layout Builder
Speed up the site building with Drupal's Bootstrap Layout BuilderSpeed up the site building with Drupal's Bootstrap Layout Builder
Speed up the site building with Drupal's Bootstrap Layout Builder
 
How to Build SPA with Vue Router 2.0
How to Build SPA with Vue Router 2.0How to Build SPA with Vue Router 2.0
How to Build SPA with Vue Router 2.0
 
Gutenberg sous le capot, modules réutilisables
Gutenberg sous le capot, modules réutilisablesGutenberg sous le capot, modules réutilisables
Gutenberg sous le capot, modules réutilisables
 
Protocol-Oriented Programming in Swift
Protocol-Oriented Programming in SwiftProtocol-Oriented Programming in Swift
Protocol-Oriented Programming in Swift
 

Kürzlich hochgeladen

+971565801893>>SAFE AND ORIGINAL ABORTION PILLS FOR SALE IN DUBAI AND ABUDHAB...
+971565801893>>SAFE AND ORIGINAL ABORTION PILLS FOR SALE IN DUBAI AND ABUDHAB...+971565801893>>SAFE AND ORIGINAL ABORTION PILLS FOR SALE IN DUBAI AND ABUDHAB...
+971565801893>>SAFE AND ORIGINAL ABORTION PILLS FOR SALE IN DUBAI AND ABUDHAB...
Health
 
TECUNIQUE: Success Stories: IT Service provider
TECUNIQUE: Success Stories: IT Service providerTECUNIQUE: Success Stories: IT Service provider
TECUNIQUE: Success Stories: IT Service provider
mohitmore19
 
CALL ON ➥8923113531 🔝Call Girls Kakori Lucknow best sexual service Online ☂️
CALL ON ➥8923113531 🔝Call Girls Kakori Lucknow best sexual service Online  ☂️CALL ON ➥8923113531 🔝Call Girls Kakori Lucknow best sexual service Online  ☂️
CALL ON ➥8923113531 🔝Call Girls Kakori Lucknow best sexual service Online ☂️
anilsa9823
 

Kürzlich hochgeladen (20)

A Secure and Reliable Document Management System is Essential.docx
A Secure and Reliable Document Management System is Essential.docxA Secure and Reliable Document Management System is Essential.docx
A Secure and Reliable Document Management System is Essential.docx
 
How To Use Server-Side Rendering with Nuxt.js
How To Use Server-Side Rendering with Nuxt.jsHow To Use Server-Side Rendering with Nuxt.js
How To Use Server-Side Rendering with Nuxt.js
 
5 Signs You Need a Fashion PLM Software.pdf
5 Signs You Need a Fashion PLM Software.pdf5 Signs You Need a Fashion PLM Software.pdf
5 Signs You Need a Fashion PLM Software.pdf
 
+971565801893>>SAFE AND ORIGINAL ABORTION PILLS FOR SALE IN DUBAI AND ABUDHAB...
+971565801893>>SAFE AND ORIGINAL ABORTION PILLS FOR SALE IN DUBAI AND ABUDHAB...+971565801893>>SAFE AND ORIGINAL ABORTION PILLS FOR SALE IN DUBAI AND ABUDHAB...
+971565801893>>SAFE AND ORIGINAL ABORTION PILLS FOR SALE IN DUBAI AND ABUDHAB...
 
How To Troubleshoot Collaboration Apps for the Modern Connected Worker
How To Troubleshoot Collaboration Apps for the Modern Connected WorkerHow To Troubleshoot Collaboration Apps for the Modern Connected Worker
How To Troubleshoot Collaboration Apps for the Modern Connected Worker
 
HR Software Buyers Guide in 2024 - HRSoftware.com
HR Software Buyers Guide in 2024 - HRSoftware.comHR Software Buyers Guide in 2024 - HRSoftware.com
HR Software Buyers Guide in 2024 - HRSoftware.com
 
Optimizing AI for immediate response in Smart CCTV
Optimizing AI for immediate response in Smart CCTVOptimizing AI for immediate response in Smart CCTV
Optimizing AI for immediate response in Smart CCTV
 
TECUNIQUE: Success Stories: IT Service provider
TECUNIQUE: Success Stories: IT Service providerTECUNIQUE: Success Stories: IT Service provider
TECUNIQUE: Success Stories: IT Service provider
 
Unveiling the Tech Salsa of LAMs with Janus in Real-Time Applications
Unveiling the Tech Salsa of LAMs with Janus in Real-Time ApplicationsUnveiling the Tech Salsa of LAMs with Janus in Real-Time Applications
Unveiling the Tech Salsa of LAMs with Janus in Real-Time Applications
 
Diamond Application Development Crafting Solutions with Precision
Diamond Application Development Crafting Solutions with PrecisionDiamond Application Development Crafting Solutions with Precision
Diamond Application Development Crafting Solutions with Precision
 
Vip Call Girls Noida ➡️ Delhi ➡️ 9999965857 No Advance 24HRS Live
Vip Call Girls Noida ➡️ Delhi ➡️ 9999965857 No Advance 24HRS LiveVip Call Girls Noida ➡️ Delhi ➡️ 9999965857 No Advance 24HRS Live
Vip Call Girls Noida ➡️ Delhi ➡️ 9999965857 No Advance 24HRS Live
 
call girls in Vaishali (Ghaziabad) 🔝 >༒8448380779 🔝 genuine Escort Service 🔝✔️✔️
call girls in Vaishali (Ghaziabad) 🔝 >༒8448380779 🔝 genuine Escort Service 🔝✔️✔️call girls in Vaishali (Ghaziabad) 🔝 >༒8448380779 🔝 genuine Escort Service 🔝✔️✔️
call girls in Vaishali (Ghaziabad) 🔝 >༒8448380779 🔝 genuine Escort Service 🔝✔️✔️
 
Learn the Fundamentals of XCUITest Framework_ A Beginner's Guide.pdf
Learn the Fundamentals of XCUITest Framework_ A Beginner's Guide.pdfLearn the Fundamentals of XCUITest Framework_ A Beginner's Guide.pdf
Learn the Fundamentals of XCUITest Framework_ A Beginner's Guide.pdf
 
Shapes for Sharing between Graph Data Spaces - and Epistemic Querying of RDF-...
Shapes for Sharing between Graph Data Spaces - and Epistemic Querying of RDF-...Shapes for Sharing between Graph Data Spaces - and Epistemic Querying of RDF-...
Shapes for Sharing between Graph Data Spaces - and Epistemic Querying of RDF-...
 
Unlocking the Future of AI Agents with Large Language Models
Unlocking the Future of AI Agents with Large Language ModelsUnlocking the Future of AI Agents with Large Language Models
Unlocking the Future of AI Agents with Large Language Models
 
Reassessing the Bedrock of Clinical Function Models: An Examination of Large ...
Reassessing the Bedrock of Clinical Function Models: An Examination of Large ...Reassessing the Bedrock of Clinical Function Models: An Examination of Large ...
Reassessing the Bedrock of Clinical Function Models: An Examination of Large ...
 
Tech Tuesday-Harness the Power of Effective Resource Planning with OnePlan’s ...
Tech Tuesday-Harness the Power of Effective Resource Planning with OnePlan’s ...Tech Tuesday-Harness the Power of Effective Resource Planning with OnePlan’s ...
Tech Tuesday-Harness the Power of Effective Resource Planning with OnePlan’s ...
 
CALL ON ➥8923113531 🔝Call Girls Kakori Lucknow best sexual service Online ☂️
CALL ON ➥8923113531 🔝Call Girls Kakori Lucknow best sexual service Online  ☂️CALL ON ➥8923113531 🔝Call Girls Kakori Lucknow best sexual service Online  ☂️
CALL ON ➥8923113531 🔝Call Girls Kakori Lucknow best sexual service Online ☂️
 
Microsoft AI Transformation Partner Playbook.pdf
Microsoft AI Transformation Partner Playbook.pdfMicrosoft AI Transformation Partner Playbook.pdf
Microsoft AI Transformation Partner Playbook.pdf
 
SyndBuddy AI 2k Review 2024: Revolutionizing Content Syndication with AI
SyndBuddy AI 2k Review 2024: Revolutionizing Content Syndication with AISyndBuddy AI 2k Review 2024: Revolutionizing Content Syndication with AI
SyndBuddy AI 2k Review 2024: Revolutionizing Content Syndication with AI
 

2 years of angular: lessons learned

  • 3.
  • 4. About Bingel ● Full lesson support ● Instruction, exercises & testing ● Maths & language courses ● Adaptive learning ● Modern & cross-platform
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 13. How it started… ng new bingel-app$ ng new bingel-student-app
  • 16. Architecture student app tasks module lessons module results module shared module
  • 17. Architecture student app tasks module lessons module results module shared module teacher app
  • 18. Architecture student app tasks module lessons module results module shared module teacher app instruction module analysis module shared module
  • 19. Architecture student app tasks module lessons module results module shared module teacher app "shared code" instruction module analysis module shared module
  • 20. Architecture student app tasks module lessons module results module shared module teacher app ui library interactions library api library common library instruction module analysis module shared module
  • 21. Architecture student app tasks module lessons module results module shared module teacher app @p1/ui @p1/interactions @p1/api @p1/common preview app admin app instruction module analysis module shared module preview module licensing module content graph module @p1/features
  • 22. Architecture student app tasks module lessons module results module shared module teacher app @p1/ui @p1/interactions @p1/api @p1/common preview app admin app instruction module analysis module shared module preview module licensing module content graph module @p1/features single repository (monorepo)
  • 23. How?
  • 24. Nx workspaces ● Builts on top of Angular CLI ● Supports monorepo approach ● Provides "affected builds" option: ○ build only what has been changed ● Better separation of packages
  • 25. Architecture: lessons learned 1. Consider a monorepo ○ simple versioning & release process ○ easier refactoring ○ well supported by Angular CLI + Nx Workspaces 2. Think in packages and their responsibilities
  • 27. Routing [ { path: 'overview', loadChildren: 'overview/overview.module#OverviewModule' }, { path: 'lessons', loadChildren: 'lessons/lessons.module#LessonsModule' }, { path: 'tasks', loadChildren: 'tasks/tasks.module#TasksModule' } ];
  • 28. Routing guards { path: 'tasks', resolve: { blocks: BlocksResolver }, children: [ { path: '', canActivate: [TasksRedirectGuard] }, { path: 'blocks', component: TasksBlocksComponent }, { path: 'blocks/:blockId/weeks/:weekId', component: TasksSelectionComponent } ] }
  • 29. Routing guards { path: 'tasks', resolve: { blocks: BlocksResolver }, children: [ { path: '', canActivate: [TasksRedirectGuard] }, { path: 'blocks', component: TasksBlocksComponent }, { path: 'blocks/:blockId/weeks/:weekId', component: TasksSelectionComponent } ] }
  • 30. Routing in Angular is reactive.
  • 31. Routing in Angular is reactive. But not necessarily.
  • 32. ActivatedRoute class WeekComponent implements OnInit { week: Week; constructor(private route: ActivatedRoute, private weekService: WeekService) {} ngOnInit(): void { this.route.paramMap .pipe( map(params => params.get('weekId')), switchMap(weekId => this.weekService.getWeek(weekId)) ) .subscribe(week => { this.week = week; }); } }
  • 33. ActivatedRoute class WeekComponent implements OnInit { week: Week; constructor(private route: ActivatedRoute, private weekService: WeekService) {} ngOnInit(): void { this.route.paramMap .pipe( map(params => params.get('weekId')), switchMap(weekId => this.weekService.getWeek(weekId)) ) .subscribe(week => { this.week = week; }); } } reactive!
  • 34. ActivatedRoute vs. ActivatedRouteSnapShot class WeekComponent { week: Week; constructor(private route: ActivatedRoute, private weekService: WeekService) {} ngOnInit(): void { this.weekService .getWeek(this.route.snapshot.paramMap.get('weekId')) .subscribe(week => this.week = week); } }
  • 35. ActivatedRoute vs. ActivatedRouteSnapShot class WeekComponent { week: Week; constructor(private route: ActivatedRoute, private weekService: WeekService) {} ngOnInit(): void { this.weekService .getWeek(this.route.snapshot.paramMap.get('weekId')) .subscribe(week => this.week = week); } } not reactive (static)!
  • 36. Routing: lessons learned 1. Make use of lazy-loaded feature modules ○ Breaks down bundles into smaller chunks 2. Make smart use of guards! 3. Get used to the (reactive) API of Angular Router
  • 38. Different types of components Page Components Routed, fetches data, (almost) no presentation logic1 Feature Components Specific presentation logic (bound to domain model)2 UI Components Generic presentation logic (not bound to domain model)3
  • 39. Different types of components TasksModule TaskSelectionComponent /tasks/blocks/week/:id TaskDetailsComponent /tasks/task/:id LessonsModule LessonsComponent /lessons
  • 40. Different types of components TasksModule TaskSelectionComponent /tasks/blocks/week/:id TaskDetailsComponent /tasks/task/:id SidebarModule SidebarComponent AccordionModule CardModule AccordionComponent CardComponent LessonsModule LessonsComponent /lessons
  • 41. Different types of components TasksModule TaskSelectionComponent /tasks/blocks/week/:id TaskDetailsComponent /tasks/task/:id SidebarModule SidebarComponent AccordionModule CardModule AccordionComponent CardComponent TaskPreviewComponent TaskMenuComponent LessonsModule LessonsComponent /lessons
  • 42. Different types of components TasksModule TaskSelectionComponent /tasks/blocks/week/:id TaskDetailsComponent /tasks/task/:id SidebarModule SidebarComponent AccordionModule CardModule AccordionComponent CardComponent TaskPreviewComponent TaskMenuComponent LessonsModule LessonsComponent /lessons
  • 43. Only be generic when needed Page components: Try to be specific. Duplicate page variants, avoid pages that become fuzzy. UI components: Be generic. Don't couple to domain model.
  • 45. Style encapsulation! 😍 Concept from Web Components ● A component has its own "shadow DOM" ● A component can only style its own elements ● Prevents "leaking" styling and unwanted side-effects ● No conventies like BEM, SMACSS, …, needed anymore
  • 46. Example @Component({ selector: 'p1-panel', template: ` <header>...</header> `, styles: [` header { margin-bottom: 1em; } `] }) class PanelComponent {}
  • 47. Don't forget the :host element!
  • 48. Don't forget the :host element! @Component({ selector: 'p1-panel', template: ` <div class="wrapper"> <header>...</header> <footer>...</footer> </div> `, styles: [` .wrapper { display: flex; } `] }) class PanelComponent {}
  • 49. Don't forget the :host element! @Component({ selector: 'p1-panel', template: ` <div class="wrapper"> <header>...</header> <footer>...</footer> </div> `, styles: [` .wrapper { display: flex; } `] }) class PanelComponent {}
  • 50. Don't forget the :host element! @Component({ selector: 'p1-panel', template: ` <header>...</header> <footer>...</footer> `, styles: [` :host { display: flex; } `] }) class PanelComponent {}
  • 51. Don't forget the :host element! @Component({ selector: 'p1-panel', template: ` <header>...</header> <footer>...</footer> `, styles: [` :host { display: flex; } `] }) class PanelComponent {}
  • 53. Robust default styling @Component({ selector: 'p1-panel', template: ` <header>...</header> <footer>...</footer>`, styles: [` :host { /* ... */ } `] }) class PanelComponent {}
  • 54. Robust default styling @Component({ selector: 'p1-some-page', template: ` <p1-panel *ngFor="week of weeks"></p1-panel> `, styles: [` p1-panel { /* ... */ } `] }) class SomePageComponent {} @Component({ selector: 'p1-panel', template: ` <header>...</header> <footer>...</footer>`, styles: [` :host { /* ... */ } `] }) class PanelComponent {}
  • 55. Robust default styling @Component({ selector: 'p1-some-page', template: ` <p1-panel *ngFor="week of weeks"></p1-panel> `, styles: [` p1-panel { /* ... */ } `] }) class SomePageComponent {} @Component({ selector: 'p1-panel', template: ` <header>...</header> <footer>...</footer>`, styles: [` :host { /* ... */ } `] }) class PanelComponent {} More default styling! Less contextual styling!
  • 56. Component styling vs. global styling 🤔 ● Global styling ○ Try to avoid as much as possible! ● Component styling ○ Makes use of style encapsulation.
  • 57. Awesome feature, :host-context() @Component({ selector: 'p1-panel', template: ` <header>...</header> `, styles: [` header { margin-bottom: 1em; } :host-context(p1-sidebar) header { margin-bottom: 0; } `] }) export class PanelComponent {}
  • 58. Styling: lessons learned 1. Prefer component styling over global styling 2. Prevent using "cheats" like ::ng-deep. It's a smell! 💩 3. Don't forget the :host element! 4. Go for robust and flexible default styling 5. Make use of CSS inherit keyword 6. Use EM/REM instead of pixels
  • 61. Unit testing Angular provides the following tools out-of-the-box: ➔ Karma runner with Jasmine as test framework 🤔 ➔ TestBed as test API for Angular Components 🤔
  • 62. TestBed API describe('ButtonComponent', () => { let fixture: ComponentFixture<ButtonComponent>; let instance: ButtonComponent; let debugElement: DebugElement; beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [ ButtonComponent ] }) .compileComponents(); fixture = TestBed.createComponent(ButtonComponent); instance = fixture.componentInstance; debugElement = fixture.debugElement; })); it('should set the class name according to the [className] input', () => { instance.className = 'danger'; fixture.detectChanges(); const button = debugElement.query(By.css('button')).nativeElement as HTMLButtonElement; expect(button.classList.contains('danger')).toBeTruthy(); expect(button.classList.contains('success')).toBeFalsy(); }); });
  • 63. TestBed API describe('ButtonComponent', () => { let fixture: ComponentFixture<ButtonComponent>; let instance: ButtonComponent; let debugElement: DebugElement; beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [ ButtonComponent ] }) .compileComponents(); fixture = TestBed.createComponent(ButtonComponent); instance = fixture.componentInstance; debugElement = fixture.debugElement; })); it('should set the class name according to the [className] input', () => { instance.className = 'danger'; fixture.detectChanges(); const button = debugElement.query(By.css('button')).nativeElement as HTMLButtonElement; expect(button.classList.contains('danger')).toBeTruthy(); expect(button.classList.contains('success')).toBeFalsy(); }); }); TL;DR 🤔
  • 64. Hello @netbasal/spectator! 😎 AWESOME library for component testing in Angular ➔ Simple API ➔ Better typings ➔ Custom matchers ➔ Mocking integration ➔ Simple way of querying
  • 65. Spectator API describe('ButtonComponent', () => { const createComponent = createTestComponentFactory(ButtonComponent); let spectator: Spectator<ButtonComponent>; beforeEach(() => { spectator = createComponent(); }); it('should set the class name according to the [className] input', () => { spectator.component.className = 'danger'; spectator.detectChanges(); expect('button').toHaveClass('danger'); expect('button').not.toHaveClass('success'); }); });
  • 67. Go for readable test code! it('should show the pressed key while pointing down', () => { const keys = spectator.queryAll('.key'); const key1 = keys[0]; // q-key const key2 = keys[1]; // w-key spectator.dispatchMouseEvent(key1, 'pointerdown'); expect(key1.classList.contains('key-pressed')).toBe(true); expect(key2.classList.contains('key-pressed')).toBe(false); expect(spectator.component.activeKey).toBe('q'); spectator.dispatchMouseEvent(key1, 'pointerup'); expect(key1.classList.contains('key-pressed')).toBe(false); expect(key2.classList.contains('key-pressed')).toBe(false); expect(spectator.component.activeKey).toBeFalsy(); spectator.dispatchMouseEvent(key2, 'pointerdown'); expect(key1.classList.contains('key-pressed')).toBe(false); expect(key2.classList.contains('key-pressed')).toBe(true); expect(spectator.component.activeKey).toBe('w'); });
  • 68. Go for readable test code! it('should show the pressed key while pointing down', () => { const keys = spectator.queryAll('.key'); const key1 = keys[0]; // q-key const key2 = keys[1]; // w-key spectator.dispatchMouseEvent(key1, 'pointerdown'); expect(key1.classList.contains('key-pressed')).toBe(true); expect(key2.classList.contains('key-pressed')).toBe(false); expect(spectator.component.activeKey).toBe('q'); spectator.dispatchMouseEvent(key1, 'pointerup'); expect(key1.classList.contains('key-pressed')).toBe(false); expect(key2.classList.contains('key-pressed')).toBe(false); expect(spectator.component.activeKey).toBeFalsy(); spectator.dispatchMouseEvent(key2, 'pointerdown'); expect(key1.classList.contains('key-pressed')).toBe(false); expect(key2.classList.contains('key-pressed')).toBe(true); expect(spectator.component.activeKey).toBe('w'); });
  • 69. Go for readable test code! it('should show the pressed key while pointing down', () => { const keys = spectator.queryAll('.key'); const keyQ = keys[0]; const keyW = keys[1]; spectator.dispatchMouseEvent(keyQ, 'pointerdown'); expect(keyQ.classList.contains('key-pressed')).toBe(true); expect(keyW.classList.contains('key-pressed')).toBe(false); expect(spectator.component.activeKey).toBe('q'); spectator.dispatchMouseEvent(keyQ, 'pointerup'); expect(keyQ.classList.contains('key-pressed')).toBe(false); expect(keyW.classList.contains('key-pressed')).toBe(false); expect(spectator.component.activeKey).toBeFalsy(); spectator.dispatchMouseEvent(keyW, 'pointerdown'); expect(keyQ.classList.contains('key-pressed')).toBe(false); expect(keyW.classList.contains('key-pressed')).toBe(true); expect(spectator.component.activeKey).toBe('w'); });
  • 70. Go for readable test code! it('should show the pressed key while pointing down', () => { const keys = spectator.queryAll('.key'); const keyQ = keys[0]; const keyW = keys[1]; spectator.dispatchMouseEvent(keyQ, 'pointerdown'); expect(keyQ.classList.contains('key-pressed')).toBe(true); expect(keyW.classList.contains('key-pressed')).toBe(false); expect(spectator.component.activeKey).toBe('q'); spectator.dispatchMouseEvent(keyQ, 'pointerup'); expect(keyQ.classList.contains('key-pressed')).toBe(false); expect(keyW.classList.contains('key-pressed')).toBe(false); expect(spectator.component.activeKey).toBeFalsy(); spectator.dispatchMouseEvent(keyW, 'pointerdown'); expect(keyQ.classList.contains('key-pressed')).toBe(false); expect(keyW.classList.contains('key-pressed')).toBe(true); expect(spectator.component.activeKey).toBe('w'); });
  • 71. Go for readable test code! it('should show the pressed key while pointing down', () => { const [keyQ, keyW] = spectator.queryAll('.key'); spectator.dispatchMouseEvent(keyQ, 'pointerdown'); expect(keyQ.classList.contains('key-pressed')).toBe(true); expect(keyW.classList.contains('key-pressed')).toBe(false); expect(spectator.component.activeKey).toBe('q'); spectator.dispatchMouseEvent(keyQ, 'pointerup'); expect(keyQ.classList.contains('key-pressed')).toBe(false); expect(keyW.classList.contains('key-pressed')).toBe(false); expect(spectator.component.activeKey).toBeFalsy(); spectator.dispatchMouseEvent(keyW, 'pointerdown'); expect(keyQ.classList.contains('key-pressed')).toBe(false); expect(keyW.classList.contains('key-pressed')).toBe(true); expect(spectator.component.activeKey).toBe('w'); });
  • 72. Go for readable test code! it('should show the pressed key while pointing down', () => { const [keyQ, keyW] = spectator.queryAll('.key'); spectator.dispatchMouseEvent(keyQ, 'pointerdown'); expect(keyQ.classList.contains('key-pressed')).toBe(true); expect(keyW.classList.contains('key-pressed')).toBe(false); expect(spectator.component.activeKey).toBe('q'); spectator.dispatchMouseEvent(keyQ, 'pointerup'); expect(keyQ.classList.contains('key-pressed')).toBe(false); expect(keyW.classList.contains('key-pressed')).toBe(false); expect(spectator.component.activeKey).toBeFalsy(); spectator.dispatchMouseEvent(keyW, 'pointerdown'); expect(keyQ.classList.contains('key-pressed')).toBe(false); expect(keyW.classList.contains('key-pressed')).toBe(true); expect(spectator.component.activeKey).toBe('w'); });
  • 73. Go for readable test code! const pointerDown = element => spectator.dispatchMouseEvent(element, 'pointerdown'); const pointerUp = element => spectator.dispatchMouseEvent(element, 'pointerdown'); it('should show the pressed key while pointing down', () => { const [keyQ, keyW] = spectator.queryAll('.key'); pointerDown(keyQ); expect(keyQ.classList.contains('key-pressed')).toBe(true); expect(keyW.classList.contains('key-pressed')).toBe(false); expect(spectator.component.activeKey).toBe('q'); pointerUp(keyQ); expect(keyQ.classList.contains('key-pressed')).toBe(false); expect(keyW.classList.contains('key-pressed')).toBe(false); expect(spectator.component.activeKey).toBeFalsy(); pointerDown(keyW); expect(keyQ.classList.contains('key-pressed')).toBe(false); expect(keyW.classList.contains('key-pressed')).toBe(true); expect(spectator.component.activeKey).toBe('w'); });
  • 74. Go for readable test code! const pointerDown = element => spectator.dispatchMouseEvent(element, 'pointerdown'); const pointerUp = element => spectator.dispatchMouseEvent(element, 'pointerdown'); it('should show the pressed key while pointing down', () => { const [keyQ, keyW] = spectator.queryAll('.key'); pointerDown(keyQ); expect(keyQ.classList.contains('key-pressed')).toBe(true); expect(keyW.classList.contains('key-pressed')).toBe(false); expect(spectator.component.activeKey).toBe('q'); pointerUp(keyQ); expect(keyQ.classList.contains('key-pressed')).toBe(false); expect(keyW.classList.contains('key-pressed')).toBe(false); expect(spectator.component.activeKey).toBeFalsy(); pointerDown(keyW); expect(keyQ.classList.contains('key-pressed')).toBe(false); expect(keyW.classList.contains('key-pressed')).toBe(true); expect(spectator.component.activeKey).toBe('w'); });
  • 75. Go for readable test code! const pointerDown = element => spectator.dispatchMouseEvent(element, 'pointerdown'); const pointerUp = element => spectator.dispatchMouseEvent(element, 'pointerdown'); it('should show the pressed key while pointing down', () => { const [keyQ, keyW] = spectator.queryAll('.key'); pointerDown(keyQ); expect(keyQ).toHaveClass('key-pressed'); expect(keyW).not.toHaveClass('key-pressed'); expect(spectator.component.activeKey).toBe('q'); pointerUp(keyQ); expect(keyQ).not.toHaveClass('key-pressed'); expect(keyW).not.toHaveClass('key-pressed'); expect(spectator.component.activeKey).toBeFalsy(); pointerDown(keyW); expect(keyQ).not.toHaveClass('key-pressed'); expect(keyW).toHaveClass('key-pressed'); expect(spectator.component.activeKey).toBe('w'); });
  • 76. Class testing or component testing? ➔ Discuss it with your team ➔ What means the "unit" in "unit testing"? ➔ What does a class test prove about the quality of a component? ➔ Technical tests vs. functional tests
  • 77. To mock... or not to mock? Pure unit tests are run in isolation. ➔ Want to mock components/directives/pipes? Hello ng-mocks!
  • 78. To mock... or not to mock? ➔ Pure unit tests are run in isolation. ➔ Want to mock components/directives/pipes? Hello ng-mocks! describe('AudioPlayerComponent', () => { const createHost = createHostComponentFactory({ component: AudioPlayerComponent, declarations: [ MockComponent(EbMediaControlsComponent) ], providers: [ { provide: AUTOPLAY_DELAY, useValue: 1000 } ] });
  • 79. To mock... or not to mock? ➔ Pure unit tests are run in isolation. ➔ Want to mock components/directives/pipes? Hello ng-mocks! But: nothing wrong with a bit of integration testing! ➔ Discuss with your team how you implement a test pyramid
  • 80. Organize your testdata ● Consider complete and type-safe testdata ● Organize fixture data in one place ● Useful when using large & complex domain models ● Don't repeat yourself
  • 81. Hello Jest! ● Fast, robust and powerful test framework ● Replaces Karma/Jasmine ● Simple migration path: compatible with Jasmine syntax (describe/it) ● Integrates very well with Angular (thymikee/jest-preset-angular) ● Integrates very well with Angular CLI (@angular-builders/jest)
  • 82. Unit testing: lessons learned! 1. Consider Spectator instead of Angular TestBed 2. Consider Jest instead of Karma/Jasmine 3. Make unit testing FUN! 4. Go for functional component testing - Don't think in methods, but in user events. - Don't test from component class, but from DOM
  • 84. Code quality - Make your TypeScript compiler as strict as possible - Make your TypeScript linting as strict as possible - Take a look at Prettier - Use modern JavaScript APIs and standards - Don't fear old browsers! - Embrace polyfills!
  • 85. Complex stuff - Take a look at @angular/cdk! - Look at @angular/material for inspiration, to learn 'the Angular way' - Use the power of MutationObserver, ResizeObserver, IntersectionObserver - Don't be afraid to refactor!
  • 86. Spend time on open-source development We use open-source stuff every day. We should care about it. 1. Report your issue, and take time to investigate! ○ If you try hard, others will as well! 2. Try to fix it yourself, be a contributor! ○ Open-source projects are for everyone! 3. Improve yourself, read blogs. Know what's going on.
  • 87. Reduce technical debt 1. Don't go for the best solution the first time! ○ Keep it simple so that you can obtain insights. ○ Prevent "over-engineering". 2. … but always improve things the second time! ○ Don't create refactor user stories, but take your refactor time for every user story! ○ This prevents postponing things and makes it more fun. 3. Too much technical debt? Bring it on the agenda for the upcoming sprint.