Model-View-Presenter with Angular

Model-View-Presenter with Angular

The Model-View-Presenter pattern increases separation of concerns, maintainability, testability and scalability in complex Angular apps by using container components, presentational components and presenters.

Presented at:
- Internet Week Denmark 2018
- ngAarhus Meetup, May 2018
- ngCopenhagen Meetup, November 2018
- Angular Online Meetup #8, September 2020

Recordings:
- ngAarhus May Meetup 2018: https://youtu.be/D_ytOCPQrI0
- Angular Online Meetup #8: https://youtu.be/C0yyvnrc0N0

Companion repository:
https://github.com/LayZeeDK/ngx-tour-of-heroes-mvp

Article:
https://indepth.dev/model-view-presenter-with-angular/

Transcript

  1. Model-View-Presenter with Angular

  2. 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
  3. OVERVIEW MODEL-VIEW-PRESENTER WITH ANGULAR

  4. 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
  5. MODEL-VIEW-PRESENTER WITH ANGULAR HORIZONTAL LAYERS Presentation Business Logic State Management

    Persistence
  6. 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
  7. BENEFITS The end result is an app that is highly:

    • Maintainable • Testable • Scalable • Performant MODEL-VIEW-PRESENTER WITH ANGULAR
  8. 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
  9. MODEL-VIEW-PRESENTER WITH ANGULAR UNIDIRECTIONAL DATA FLOW https://bit.do/mvp-animations

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

  11. 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
  12. CONTAINER COMPONENTS MODEL-VIEW-PRESENTER WITH ANGULAR

  13. 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)); } }
  14. 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)); } }
  15. CONTAINER COMPONENTS Isolate and extract integration with non- presentational layers

    into a container component. MODEL-VIEW-PRESENTER WITH ANGULAR class DashboardContainerComponent { topHeroes$: Observable<Hero[]> = 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)); } }
  16. 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<Hero[]> = this.heroService .getHeroes().pipe( map(heroes => heroes.slice(1, 5)), ); constructor(private heroService: HeroService) { } } class DashboardComponent { heroes: Hero[] = []; }
  17. 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<Hero[]> = this.heroService .getHeroes().pipe( map(heroes => heroes.slice(1, 5)), ); constructor(private heroService: HeroService) { } } class DashboardComponent { heroes: Hero[] = []; } <app-dashboard-ui></app-dashboard-ui>
  18. 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<Hero[]> = this.heroService .getHeroes().pipe( map(heroes => heroes.slice(1, 5)), ); constructor(private heroService: HeroService) { } } class DashboardComponent { heroes: Hero[] = []; } <app-dashboard-ui [heroes]="topHeroes$ | async" title="Top Heroes"></app-dashboard-ui>
  19. PRESENTATIONAL COMPONENTS MODEL-VIEW-PRESENTER WITH ANGULAR

  20. PRESENTATIONAL COMPONENTS MODEL-VIEW-PRESENTER WITH ANGULAR @Component({ selector: 'app-dashboard', // (…)

    }) class DashboardComponent { heroes: Hero[]; } <app-dashboard-ui [heroes]="topHeroes$ | async" title="Top Heroes"></app-dashboard-ui>
  21. PRESENTATIONAL COMPONENTS MODEL-VIEW-PRESENTER WITH ANGULAR @Component({ selector: 'app-dashboard-ui', // //

    (…) }) class DashboardComponent { heroes: Hero[]; } <app-dashboard-ui [heroes]="topHeroes$ | async" title="Top Heroes"></app-dashboard-ui>
  22. 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; // } <app-dashboard-ui [heroes]="topHeroes$ | async" title="Top Heroes"></app-dashboard-ui>
  23. 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 <h3>{{title}}</h3> <div class="grid grid-pad"> <a *ngFor="let hero of heroes" class="col-1-4" routerLink="/detail/{{hero.id}}"> <div class="module hero"> <h4>{{hero.name}}</h4> </div> </a> </div> <app-hero-search></app-hero-search> @Component({ selector: 'app-dashboard-ui', // (…) }) class DashboardComponent { @Input() heroes: Hero[]; @Input() title: string; }
  24. 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 <h3>{{title}}</h3> <div class="grid grid-pad"> <a *ngFor="let hero of heroes" class="col-1-4" routerLink="/detail/{{hero.id}}"> <div class="module hero"> <h4>{{hero.name}}</h4> </div> </a> </div> <app-hero-search></app-hero-search> @Component({ changeDetection: ChangeDetectionStrategy.OnPush, // selector: 'app-dashboard-ui', // (…) }) class DashboardComponent { @Input() heroes: Hero[]; @Input() title: string; }
  25. ADVANCED EXAMPLE MODEL-VIEW-PRESENTER WITH ANGULAR

  26. 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); } }
  27. 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); } }
  28. ADVANCED EXAMPLE Isolate and extract integration with non- presentational layers

    into a container component. MODEL-VIEW-PRESENTER WITH ANGULAR class HeroesContainerComponent { heroes$: Observable<Hero[]> = 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); } }
  29. ADVANCED EXAMPLE Isolate and extract integration with non- presentational layers

    into a container component. MODEL-VIEW-PRESENTER WITH ANGULAR class HeroesContainerComponent { heroes$: Observable<Hero[]> = 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); } }
  30. ADVANCED EXAMPLE Isolate and extract integration with non- presentational layers

    into a container component. MODEL-VIEW-PRESENTER WITH ANGULAR class HeroesContainerComponent { heroes$: Observable<Hero[]> = 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(); } }
  31. 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<Hero>(); // heroes$: Observable<Hero[]> = 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); }); } // (…) }
  32. 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<Hero>(); heroes$: Observable<Hero[]> = 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); }); } // (…) }
  33. 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<Hero>(); heroes$: Observable<Hero[]> = 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; } } // (…) }
  34. 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<Hero>(); private heroRemoved = new Subject<Hero>(); // heroes$: Observable<Hero[]> = 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(); } }
  35. 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<Hero>(); private heroRemoved = new Subject<Hero>(); heroes$: Observable<Hero[]> = 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(); } }
  36. 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<Hero>(); private heroRemoved = new Subject<Hero>(); heroes$: Observable<Hero[]> = 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; } } }
  37. 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<Hero>(); private heroRemoved = new Subject<Hero>(); heroes$: Observable<Hero[]> = 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), }); } }
  38. 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; } } } <app-heroes-ui [heroes]="heroes$ | async" title="My Heroes" (add)="onAdd($event)" (remove)="onDelete($event)"></app-heroes-ui>
  39. 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; } } } <app-heroes-ui [heroes]="heroes$ | async" title="My Heroes" (add)="onAdd($event)" (remove)="onDelete($event)"></app-heroes-ui>
  40. 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<string>(); // @Output() remove = new EventEmitter<Hero>(); // onAdd(name: string): void { name = name.trim(); if (!name) { return; } this.add.emit(name); // } } <app-heroes-ui [heroes]="heroes$ | async" title="My Heroes" (add)="onAdd($event)" (remove)="onDelete($event)"></app-heroes-ui>
  41. 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<string>(); @Output() remove = new EventEmitter<Hero>(); onAdd(name: string): void { name = name.trim(); if (!name) { return; } this.add.emit(name); } } <label>Hero name: <input #heroName /></label> <button (click)="onAdd(heroName.value); heroName.value='';"> add </button>
  42. 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<string>(); @Output() remove = new EventEmitter<Hero>(); onAdd(name: string): void { name = name.trim(); if (!name) { return; } this.add.emit(name); } } <ul class="heroes"><li *ngFor="let hero of heroes"> <a routerLink="/detail/{{hero.id}}"><!-- (...) --></a> <button class="delete" title="delete hero" (click)="remove.emit(hero)">x</button> </li></ul>
  43. 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<string>(); @Output() remove = new EventEmitter<Hero>(); onHero(name: string): void { name = name.trim(); if (!name) { return; } this.add.emit(name); } } <ul class="heroes"><li *ngFor="let hero of heroes"> <a routerLink="/detail/{{hero.id}}"><!-- (...) --></a> <button class="delete" title="delete hero" (click)="remove.emit(hero)">x</button> </li></ul>
  44. PRESENTERS MODEL-VIEW-PRESENTER WITH ANGULAR

  45. class HeroesComponent { @Input() heroes: Hero[]; @Input() title: string; @Output()

    add = new EventEmitter<string>(); @Output() remove = new EventEmitter<Hero>(); onAdd(name: string): void { name = name.trim(); if (!name) { return; } this.add.emit(name); } } PRESENTERS MODEL-VIEW-PRESENTER WITH ANGULAR
  46. class HeroesPresenter { } // class HeroesComponent { // (…)

    @Output() add = new EventEmitter<string>(); 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
  47. PRESENTERS Extract complex presentational logic into a presenter. MODEL-VIEW-PRESENTER WITH

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

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

    ANGULAR class HeroesPresenter { private add = new Subject<string>(); add$: Observable<string> = this.add.asObservable(); onAdd(name: string): void { name = name.trim(); if (!name) { return; } this.add.next(name); } } class HeroesComponent { // (…) @Output() add = new EventEmitter<string>(); onAdd(name: string): void { } }
  50. @Component({ // (...) }) class HeroesComponent { @Input() heroes: Hero[];

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

    @Input() heroes: Hero[]; @Input() title: string; @Output() add = new EventEmitter<string>(); @Output() remove = new EventEmitter<Hero>(); 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
  52. class HeroesComponent { @Input() heroes: Hero[]; @Input() title: string; @Output()

    add = new EventEmitter<string>(); @Output() remove = new EventEmitter<Hero>(); 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
  53. class HeroesComponent implements OnInit { // @Input() heroes: Hero[]; @Input()

    title: string; @Output() add = new EventEmitter<string>(); @Output() remove = new EventEmitter<Hero>(); 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
  54. class HeroesComponent implements OnInit { @Input() heroes: Hero[]; @Input() title:

    string; @Output() add = new EventEmitter<string>(); @Output() remove = new EventEmitter<Hero>(); 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
  55. class HeroesComponent implements OnDestroy, OnInit { // // (...) @Output()

    add = new EventEmitter<string>(); // (…) private destroy: Subject<void> = 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
  56. TESTING MODEL-VIEW-PRESENTER WITH ANGULAR

  57. 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
  58. PRESENTER INJECTION MODEL-VIEW-PRESENTER WITH ANGULAR

  59. PRESENTER INJECTION MODEL-VIEW-PRESENTER WITH ANGULAR @Component({ providers: [HeroesPresenter], // (...)

    }) class HeroesComponent { @Input() heroes: Hero[]; @Input() title: string; @Output() add = new EventEmitter<string>(); @Output() remove = new EventEmitter<Hero>(); constructor( private presenter: HeroesPresenter) { } }
  60. 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<string>(); @Output() remove = new EventEmitter<Hero>(); constructor( private presenter: HeroesPresenter) { } }
  61. 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<string>(); @Output() remove = new EventEmitter<Hero>(); constructor( @Self() private presenter: HeroesPresenter) { } // }
  62. COMPONENT TAX CUTS MODEL-VIEW-PRESENTER WITH ANGULAR

  63. CONTAINER DIRECTIVES MODEL-VIEW-PRESENTER WITH ANGULAR <app-hero-search-ui [heroes]="heroes$ | async" title="Hero

    Search" (search)="search($event)"></app-hero-search-ui> <app-hero-search> <app-hero-search-ui [heroes]="heroes$ | async" title="Hero Search" (search)="onSearch($event)"></app-hero-search-ui> </app-hero-search>
  64. 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<string>(); heroes$: Observable<Hero[]> = this.searchTerms.pipe( switchMap(term => this.heroService.searchHeroes(term))); constructor(private heroService: HeroService) { } onSearch(term: string): void { this.searchTerms.next(term); } }
  65. 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<string>(); // onSearch(term: string): void { // this.searchTerms.next(term); // } } class HeroSearchContainerComponent { private searchTerms = new Subject<string>(); // ✂️ heroes$: Observable<Hero[]> = this.searchTerms.pipe( switchMap(term => this.heroService.searchHeroes(term))); constructor(private heroService: HeroService) { } onSearch(term: string): void { // ✂️ this.searchTerms.next(term); // ✂️ } }
  66. 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<string>(); onSearch(term: string): void { this.searchTerms.next(term); } } class HeroSearchContainerComponent { heroes$: Observable<Hero[]> = this.searchTerms.pipe( switchMap(term => this.heroService.searchHeroes(term))); constructor(private heroService: HeroService) { } }
  67. 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<string>(); heroes$: Observable<Hero[]> = 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<Hero[]> = this.searchTerms.pipe( // ✂️ switchMap(term => this.heroService.searchHeroes(term))); // ✂️ constructor(private heroService: HeroService) { } // ✂️ }
  68. 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<string>(); heroes$: Observable<Hero[]> = this.searchTerms.pipe( switchMap(term => this.heroService.searchHeroes(term))); constructor(private heroService: HeroService) { } onSearch(term: string): void { this.searchTerms.next(term); } } class HeroSearchContainerComponent { }
  69. CONTAINER DIRECTIVES Initialize presentational component properties. MODEL-VIEW-PRESENTER WITH ANGULAR @Directive({

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

    selector: '[appHeroSearch]' }) class HeroSearchDirective implements OnInit {// private searchTerms = new Subject<string>(); heroes$: Observable<Hero[]> = 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); } }
  71. CONTAINER DIRECTIVES Initialize presentational component properties. MODEL-VIEW-PRESENTER WITH ANGULAR @Directive({

    selector: '[appHeroSearch]' }) class HeroSearchDirective implements OnInit { private searchTerms = new Subject<string>(); heroes$: Observable<Hero[]> = 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); } }
  72. 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<string>(); heroes$: Observable<Hero[]> = 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); } }
  73. 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<string>(); heroes$: Observable<Hero[]> = 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); } }
  74. 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); } // (...) }
  75. 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<void>(); // // (...) 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(); // } // (...) }
  76. 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<void>(); // (...) 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(); } // (...) }
  77. 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 <app-hero-search-ui></app-hero-search-ui>
  78. 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 <app-hero-search-ui appHeroSearch></app-hero-search-ui>
  79. <app-hero-search> <app-hero-search-ui [heroes]="heroes$ | async" title="Hero Search" (search)="onSearch($event)"></app-hero-search-ui> </app-hero-search> CONTAINER

    DIRECTIVES Before, in parent component template Before, in container component template After, in parent component template MODEL-VIEW-PRESENTER WITH ANGULAR <app-hero-search-ui appHeroSearch></app-hero-search-ui>
  80. 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<string>(); heroes$: Observable<Hero[]> = this.searchTerms.pipe( switchMap(term => this.heroService.searchHeroes(term))); constructor(private heroService: HeroService) {} onSearch(term: string): void { this.searchTerms.next(term); } }
  81. 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<string>(); heroes$: Observable<Hero[]> = this.searchTerms.pipe( switchMap(term => this.heroService.searchHeroes(term))); constructor(private heroService: HeroService) {} onSearch(term: string): void { this.searchTerms.next(term); } }
  82. 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<string>(); // onSearch(term: string): void { // this.searchTerms.next(term); // } } class HeroSearchContainerComponent { private searchTerms = new Subject<string>(); // ✂️ heroes$: Observable<Hero[]> = this.searchTerms.pipe( switchMap(term => this.heroService.searchHeroes(term))); constructor(private heroService: HeroService) {} onSearch(term: string): void { // ✂️ this.searchTerms.next(term); // ✂️ } }
  83. 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<string>(); onSearch(term: string): void { this.searchTerms.next(term); } } class HeroSearchContainerComponent { heroes$: Observable<Hero[]> = this.searchTerms.pipe( switchMap(term => this.heroService.searchHeroes(term))); constructor(private heroService: HeroService) { } }
  84. 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<string>(); heroes$: Observable<Hero[]> = 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<Hero[]> = this.searchTerms.pipe( // ✂️ switchMap(term => // ✂️ this.heroService.searchHeroes(term))); // ✂️ constructor(private heroService: HeroService) { } // ✂️ }
  85. 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<string>(); heroes$: Observable<Hero[]> = this.searchTerms.pipe( switchMap(term => this.heroService.searchHeroes(term))); constructor(private heroService: HeroService) { } onSearch(term: string): void { this.searchTerms.next(term); } } class HeroSearchContainerComponent { }
  86. 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<string>(); heroes$: Observable<Hero[]> = this.searchTerms.pipe( switchMap(term => this.heroService.searchHeroes(term))); constructor(private heroService: HeroService) { } onSearch(term: string): void { this.searchTerms.next(term); } } <app-hero-search-ui title="Hero Search"></app-hero-search-ui>
  87. 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<string>(); heroes$: Observable<Hero[]> = this.searchTerms.pipe( switchMap(term => this.heroService.searchHeroes(term))); constructor(private heroService: HeroService) { } onSearch(term: string): void { this.searchTerms.next(term); } } <app-hero-search-ui #provider="heroSearchProvider" [heroes]="provider.heroes$ | async" title="Hero Search" (search)="provider.onSearch($event)"></app-hero-search-ui>
  88. <app-hero-search> <app-hero-search-ui [heroes]="heroes$ | async" title="Hero Search" (search)="onSearch($event)"></app-hero-search-ui> </app-hero-search> PROVIDER

    DIRECTIVES Before, in parent component template Before, in container component template After, in parent component template MODEL-VIEW-PRESENTER WITH ANGULAR <app-hero-search-ui #provider="heroSearchProvider" [heroes]="provider.heroes$ | async" title="Hero Search" (search)="provider.onSearch($event)"></app-hero-search-ui>
  89. CLOSING REMARKS MODEL-VIEW-PRESENTER WITH ANGULAR

  90. 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
  91. 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
  92. 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
  93. 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/ larsbrinknielsen@gmail.com

    MODEL-VIEW-PRESENTER WITH ANGULAR