Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

2 years of angular: lessons learned

97 views

Published on

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.

Published in: Software
  • Be the first to comment

2 years of angular: lessons learned

  1. 1. Lessons Learned 2 years of
  2. 2. Dirk Luijk @dirkluijk #frontend #angular #fullstack
  3. 3. About Bingel ● Full lesson support ● Instruction, exercises & testing ● Maths & language courses ● Adaptive learning ● Modern & cross-platform
  4. 4. Architecture
  5. 5. How it started… ng new bingel-app$ ng new bingel-student-app
  6. 6. Architecture student app
  7. 7. Architecture student app tasks module lessons module results module
  8. 8. Architecture student app tasks module lessons module results module shared module
  9. 9. Architecture student app tasks module lessons module results module shared module teacher app
  10. 10. Architecture student app tasks module lessons module results module shared module teacher app instruction module analysis module shared module
  11. 11. Architecture student app tasks module lessons module results module shared module teacher app "shared code" instruction module analysis module shared module
  12. 12. 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
  13. 13. 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
  14. 14. 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)
  15. 15. How?
  16. 16. 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
  17. 17. 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
  18. 18. Routing
  19. 19. Routing [ { path: 'overview', loadChildren: 'overview/overview.module#OverviewModule' }, { path: 'lessons', loadChildren: 'lessons/lessons.module#LessonsModule' }, { path: 'tasks', loadChildren: 'tasks/tasks.module#TasksModule' } ];
  20. 20. Routing guards { path: 'tasks', resolve: { blocks: BlocksResolver }, children: [ { path: '', canActivate: [TasksRedirectGuard] }, { path: 'blocks', component: TasksBlocksComponent }, { path: 'blocks/:blockId/weeks/:weekId', component: TasksSelectionComponent } ] }
  21. 21. Routing guards { path: 'tasks', resolve: { blocks: BlocksResolver }, children: [ { path: '', canActivate: [TasksRedirectGuard] }, { path: 'blocks', component: TasksBlocksComponent }, { path: 'blocks/:blockId/weeks/:weekId', component: TasksSelectionComponent } ] }
  22. 22. Routing in Angular is reactive.
  23. 23. Routing in Angular is reactive. But not necessarily.
  24. 24. 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; }); } }
  25. 25. 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!
  26. 26. 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); } }
  27. 27. 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)!
  28. 28. 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
  29. 29. Components
  30. 30. 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
  31. 31. Different types of components TasksModule TaskSelectionComponent /tasks/blocks/week/:id TaskDetailsComponent /tasks/task/:id LessonsModule LessonsComponent /lessons
  32. 32. Different types of components TasksModule TaskSelectionComponent /tasks/blocks/week/:id TaskDetailsComponent /tasks/task/:id SidebarModule SidebarComponent AccordionModule CardModule AccordionComponent CardComponent LessonsModule LessonsComponent /lessons
  33. 33. 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
  34. 34. 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
  35. 35. 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.
  36. 36. Styling
  37. 37. 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
  38. 38. Example @Component({ selector: 'p1-panel', template: ` <header>...</header> `, styles: [` header { margin-bottom: 1em; } `] }) class PanelComponent {}
  39. 39. Don't forget the :host element!
  40. 40. 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 {}
  41. 41. 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 {}
  42. 42. Don't forget the :host element! @Component({ selector: 'p1-panel', template: ` <header>...</header> <footer>...</footer> `, styles: [` :host { display: flex; } `] }) class PanelComponent {}
  43. 43. Don't forget the :host element! @Component({ selector: 'p1-panel', template: ` <header>...</header> <footer>...</footer> `, styles: [` :host { display: flex; } `] }) class PanelComponent {}
  44. 44. Robust default styling
  45. 45. Robust default styling @Component({ selector: 'p1-panel', template: ` <header>...</header> <footer>...</footer>`, styles: [` :host { /* ... */ } `] }) class PanelComponent {}
  46. 46. 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 {}
  47. 47. 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!
  48. 48. Component styling vs. global styling 🤔 ● Global styling ○ Try to avoid as much as possible! ● Component styling ○ Makes use of style encapsulation.
  49. 49. 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 {}
  50. 50. 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
  51. 51. Testing! 🤮
  52. 52. Testing! 😍
  53. 53. 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 🤔
  54. 54. 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(); }); });
  55. 55. 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 🤔
  56. 56. Hello @netbasal/spectator! 😎 AWESOME library for component testing in Angular ➔ Simple API ➔ Better typings ➔ Custom matchers ➔ Mocking integration ➔ Simple way of querying
  57. 57. 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'); }); });
  58. 58. Example: p1-keyboard
  59. 59. 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'); });
  60. 60. 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'); });
  61. 61. 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'); });
  62. 62. 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'); });
  63. 63. 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'); });
  64. 64. 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'); });
  65. 65. 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'); });
  66. 66. 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'); });
  67. 67. 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'); });
  68. 68. 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
  69. 69. To mock... or not to mock? Pure unit tests are run in isolation. ➔ Want to mock components/directives/pipes? Hello ng-mocks!
  70. 70. 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 } ] });
  71. 71. 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
  72. 72. 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
  73. 73. 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)
  74. 74. 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
  75. 75. Wrapping up...
  76. 76. 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!
  77. 77. 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!
  78. 78. 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.
  79. 79. 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.
  80. 80. Thanks! 😁

×