Model-View-Presenter with Angular

Hi there!

Feel free to send me any questions about Model-View-Presenter with Angular, the article or the pattern.

3 Likes

Hello Lars! Awesome article!

I would like to know your opinion on where complex logic should go within this pattern. From your article i understand that:

  • container components: are responsible for fetching data from service and passing that (with suitable shape) to a presentational component

  • presentational component: (or dumb component) is responsible of displaying the view and if needed passing down data to a presenter.

  • presenter: is in charge of complex business logic. In this type of service i would do all business logic related so that the presentational component displays the view correctly

Example
Lets say we have a product card component that displays product data on the screen.

An occasional discount on a product, like a coupon, will be applied, component API would be:
@Input product: Product
@Input coupon: string

Presenter logic would be:

  1. Apply coupon discount to product price
  2. Calculate percentage discount to display on the product card as a label
  3. If percentage discount is bigger than 10% show percentage discount label in a different color.

My question is:
How would a presentational component with multiple inputs (@Input) pass those inputs to a presentar so that the presenter can execute complex logic on them, knowing that inputs have no specific order when bindings occurs. Even if there was a specific order, how would you wait for all inputs to have non-null values to call complexLogic() on the presenter?

How do you synchronize the 2 inputs to invoque the presenter correctly?

Thank you!

1 Like

Thank you for posting your question in inDepth Community, Mauricio!

The app would have to send the coupon code to the server to calculate a discount, right?

Hi Lars, yes it would have to send it to the server. The purpose of the example is to have a component with more than 1 input and logic on a presenter depending on these inputs.

If it helps, i can try to think about another example that serves the purpose.

Thanks.

Server communication is a job for a container component or even better a service which the container component forwards control to.

Maybe we can change the example to a percentage being passed to the presentational slide component?

1 Like

Hi Lars

I have question relating container and presentational components

Question 1)
Can a container have in its template another container ?

Question 2)
Can a presentational component have in its template a container ?

Agree. Yes, a percentage being passed to the presentational component is ok. As long as there are 2 Inputs on which a logic depends on, i think it serves the purpose.

Hey Dhaval,

You can have other templates in a presentational component’s template (if that’s the question you’re asking), but these templates need to also be dump components. So the presentational component can pass data into them (using @Input()) and the presentational component can listen for outputs from these templates.

I think it’s best to avoid having presentational components inside other presentational components. It makes it hard to keep track of the flow of data through the app.

Hope that helps.

Stephen

Great overview of the MVP pattern Lars.

1 Like

Thanks Stephen. I guess the question I was asking was

  1. in a container template, can you have another conatiner component as its ViewChild?
  2. In a presenter template , can you have another conatiner component as its ViewChild ?

If we have a component whose responsibility it is to simply compose container components, this is a presentational component. It has nothing to do with state, it simply orders other components to lay out a piece of a page.

We can also put container components in content children and templates that we pass to a presentational component.

Container components shouldn’t take state from parent components.

The main thing is to not have mixed components. Components should either have presentational concerns or glue the presentation layer to the rest of our app using services and other kinds of dependencies.

These answers might not be final. Try to convince me otherwise and I will be happy to discuss.

2 Likes

Hi @mauricio ,

Here’s a StackBlitz workspace demonstrating your example with some sample data.

Input properties are marked as optional (can be undefined). If you want them to support null, you would have to reflect that in their types.

Getters on the component model forwards input properties to the presenter to delegate computation of the UI properties.

The presenter accepts optional parameters and use default bottom values instead.

The component template binds to the UI properties of the component model (the getters) and apply formatting using pipes.

The color of the sale label is controlled through the conditional addition of a state CSS class which is bound to a UI property.

4 Likes

This is awesome, thank you for taking the time for doing this, it is a great example.

I added a new attribute “specialDiscount” that is independent of Items, what im trying to see with this example is how MVP would work when the presenter receives inputs that are not synced.

In this example you will see how after 5 seconds (simulating an http request to an independent service) the product card changes. This is trying to simulate if the user is elegible for a special deal and you would get that from a different service than the one you use for getting Items.

Im tyring to identify the proper way of using the pattern but also how not to use it.

I know this is not a great example and one would solve it using a different approach, like combineLatest but maybe there are cases where this is still applicable.

Again, thank you @LayZee !

Note: Please check “// NEW INDEPENDENT FIELD SIMULATING DELAY WITH items$” comment on carousel.container.ts

Which product is the special discount for? We are outside the scope of Model-View-Presenter now. The container component just needs to call an Angular service. The service figures out the business logic. MVP ends at the container components.

Hi @LayZee.
I downloaded the MVP version of the Tour of Heroes app from the GitHub repo that you provided in the article and the original one from Angular.io in order to compare them side by side. And it seems like there is a small omission. MessagesComponent is never going to be shown.

Both MessagesContainerComponent and MessagesComponent make use of ChangeDetectionStrategy.OnPush but the only source of data they manipulate with which is messages: string[] array never changes its reference after it has been updated with the new log. We only mutate it directly. If you change OnPush strategy on those components to the default one it will work.

I’ve tried to fix that with a few more observables around these components but the solution seemed sort of indelicate. Eventually, I ended up adding one more BehaviorSubject in MessageService with its initial value of messages and then subscribing to it into the template of MessagesContainerComponent via async pipe.

Here is the piece of code:
// message.service.ts

@Injectable({ providedIn: 'root' })
export class MessageService {
  _messages: string[] = [];
  messages: BehaviorSubject<string[]> = new BehaviorSubject(this._messages);

  add(message: string) {
    this._messages.push(message);
    this.messages.next(this._messages.slice());
  }

  clear() {
    this._messages = [];
  }
}
// messages.container.ts

@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  selector: 'app-messages',
  template: `
    <app-messages-ui
      [messages]="messages | async"
      title="Messages"
      (clear)="clearMessages()">
    </app-messages-ui>
  `
})
export class MessagesContainerComponent {
  get messages(): any {
    return this.messageService.messages;
  }

  constructor(private messageService: MessageService) {}

  clearMessages(): void {
    this.messageService.clear();
  }
}

What do you think of that? Maybe there is a way to handle it more elegantly in conformity with MVP pattern and preserve OnPush strategy.

By the way, thank you for the series of articles on MVP pattern, it is absolutely amazing. I wish I’d read it earlier.

Hi Ihor,

Thank you for your feedback. I didn’t notice that the messages component was missing :smile:

There are so many things that can be improved in the Tour of Heroes codebase. For the purpose of the Model-View-Presenter article series, I wanted to focus on components and component level services (such as presenters).

I also wanted the refactored code to be similar to the original codebase in terms of statements and data structures. Because of this, I didn’t want to touch the original services. Now that you’ve identified this issue, I did make a single change in the MessageService though.

I changed the add method from

add(message: string) {
  this.messages.push(message);
}

to

add(message: string) {
  this.messages = [...this.messages, message];
}

This is one of the problems with the default change detection strategy as far as I’m concerned. It doesn’t discourage us from using shared mutable state which is almost always a bad idea.

1 Like

Hi Lars,

Can a Smart component have inputs() as well as external dependency and Outputs()?

Hi Dhaval,

In my definition, a container component never has input or output properties. If you find yourself needing input or output properties, you probably have a mixed component.

Either split into a container component and a presentational component or use a service to share state between container components at different levels in the component tree.

Hello @LayZee!
Lets say you want to make your component accesible for other developers, maybe create a library or just use it in different contexts within an application.

How would you replace the presenter? If i import this component and would like to replace presenter logic with my custom presenter, what would be the best way to do that?

With current implementation, presentational component is coupled with presenter, i wonder how can we make this repleaceable so that i can provide different presenters logic.

Also, i think provider should remain at component level, in case presenter has UI state.

Thank you!

1 Like

Thank you for your question, @mauricio.

If we want to be able to replace the presenter while providing it at the component level, we could allow for a parent to provide the presenter by using the @Optional() and @SkipSelf() decorators.

Let’s take an example from my article Presenters with Angular:

// heroes.component.ts
import { Component, Optional, Output, SkipSelf } from '@angular/core';

import { HeroesPresenter } from './heroes.presenter';

@Component({
  providers: [HeroesPresenter],
  selector: 'app-heroes-ui',
  styleUrls: ['./heroes.component.css'],
  templateUrl: './heroes.component.html',
})
export class HeroesComponent {
  private presenter: HeroesPresenter;

  @Output('add')
  add$ = this.presenter.add$;

  constructor(
    @Optional() @SkipSelf() parentPresenter: HeroesPresenter | undefined, // 👈
    @Self() ownPresenter: HeroesPresenter // 👈
  ) {
    this.presenter = parentPresenter ?? ownPresenter;
  }

  addHero(): void {
    this.presenter.addHero();
  }
}

We could provide it in a container component like so:

// heroes.container.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { noop, Observable, Subject } from 'rxjs';
import { multiScan } from 'rxjs-multi-scan';

import { Hero } from '../hero';
import { HeroService } from '../hero.service';
import { HeroesPresenter } from './heroes.presenter';
import { VillainsPresenter } from './villains.presenter';

@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  providers: [
    {
       provide: HeroesPresenter, // 👈
       useExisting: VillainsPresenter, // 👈
    },
  ],
  selector: 'app-heroes',
  templateUrl: './heroes.container.html',
})
export class HeroesContainerComponent {
  private heroAdd: Subject<Hero> = new Subject();
  private heroRemove: Subject<Hero> = new Subject();

  heroes$: Observable<Hero[]> = multiScan(
    this.heroService.getHeroes(),
    (heroes, loadedHeroes) => [...heroes, ...loadedHeroes],
    this.heroAdd,
    (heroes, hero) => [...heroes, hero],
    this.heroRemove,
    (heroes, hero) => heroes.filter(h => h !== hero),
    []);

  constructor(private heroService: HeroService) {}

  add(name: string): void {
    this.heroService.addHero({ name } as Hero)
      .subscribe({
        next: h => this.heroAdd.next(h),
        error: noop,
      });
  }

  delete(hero: Hero): void {
    this.heroRemove.next(hero);
    this.heroService.deleteHero(hero)
      .subscribe({
        error: () => this.heroAdd.next(hero),
      });
  }
}