Slide 1

Slide 1 text

Model-View-Presenter with Angular

Slide 2

Slide 2 text

LARS GYRUP BRINK NIELSEN Proud father of twin girls 35-year-old native Dane inDepth.dev Writer, Tech Speaker, Podcast Host, OSS Contributor, Microsoft MVP Frontend Architect at Systemate B.Sc. in Computer Science MODEL-VIEW-PRESENTER WITH ANGULAR

Slide 3

Slide 3 text

OVERVIEW MODEL-VIEW-PRESENTER WITH ANGULAR

Slide 4

Slide 4 text

MODEL-VIEW-PRESENTER The model is the application state that is displayed to the user. The view is a thin user interface that presents the application state and translates user interactions to component- specific events, often delegating to the presenter. The presenter deals with complex presentational and behavioural logic. MODEL-VIEW-PRESENTER WITH ANGULAR

Slide 5

Slide 5 text

MODEL-VIEW-PRESENTER WITH ANGULAR HORIZONTAL LAYERS Presentation Business Logic State Management Persistence

Slide 6

Slide 6 text

SEPARATION OF CONCERNS Business logic Application-specific logic, domain logic, validation rules Persistence WebStorage, IndexedDB, WebSQL, HTTP, WebSocket, GraphQL, Firebase, Meteor Messaging WebRTC, WebSocket, Push API Server-Sent Events I/O Web Bluetooth, WebUSB, NFC, camera, microphone, proximity sensor, ambient light sensor Presentation DOM manipulation, event listeners, formatting User interaction UI behaviour, form validation State management Application state management, application-specific events MODEL-VIEW-PRESENTER WITH ANGULAR

Slide 7

Slide 7 text

BENEFITS The end result is an app that is highly: • Maintainable • Testable • Scalable • Performant MODEL-VIEW-PRESENTER WITH ANGULAR

Slide 8

Slide 8 text

MODEL-VIEW-PRESENTER WITH ANGULAR Container Component Presentational Component Presenter Integrates with non-presentational application layers Pure presentational, interactive view Thin component model Simple component template Complex presentational logic

Slide 9

Slide 9 text

MODEL-VIEW-PRESENTER WITH ANGULAR UNIDIRECTIONAL DATA FLOW https://bit.do/mvp-animations

Slide 10

Slide 10 text

MODEL-VIEW-PRESENTER WITH ANGULAR UNIDIRECTIONAL DATA FLOW https://bit.do/mvp-animations

Slide 11

Slide 11 text

TOUR OF HEROES – MVP STYLE Repository https://github.com/LayZeeDK/ngx-tour-of-heroes-mvp Short URL https://bit.do/toh-mvp MODEL-VIEW-PRESENTER WITH ANGULAR

Slide 12

Slide 12 text

CONTAINER COMPONENTS MODEL-VIEW-PRESENTER WITH ANGULAR

Slide 13

Slide 13 text

CONTAINER COMPONENTS MODEL-VIEW-PRESENTER WITH ANGULAR class DashboardComponent implements OnInit { heroes: Hero[] = []; constructor(private heroService: HeroService) { } ngOnInit() { this.getHeroes(); } private getHeroes(): void { this.heroService.getHeroes().subscribe(heroes => this.heroes = heroes.slice(1, 5)); } }

Slide 14

Slide 14 text

CONTAINER COMPONENTS Isolate and extract integration with non- presentational layers into a container component. MODEL-VIEW-PRESENTER WITH ANGULAR class DashboardContainerComponent { } // class DashboardComponent implements OnInit { heroes: Hero[] = []; constructor(private heroService: HeroService) { } ngOnInit() { this.getHeroes(); } private getHeroes(): void { this.heroService.getHeroes().subscribe(heroes => this.heroes = heroes.slice(1, 5)); } }

Slide 15

Slide 15 text

CONTAINER COMPONENTS Isolate and extract integration with non- presentational layers into a container component. MODEL-VIEW-PRESENTER WITH ANGULAR class DashboardContainerComponent { topHeroes$: Observable = this.heroService // .getHeroes().pipe(// map(heroes => heroes.slice(1, 5)), // ); constructor(private heroService: HeroService) { } // } class DashboardComponent implements OnInit { heroes: Hero[] = []; constructor(private heroService: HeroService) { } // ✂️ ngOnInit() { this.getHeroes(); } private getHeroes(): void { this.heroService.getHeroes().subscribe(heroes => this.heroes = heroes.slice(1, 5)); } }

Slide 16

Slide 16 text

CONTAINER COMPONENTS Isolate and extract integration with non- presentational layers into a container component. The container component model streams data from the application state through observables. MODEL-VIEW-PRESENTER WITH ANGULAR class DashboardContainerComponent { topHeroes$: Observable = this.heroService .getHeroes().pipe( map(heroes => heroes.slice(1, 5)), ); constructor(private heroService: HeroService) { } } class DashboardComponent { heroes: Hero[] = []; }

Slide 17

Slide 17 text

CONTAINER COMPONENTS Isolate and extract integration with non- presentational layers into a container component. The container component model streams data from the application state through observables. Connect the container component to the presentational component with data bindings. MODEL-VIEW-PRESENTER WITH ANGULAR class DashboardContainerComponent { topHeroes$: Observable = this.heroService .getHeroes().pipe( map(heroes => heroes.slice(1, 5)), ); constructor(private heroService: HeroService) { } } class DashboardComponent { heroes: Hero[] = []; }

Slide 18

Slide 18 text

CONTAINER COMPONENTS Isolate and extract integration with non- presentational layers into a container component. The container component model streams data from the application state through observables. Connect the container component to the presentational component with data bindings. MODEL-VIEW-PRESENTER WITH ANGULAR class DashboardContainerComponent { topHeroes$: Observable = this.heroService .getHeroes().pipe( map(heroes => heroes.slice(1, 5)), ); constructor(private heroService: HeroService) { } } class DashboardComponent { heroes: Hero[] = []; }

Slide 19

Slide 19 text

PRESENTATIONAL COMPONENTS MODEL-VIEW-PRESENTER WITH ANGULAR

Slide 20

Slide 20 text

PRESENTATIONAL COMPONENTS MODEL-VIEW-PRESENTER WITH ANGULAR @Component({ selector: 'app-dashboard', // (…) }) class DashboardComponent { heroes: Hero[]; }

Slide 21

Slide 21 text

PRESENTATIONAL COMPONENTS MODEL-VIEW-PRESENTER WITH ANGULAR @Component({ selector: 'app-dashboard-ui', // // (…) }) class DashboardComponent { heroes: Hero[]; }

Slide 22

Slide 22 text

PRESENTATIONAL COMPONENTS Declare the Presentational Component’s data binding API. MODEL-VIEW-PRESENTER WITH ANGULAR @Component({ selector: 'app-dashboard-ui', // (…) }) class DashboardComponent { @Input() heroes: Hero[]; // @Input() title: string; // }

Slide 23

Slide 23 text

PRESENTATIONAL COMPONENTS Declare the Presentational Component’s data binding API. Use minimal presentational logic in the component template and component model. MODEL-VIEW-PRESENTER WITH ANGULAR

{{title}}

@Component({ selector: 'app-dashboard-ui', // (…) }) class DashboardComponent { @Input() heroes: Hero[]; @Input() title: string; }

Slide 24

Slide 24 text

PRESENTATIONAL COMPONENTS Declare the Presentational Component’s data binding API. Use minimal presentational logic in the component template and component model. Apply the OnPush change detection strategy. MODEL-VIEW-PRESENTER WITH ANGULAR

{{title}}

@Component({ changeDetection: ChangeDetectionStrategy.OnPush, // selector: 'app-dashboard-ui', // (…) }) class DashboardComponent { @Input() heroes: Hero[]; @Input() title: string; }

Slide 25

Slide 25 text

ADVANCED EXAMPLE MODEL-VIEW-PRESENTER WITH ANGULAR

Slide 26

Slide 26 text

ADVANCED EXAMPLE MODEL-VIEW-PRESENTER WITH ANGULAR class HeroesComponent implements OnInit { heroes: Hero[]; constructor(private heroService: HeroService) { } ngOnInit() { this.getHeroes(); } onAdd(name: string): void { name = name.trim(); if (!name) { return; } this.heroService.addHero({ name } as Hero) .subscribe(hero => { this.heroes.push(hero); }); } onDelete(hero: Hero): void { this.heroes = this.heroes.filter(h => h !== hero); this.heroService.deleteHero(hero).subscribe(); } private getHeroes(): void { this.heroService.getHeroes() .subscribe(heroes => this.heroes = heroes); } }

Slide 27

Slide 27 text

ADVANCED EXAMPLE Isolate and extract integration with non- presentational layers into a container component. MODEL-VIEW-PRESENTER WITH ANGULAR class HeroesContainerComponent { } // class HeroesComponent implements OnInit { heroes: Hero[]; constructor(private heroService: HeroService) { } ngOnInit() { this.getHeroes(); } onAdd(name: string): void { name = name.trim(); if (!name) { return; } this.heroService.addHero({ name } as Hero) .subscribe(hero => { this.heroes.push(hero); }); } onDelete(hero: Hero): void { this.heroes = this.heroes.filter(h => h !== hero); this.heroService.deleteHero(hero).subscribe(); } private getHeroes(): void { this.heroService.getHeroes() .subscribe(heroes => this.heroes = heroes); } }

Slide 28

Slide 28 text

ADVANCED EXAMPLE Isolate and extract integration with non- presentational layers into a container component. MODEL-VIEW-PRESENTER WITH ANGULAR class HeroesContainerComponent { heroes$: Observable = multiScan([]); // } class HeroesComponent implements OnInit { heroes: Hero[]; constructor(private heroService: HeroService) { } ngOnInit() { this.getHeroes(); } onAdd(name: string): void { name = name.trim(); if (!name) { return; } this.heroService.addHero({ name } as Hero) .subscribe(hero => { this.heroes.push(hero); }); } onDelete(hero: Hero): void { this.heroes = this.heroes.filter(h => h !== hero); this.heroService.deleteHero(hero).subscribe(); } private getHeroes(): void { this.heroService.getHeroes() .subscribe(heroes => this.heroes = heroes); } }

Slide 29

Slide 29 text

ADVANCED EXAMPLE Isolate and extract integration with non- presentational layers into a container component. MODEL-VIEW-PRESENTER WITH ANGULAR class HeroesContainerComponent { heroes$: Observable = multiScan( this.heroService.getHeroes(), // (heroes, loadedHeroes) => [...heroes, ...loadedHeroes], // []); constructor(private heroService: HeroService) { } // } class HeroesComponent implements OnInit { heroes: Hero[]; constructor(private heroService: HeroService) { } // ngOnInit() { this.getHeroes(); } // (…) private getHeroes(): void { this.heroService.getHeroes() .subscribe(heroes => this.heroes = heroes); } }

Slide 30

Slide 30 text

ADVANCED EXAMPLE Isolate and extract integration with non- presentational layers into a container component. MODEL-VIEW-PRESENTER WITH ANGULAR class HeroesContainerComponent { heroes$: Observable = multiScan( this.heroService.getHeroes(), (heroes, loadedHeroes) => [...heroes, ...loadedHeroes], []); constructor(private heroService: HeroService) { } } class HeroesComponent { heroes: Hero[]; constructor(private heroService: HeroService) { } onAdd(name: string): void { name = name.trim(); if (!name) { return; } this.heroService.addHero({ name } as Hero) .subscribe(hero => { this.heroes.push(hero); }); } onDelete(hero: Hero): void { this.heroes = this.heroes.filter(h => h !== hero); this.heroService.deleteHero(hero).subscribe(); } }

Slide 31

Slide 31 text

ADVANCED EXAMPLE Isolate and extract integration with non- presentational layers into a container component. MODEL-VIEW-PRESENTER WITH ANGULAR class HeroesContainerComponent { private heroAdded = new Subject(); // heroes$: Observable = multiScan( this.heroService.getHeroes(), (heroes, loadedHeroes) => [...heroes, ...loadedHeroes], this.heroAdded, // (heroes, hero) => [...heroes, hero], // []); constructor(private heroService: HeroService) { } } class HeroesComponent { heroes: Hero[]; constructor(private heroService: HeroService) { } onAdd(name: string): void { name = name.trim(); if (!name) { return; } this.heroService.addHero({ name } as Hero) .subscribe(hero => { this.heroes.push(hero); }); } // (…) }

Slide 32

Slide 32 text

ADVANCED EXAMPLE Isolate and extract integration with non- presentational layers into a container component. MODEL-VIEW-PRESENTER WITH ANGULAR class HeroesContainerComponent { private heroAdded = new Subject(); heroes$: Observable = multiScan( // (…) this.heroAdded, (heroes, hero) => [...heroes, hero], []); constructor(private heroService: HeroService) { } onAdd(name: string): void { // this.heroService.addHero({ name } as Hero).subscribe({ // next: hero => this.heroAdded.next(hero), // error: noop, // }); } } class HeroesComponent { heroes: Hero[]; constructor(private heroService: HeroService) { } onAdd(name: string): void { name = name.trim(); if (!name) { return; } this.heroService.addHero({ name } as Hero) .subscribe(hero => { this.heroes.push(hero); }); } // (…) }

Slide 33

Slide 33 text

ADVANCED EXAMPLE Isolate and extract integration with non- presentational layers into a container component. MODEL-VIEW-PRESENTER WITH ANGULAR class HeroesContainerComponent { private heroAdded = new Subject(); heroes$: Observable = multiScan( // (…) this.heroAdded, (heroes, hero) => [...heroes, hero], []); constructor(private heroService: HeroService) { } onAdd(name: string): void { this.heroService.addHero({ name } as Hero).subscribe({ next: hero => this.heroAdded.next(hero), error: noop, }); } } class HeroesComponent { heroes: Hero[]; constructor(private heroService: HeroService) { } onAdd(name: string): void { name = name.trim(); if (!name) { return; } } // (…) }

Slide 34

Slide 34 text

ADVANCED EXAMPLE Isolate and extract integration with non- presentational layers into a container component. MODEL-VIEW-PRESENTER WITH ANGULAR class HeroesContainerComponent { private heroAdded = new Subject(); private heroRemoved = new Subject(); // heroes$: Observable = multiScan( // (…) this.heroRemoved, // (heroes, hero) => heroes.filter(h => h !== hero), // []); constructor(private heroService: HeroService) { } // (…) } class HeroesComponent { heroes: Hero[]; constructor(private heroService: HeroService) { } // (…) onDelete(hero: Hero): void { this.heroes = this.heroes.filter(h => h !== hero); this.heroService.deleteHero(hero).subscribe(); } }

Slide 35

Slide 35 text

ADVANCED EXAMPLE Isolate and extract integration with non- presentational layers into a container component. MODEL-VIEW-PRESENTER WITH ANGULAR class HeroesContainerComponent { private heroAdded = new Subject(); private heroRemoved = new Subject(); heroes$: Observable = multiScan( // (…) this.heroRemoved, (heroes, hero) => heroes.filter(h => h !== hero), []); constructor(private heroService: HeroService) { } // (…) onDelete(hero: Hero): void { // this.heroRemove.next(hero); // this.heroService.deleteHero(hero).subscribe({ // error: () => this.heroAdd.next(hero), // }); } } class HeroesComponent { heroes: Hero[]; constructor(private heroService: HeroService) { } // (…) onDelete(hero: Hero): void { this.heroes = this.heroes.filter(h => h !== hero); this.heroService.deleteHero(hero).subscribe(); } }

Slide 36

Slide 36 text

ADVANCED EXAMPLE Isolate and extract integration with non- presentational layers into a container component. MODEL-VIEW-PRESENTER WITH ANGULAR class HeroesContainerComponent { private heroAdded = new Subject(); private heroRemoved = new Subject(); heroes$: Observable = multiScan( // (…) this.heroRemoved, (heroes, hero) => heroes.filter(h => h !== hero), []); constructor(private heroService: HeroService) { } // (…) onDelete(hero: Hero): void { this.heroRemove.next(hero); this.heroService.deleteHero(hero).subscribe({ error: () => this.heroAdd.next(hero), }); } } class HeroesComponent { heroes: Hero[]; onAdd(name: string): void { name = name.trim(); if (!name) { return; } } }

Slide 37

Slide 37 text

ADVANCED EXAMPLE Isolate and extract integration with non- presentational layers into a container component. MODEL-VIEW-PRESENTER WITH ANGULAR class HeroesContainerComponent { private heroAdded = new Subject(); private heroRemoved = new Subject(); heroes$: Observable = multiScan( this.heroService.getHeroes(), (heroes, loadedHeroes) => [...heroes, ...loadedHeroes], this.heroAdded, (heroes, hero) => [...heroes, hero], this.heroRemoved, (heroes, hero) => heroes.filter(h => h !== hero), []); constructor(private heroService: HeroService) { } onAdd(name: string): void { this.heroService.addHero({ name } as Hero).subscribe({ next: hero => this.heroAdded.next(hero), error: noop, }); } onDelete(hero: Hero): void { this.heroRemove.next(hero); this.heroService.deleteHero(hero).subscribe({ error: () => this.heroAdd.next(hero), }); } }

Slide 38

Slide 38 text

ADVANCED EXAMPLE Isolate and extract integration with non- presentational layers into a container component. Declare the presentational component’s data binding API. MODEL-VIEW-PRESENTER WITH ANGULAR @Component({ selector: 'app-heroes-ui', // (...) }) class HeroesComponent { heroes: Hero[]; onAdd(name: string): void { name = name.trim(); if (!name) { return; } } }

Slide 39

Slide 39 text

ADVANCED EXAMPLE Isolate and extract integration with non- presentational layers into a container component. Declare the presentational component’s data binding API. MODEL-VIEW-PRESENTER WITH ANGULAR @Component({ selector: 'app-heroes-ui', // (...) }) class HeroesComponent { @Input() heroes: Hero[]; // @Input() title: string; // onAdd(name: string): void { name = name.trim(); if (!name) { return; } } }

Slide 40

Slide 40 text

ADVANCED EXAMPLE Isolate and extract integration with non- presentational layers into a container component. Declare the presentational component’s data binding API. MODEL-VIEW-PRESENTER WITH ANGULAR @Component({ selector: 'app-heroes-ui', // (...) }) class HeroesComponent { @Input() heroes: Hero[]; @Input() title: string; @Output() add = new EventEmitter(); // @Output() remove = new EventEmitter(); // onAdd(name: string): void { name = name.trim(); if (!name) { return; } this.add.emit(name); // } }

Slide 41

Slide 41 text

ADVANCED EXAMPLE Isolate and extract integration with non- presentational layers into a container component. Declare the presentational component’s data binding API. Use minimal presentational logic in the component template and component model. MODEL-VIEW-PRESENTER WITH ANGULAR @Component({ selector: 'app-heroes-ui', // (...) }) class HeroesComponent { @Input() heroes: Hero[]; @Input() title: string; @Output() add = new EventEmitter(); @Output() remove = new EventEmitter(); onAdd(name: string): void { name = name.trim(); if (!name) { return; } this.add.emit(name); } } Hero name: add

Slide 42

Slide 42 text

ADVANCED EXAMPLE Isolate and extract integration with non- presentational layers into a container component. Declare the presentational component’s data binding API. Use minimal presentational logic in the component template and component model. MODEL-VIEW-PRESENTER WITH ANGULAR @Component({ selector: 'app-heroes-ui', // (...) }) class HeroesComponent { @Input() heroes: Hero[]; @Input() title: string; @Output() add = new EventEmitter(); @Output() remove = new EventEmitter(); onAdd(name: string): void { name = name.trim(); if (!name) { return; } this.add.emit(name); } }
  • x

Slide 43

Slide 43 text

ADVANCED EXAMPLE Isolate and extract integration with non- presentational layers into a container component. Declare the presentational component’s data binding API. Use minimal presentational logic in the component template and component model. Apply the OnPush change detection strategy. MODEL-VIEW-PRESENTER WITH ANGULAR @Component({ changeDetection: ChangeDetectionStrategy.OnPush, // selector: 'app-heroes-ui', // (...) }) class HeroesComponent { @Input() heroes: Hero[]; @Input() title: string; @Output() add = new EventEmitter(); @Output() remove = new EventEmitter(); onHero(name: string): void { name = name.trim(); if (!name) { return; } this.add.emit(name); } }
  • x

Slide 44

Slide 44 text

PRESENTERS MODEL-VIEW-PRESENTER WITH ANGULAR

Slide 45

Slide 45 text

class HeroesComponent { @Input() heroes: Hero[]; @Input() title: string; @Output() add = new EventEmitter(); @Output() remove = new EventEmitter(); onAdd(name: string): void { name = name.trim(); if (!name) { return; } this.add.emit(name); } } PRESENTERS MODEL-VIEW-PRESENTER WITH ANGULAR

Slide 46

Slide 46 text

class HeroesPresenter { } // class HeroesComponent { // (…) @Output() add = new EventEmitter(); onAdd(name: string): void { name = name.trim(); if (!name) { return; } this.add.emit(name); } } PRESENTERS Extract complex presentational logic into a presenter. MODEL-VIEW-PRESENTER WITH ANGULAR

Slide 47

Slide 47 text

PRESENTERS Extract complex presentational logic into a presenter. MODEL-VIEW-PRESENTER WITH ANGULAR class HeroesPresenter { private add = new Subject(); // add$: Observable = this.add.asObservable(); // } class HeroesComponent { // (…) @Output() add = new EventEmitter(); onAdd(name: string): void { name = name.trim(); if (!name) { return; } this.add.emit(name); } }

Slide 48

Slide 48 text

PRESENTERS Extract complex presentational logic into a presenter. MODEL-VIEW-PRESENTER WITH ANGULAR class HeroesPresenter { private add = new Subject(); add$: Observable = this.add.asObservable(); onAdd(name: string): void {// name = name.trim(); // if (!name) { return; } // this.add.next(name); // } } class HeroesComponent { // (…) @Output() add = new EventEmitter(); onAdd(name: string): void { // name = name.trim(); // ✂️ if (!name) { return; } // ✂️ this.add.emit(name); // ✂️ } }

Slide 49

Slide 49 text

PRESENTERS Extract complex presentational logic into a presenter. MODEL-VIEW-PRESENTER WITH ANGULAR class HeroesPresenter { private add = new Subject(); add$: Observable = this.add.asObservable(); onAdd(name: string): void { name = name.trim(); if (!name) { return; } this.add.next(name); } } class HeroesComponent { // (…) @Output() add = new EventEmitter(); onAdd(name: string): void { } }

Slide 50

Slide 50 text

@Component({ // (...) }) class HeroesComponent { @Input() heroes: Hero[]; @Input() title: string; @Output() add = new EventEmitter(); @Output() remove = new EventEmitter(); onAdd(name: string): void { } } PRESENTERS Extract complex presentational logic into a presenter. Inject the presenter into the presentational component. MODEL-VIEW-PRESENTER WITH ANGULAR

Slide 51

Slide 51 text

@Component({ providers: [HeroesPresenter], // // (...) }) class HeroesComponent { @Input() heroes: Hero[]; @Input() title: string; @Output() add = new EventEmitter(); @Output() remove = new EventEmitter(); constructor(private presenter: HeroesPresenter) { } // onAdd(name: string): void { } } PRESENTERS Extract complex presentational logic into a presenter. Inject the presenter into the presentational component. MODEL-VIEW-PRESENTER WITH ANGULAR

Slide 52

Slide 52 text

class HeroesComponent { @Input() heroes: Hero[]; @Input() title: string; @Output() add = new EventEmitter(); @Output() remove = new EventEmitter(); constructor(private presenter: HeroesPresenter) { } onAdd(name: string): void { } } PRESENTERS Extract complex presentational logic into a presenter. Inject the presenter into the presentational component. Connect the presenter to the presentational component’s data binding API. MODEL-VIEW-PRESENTER WITH ANGULAR

Slide 53

Slide 53 text

class HeroesComponent implements OnInit { // @Input() heroes: Hero[]; @Input() title: string; @Output() add = new EventEmitter(); @Output() remove = new EventEmitter(); constructor(private presenter: HeroesPresenter) { } ngOnInit(): void { // this.presenter.add$ // .subscribe(name => this.add.emit(name)); // } onAdd(name: string): void { } } PRESENTERS Extract complex presentational logic into a presenter. Inject the presenter into the presentational component. Connect the presenter to the presentational component’s data binding API. MODEL-VIEW-PRESENTER WITH ANGULAR

Slide 54

Slide 54 text

class HeroesComponent implements OnInit { @Input() heroes: Hero[]; @Input() title: string; @Output() add = new EventEmitter(); @Output() remove = new EventEmitter(); constructor(private presenter: HeroesPresenter) { } ngOnInit(): void { this.presenter.add$ .subscribe(name => this.add.emit(name)); } onAdd(name: string): void { this.presenter.onAdd(name); // } } PRESENTERS Extract complex presentational logic into a presenter. Inject the presenter into the presentational component. Connect the presenter to the presentational component’s data binding API. MODEL-VIEW-PRESENTER WITH ANGULAR

Slide 55

Slide 55 text

class HeroesComponent implements OnDestroy, OnInit { // // (...) @Output() add = new EventEmitter(); // (…) private destroy: Subject = new Subject(); // constructor(private presenter: HeroesPresenter) { } ngOnInit(): void { this.presenter.add$.pipe( // takeUntil(this.destroy), // ).subscribe(name => this.add.emit(name)); } ngOnDestroy(): void {// this.destroy.next(); // this.destroy.complete(); // } // (…) } PRESENTERS Extract complex presentational logic into a presenter. Inject the presenter into the presentational component. Connect the presenter to the presentational component’s data binding API. Manage subscriptions. MODEL-VIEW-PRESENTER WITH ANGULAR

Slide 56

Slide 56 text

TESTING MODEL-VIEW-PRESENTER WITH ANGULAR

Slide 57

Slide 57 text

UNIT TESTING Most presentational components and their templates are hardly worth testing. Presenters are naturally isolated as they are usually free of injected dependencies. Container components are relatively easy to isolate and their templates are never worth testing. Rarely a need to use Angular testing modules (TestBed). MODEL-VIEW-PRESENTER WITH ANGULAR

Slide 58

Slide 58 text

PRESENTER INJECTION MODEL-VIEW-PRESENTER WITH ANGULAR

Slide 59

Slide 59 text

PRESENTER INJECTION MODEL-VIEW-PRESENTER WITH ANGULAR @Component({ providers: [HeroesPresenter], // (...) }) class HeroesComponent { @Input() heroes: Hero[]; @Input() title: string; @Output() add = new EventEmitter(); @Output() remove = new EventEmitter(); constructor( private presenter: HeroesPresenter) { } }

Slide 60

Slide 60 text

PRESENTER INJECTION Prevent the presenter provider from leaking into content child components. MODEL-VIEW-PRESENTER WITH ANGULAR @Component({ viewProviders: [HeroesPresenter], // // (...) }) class HeroesComponent { @Input() heroes: Hero[]; @Input() title: string; @Output() add = new EventEmitter(); @Output() remove = new EventEmitter(); constructor( private presenter: HeroesPresenter) { } }

Slide 61

Slide 61 text

PRESENTER INJECTION Prevent the presenter provider from leaking into content child components. Be explicit about the injector in the presentational component. MODEL-VIEW-PRESENTER WITH ANGULAR @Component({ viewProviders: [HeroesPresenter], // (...) }) class HeroesComponent { @Input() heroes: Hero[]; @Input() title: string; @Output() add = new EventEmitter(); @Output() remove = new EventEmitter(); constructor( @Self() private presenter: HeroesPresenter) { } // }

Slide 62

Slide 62 text

COMPONENT TAX CUTS MODEL-VIEW-PRESENTER WITH ANGULAR

Slide 63

Slide 63 text

CONTAINER DIRECTIVES MODEL-VIEW-PRESENTER WITH ANGULAR

Slide 64

Slide 64 text

CONTAINER DIRECTIVES Extract container component’s UI properties and event handlers to container directive. MODEL-VIEW-PRESENTER WITH ANGULAR @Directive({ selector: '[appHeroSearch]' }) // class HeroSearchDirective { } // class HeroSearchContainerComponent { private searchTerms = new Subject(); heroes$: Observable = this.searchTerms.pipe( switchMap(term => this.heroService.searchHeroes(term))); constructor(private heroService: HeroService) { } onSearch(term: string): void { this.searchTerms.next(term); } }

Slide 65

Slide 65 text

CONTAINER DIRECTIVES Extract container component’s UI properties and event handlers to container directive. MODEL-VIEW-PRESENTER WITH ANGULAR @Directive({ selector: '[appHeroSearch]' }) class HeroSearchDirective { private searchTerms = new Subject(); // onSearch(term: string): void { // this.searchTerms.next(term); // } } class HeroSearchContainerComponent { private searchTerms = new Subject(); // ✂️ heroes$: Observable = this.searchTerms.pipe( switchMap(term => this.heroService.searchHeroes(term))); constructor(private heroService: HeroService) { } onSearch(term: string): void { // ✂️ this.searchTerms.next(term); // ✂️ } }

Slide 66

Slide 66 text

CONTAINER DIRECTIVES Extract container component’s UI properties and event handlers to container directive. MODEL-VIEW-PRESENTER WITH ANGULAR @Directive({ selector: '[appHeroSearch]' }) class HeroSearchDirective { private searchTerms = new Subject(); onSearch(term: string): void { this.searchTerms.next(term); } } class HeroSearchContainerComponent { heroes$: Observable = this.searchTerms.pipe( switchMap(term => this.heroService.searchHeroes(term))); constructor(private heroService: HeroService) { } }

Slide 67

Slide 67 text

CONTAINER DIRECTIVES Extract container component’s UI properties and event handlers to container directive. MODEL-VIEW-PRESENTER WITH ANGULAR @Directive({ selector: '[appHeroSearch]' }) class HeroSearchDirective { private searchTerms = new Subject(); heroes$: Observable = this.searchTerms.pipe( // switchMap(term => this.heroService.searchHeroes(term))); // constructor(private heroService: HeroService) { } // onSearch(term: string): void { this.searchTerms.next(term); } } class HeroSearchContainerComponent { heroes$: Observable = this.searchTerms.pipe( // ✂️ switchMap(term => this.heroService.searchHeroes(term))); // ✂️ constructor(private heroService: HeroService) { } // ✂️ }

Slide 68

Slide 68 text

CONTAINER DIRECTIVES Extract container component’s UI properties and event handlers to container directive. MODEL-VIEW-PRESENTER WITH ANGULAR @Directive({ selector: '[appHeroSearch]' }) class HeroSearchDirective { private searchTerms = new Subject(); heroes$: Observable = this.searchTerms.pipe( switchMap(term => this.heroService.searchHeroes(term))); constructor(private heroService: HeroService) { } onSearch(term: string): void { this.searchTerms.next(term); } } class HeroSearchContainerComponent { }

Slide 69

Slide 69 text

CONTAINER DIRECTIVES Initialize presentational component properties. MODEL-VIEW-PRESENTER WITH ANGULAR @Directive({ selector: '[appHeroSearch]' }) class HeroSearchDirective { private searchTerms = new Subject(); heroes$: Observable = this.searchTerms.pipe( switchMap(term => this.heroService.searchHeroes(term))); constructor(private heroService: HeroService) { } onSearch(term: string): void { this.searchTerms.next(term); } }

Slide 70

Slide 70 text

CONTAINER DIRECTIVES Initialize presentational component properties. MODEL-VIEW-PRESENTER WITH ANGULAR @Directive({ selector: '[appHeroSearch]' }) class HeroSearchDirective implements OnInit {// private searchTerms = new Subject(); heroes$: Observable = this.searchTerms.pipe( switchMap(term => this.heroService.searchHeroes(term))); constructor(private heroService: HeroService, @Host() private component: HeroSearchComponent) { } // ngOnInit(): void { } // onSearch(term: string): void { this.searchTerms.next(term); } }

Slide 71

Slide 71 text

CONTAINER DIRECTIVES Initialize presentational component properties. MODEL-VIEW-PRESENTER WITH ANGULAR @Directive({ selector: '[appHeroSearch]' }) class HeroSearchDirective implements OnInit { private searchTerms = new Subject(); heroes$: Observable = this.searchTerms.pipe( switchMap(term => this.heroService.searchHeroes(term))); constructor(private heroService: HeroService, @Host() private component: HeroSearchComponent) { } ngOnInit(): void { this.component.title = 'Hero Search'; // } onSearch(term: string): void { this.searchTerms.next(term); } }

Slide 72

Slide 72 text

CONTAINER DIRECTIVES Initialize presentational component properties. Subscribe to presentational component events. MODEL-VIEW-PRESENTER WITH ANGULAR @Directive({ selector: '[appHeroSearch]' }) class HeroSearchDirective implements OnInit { private searchTerms = new Subject(); heroes$: Observable = this.searchTerms.pipe( switchMap(term => this.heroService.searchHeroes(term))); constructor(private heroService: HeroService, @Host() private component: HeroSearchComponent) { } ngOnInit(): void { this.component.title = 'Hero Search'; this.component.search // .subscribe(searchTerms => this.onSearch(searchTerms)); // } onSearch(term: string): void { this.searchTerms.next(term); } }

Slide 73

Slide 73 text

CONTAINER DIRECTIVES Initialize presentational component properties. Subscribe to presentational component events. Stream application state to presentational component properties. MODEL-VIEW-PRESENTER WITH ANGULAR @Directive({ selector: '[appHeroSearch]' }) class HeroSearchDirective implements OnInit { private searchTerms = new Subject(); heroes$: Observable = this.searchTerms.pipe( switchMap(term => this.heroService.searchHeroes(term))); constructor(private heroService: HeroService, @Host() private component: HeroSearchComponent) { } ngOnInit(): void { this.component.title = 'Hero Search'; this.component.search .subscribe(searchTerms => this.onSearch(searchTerms)); this.heroes$ // .subscribe(heroes => this.component.heroes = heroes); // } onSearch(term: string): void { this.searchTerms.next(term); } }

Slide 74

Slide 74 text

CONTAINER DIRECTIVES Initialize presentational component properties. Subscribe to presentational component events. Stream application state to presentational component properties. Manage application state subscriptions and presentational component event subscriptions. MODEL-VIEW-PRESENTER WITH ANGULAR class HeroSearchDirective implements OnInit { // (...) ngOnInit(): void { this.component.title = 'Hero Search'; this.component.search .subscribe(searchTerms => this.onSearch(searchTerms)); this.heroes$ .subscribe(heroes => this.component.heroes = heroes); } // (...) }

Slide 75

Slide 75 text

CONTAINER DIRECTIVES Initialize presentational component properties. Subscribe to presentational component events. Stream application state to presentational component properties. Manage application state subscriptions and presentational component event subscriptions. MODEL-VIEW-PRESENTER WITH ANGULAR class HeroSearchDirective implements OnDestroy, OnInit { // private destroy = new Subject(); // // (...) ngOnInit(): void { this.component.title = 'Hero Search'; this.component.search .subscribe(searchTerms => this.onSearch(searchTerms)); this.heroes$ .subscribe(heroes => this.component.heroes = heroes); } ngOnDestroy(): void { // this.destroy.next(); // this.destroy.complete(); // } // (...) }

Slide 76

Slide 76 text

CONTAINER DIRECTIVES Initialize presentational component properties. Subscribe to presentational component events. Stream application state to presentational component properties. Manage application state subscriptions and presentational component event subscriptions. MODEL-VIEW-PRESENTER WITH ANGULAR class HeroSearchDirective implements OnDestroy, OnInit { private destroy = new Subject(); // (...) ngOnInit(): void { this.component.title = 'Hero Search'; this.component.search.pipe( // takeUntil(this.destroy), // ).subscribe(searchTerms => this.onSearch(searchTerms)); this.heroes$.pipe( // takeUntil(this.destroy), // ).subscribe(heroes => this.component.heroes = heroes); } ngOnDestroy(): void { this.destroy.next(); this.destroy.complete(); } // (...) }

Slide 77

Slide 77 text

CONTAINER DIRECTIVES Initialize presentational component properties. Subscribe to presentational component events. Stream application state to presentational component properties. Manage application state subscriptions and presentational component event subscriptions. Add the container directive to the presentational component in its parent component template. MODEL-VIEW-PRESENTER WITH ANGULAR

Slide 78

Slide 78 text

CONTAINER DIRECTIVES Initialize presentational component properties. Subscribe to presentational component events. Stream application state to presentational component properties. Manage application state subscriptions and presentational component event subscriptions. Add the container directive to the presentational component in its parent component template. MODEL-VIEW-PRESENTER WITH ANGULAR

Slide 79

Slide 79 text

CONTAINER DIRECTIVES Before, in parent component template Before, in container component template After, in parent component template MODEL-VIEW-PRESENTER WITH ANGULAR

Slide 80

Slide 80 text

PROVIDER DIRECTIVES Isolate and extract integration with non- presentational layers into a provider directive. MODEL-VIEW-PRESENTER WITH ANGULAR @Directive({ // exportAs: 'heroSearchProvider', // selector: 'app-hero-search-ui', // }) class HeroSearchProviderDirective { } // class HeroSearchContainerComponent { private searchTerms = new Subject(); heroes$: Observable = this.searchTerms.pipe( switchMap(term => this.heroService.searchHeroes(term))); constructor(private heroService: HeroService) {} onSearch(term: string): void { this.searchTerms.next(term); } }

Slide 81

Slide 81 text

PROVIDER DIRECTIVES Isolate and extract integration with non- presentational layers into a provider directive. MODEL-VIEW-PRESENTER WITH ANGULAR @Directive({ exportAs: 'heroSearchProvider', selector: 'app-hero-search-ui', }) class HeroSearchProviderDirective { } class HeroSearchContainerComponent { private searchTerms = new Subject(); heroes$: Observable = this.searchTerms.pipe( switchMap(term => this.heroService.searchHeroes(term))); constructor(private heroService: HeroService) {} onSearch(term: string): void { this.searchTerms.next(term); } }

Slide 82

Slide 82 text

PROVIDER DIRECTIVES Isolate and extract integration with non- presentational layers into a provider directive. MODEL-VIEW-PRESENTER WITH ANGULAR @Directive({ exportAs: 'heroSearchProvider', selector: 'app-hero-search-ui', }) class HeroSearchProviderDirective { private searchTerms = new Subject(); // onSearch(term: string): void { // this.searchTerms.next(term); // } } class HeroSearchContainerComponent { private searchTerms = new Subject(); // ✂️ heroes$: Observable = this.searchTerms.pipe( switchMap(term => this.heroService.searchHeroes(term))); constructor(private heroService: HeroService) {} onSearch(term: string): void { // ✂️ this.searchTerms.next(term); // ✂️ } }

Slide 83

Slide 83 text

PROVIDER DIRECTIVES Isolate and extract integration with non- presentational layers into a provider directive. MODEL-VIEW-PRESENTER WITH ANGULAR @Directive({ exportAs: 'heroSearchProvider', selector: 'app-hero-search-ui', }) class HeroSearchProviderDirective { private searchTerms = new Subject(); onSearch(term: string): void { this.searchTerms.next(term); } } class HeroSearchContainerComponent { heroes$: Observable = this.searchTerms.pipe( switchMap(term => this.heroService.searchHeroes(term))); constructor(private heroService: HeroService) { } }

Slide 84

Slide 84 text

PROVIDER DIRECTIVES Isolate and extract integration with non- presentational layers into a provider directive. MODEL-VIEW-PRESENTER WITH ANGULAR @Directive({ exportAs: 'heroSearchProvider', selector: 'app-hero-search-ui', }) class HeroSearchProviderDirective { private searchTerms = new Subject(); heroes$: Observable = this.searchTerms.pipe( // switchMap(term => // this.heroService.searchHeroes(term))); // constructor(private heroService: HeroService) { } // onSearch(term: string): void { this.searchTerms.next(term); } } class HeroSearchContainerComponent { heroes$: Observable = this.searchTerms.pipe( // ✂️ switchMap(term => // ✂️ this.heroService.searchHeroes(term))); // ✂️ constructor(private heroService: HeroService) { } // ✂️ }

Slide 85

Slide 85 text

PROVIDER DIRECTIVES Isolate and extract integration with non- presentational layers into a provider directive. MODEL-VIEW-PRESENTER WITH ANGULAR @Directive({ exportAs: 'heroSearchProvider', selector: 'app-hero-search-ui', }) class HeroSearchProviderDirective { private searchTerms = new Subject(); heroes$: Observable = this.searchTerms.pipe( switchMap(term => this.heroService.searchHeroes(term))); constructor(private heroService: HeroService) { } onSearch(term: string): void { this.searchTerms.next(term); } } class HeroSearchContainerComponent { }

Slide 86

Slide 86 text

PROVIDER DIRECTIVES Isolate and extract integration with non- presentational layers into a provider directive. Add the provider directive to the presentational component in the parent component’s template. MODEL-VIEW-PRESENTER WITH ANGULAR @Directive({ exportAs: 'heroSearchProvider', selector: 'app-hero-search-ui', }) class HeroSearchProviderDirective { private searchTerms = new Subject(); heroes$: Observable = this.searchTerms.pipe( switchMap(term => this.heroService.searchHeroes(term))); constructor(private heroService: HeroService) { } onSearch(term: string): void { this.searchTerms.next(term); } }

Slide 87

Slide 87 text

PROVIDER DIRECTIVES Isolate and extract integration with non- presentational layers into a provider directive. Add the provider directive to the presentational component in the parent component’s template. Connect the provider directive to the presentational component’s data binding API. MODEL-VIEW-PRESENTER WITH ANGULAR @Directive({ exportAs: 'heroSearchProvider', selector: 'app-hero-search-ui', }) class HeroSearchProviderDirective { private searchTerms = new Subject(); heroes$: Observable = this.searchTerms.pipe( switchMap(term => this.heroService.searchHeroes(term))); constructor(private heroService: HeroService) { } onSearch(term: string): void { this.searchTerms.next(term); } }

Slide 88

Slide 88 text

PROVIDER DIRECTIVES Before, in parent component template Before, in container component template After, in parent component template MODEL-VIEW-PRESENTER WITH ANGULAR

Slide 89

Slide 89 text

CLOSING REMARKS MODEL-VIEW-PRESENTER WITH ANGULAR

Slide 90

Slide 90 text

SUMMARY Isolate and extract integration with non-presentational layers into a container component or provider directive. Extract complex presentational logic into a presenter. Manage subscriptions for connected observables. Unit test container components, provider directives and presenters. End-to-end test presentational components in the context of an entire application. MODEL-VIEW-PRESENTER WITH ANGULAR

Slide 91

Slide 91 text

ADVICE Don’t be dogmatic about this pattern. Mix’n’match and apply as needed. Container components and presenters can be shared between multiple presentational components. Think carefully about types of state. Use fake container components during development, for component tests and in style guides. Split work into visual, application, and integration. MODEL-VIEW-PRESENTER WITH ANGULAR

Slide 92

Slide 92 text

ATTRIBUTIONS Jason Bonta, Michael “chantastic” Chan and Dan Abramov for describing container components. Dan Abramov for teaching about provider components. Dave M. Bush for introducing the idea of Model-View-Presenter with Angular. Roy Peled for describing Model-View-Presenter for JavaScript. Justin ”Schwarty” Schwartzenberger for his ”Embrace Component Tranquility” talk about component taxes. MODEL-VIEW-PRESENTER WITH ANGULAR

Slide 93

Slide 93 text

GET IN TOUCH https://bit.do/mvp-slides https://bit.do/mvp-with-angular https://bit.do/toh-mvp https://github.com/LayZeeDK https://twitter.com/LayZeeDK https://www.linkedin.com/in/larsgbn/ [email protected] MODEL-VIEW-PRESENTER WITH ANGULAR