Over the past two years I have been working at Malmberg to build an amazing new platform. In this presentation I would like to share the lessons we learned using Angular.
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
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
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
56. Component styling vs. global styling 🤔
● Global styling
○ Try to avoid as much as possible!
● Component styling
○ Makes use of style encapsulation.
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.