Directive-based presentation

In Angular, presentation doesn't always require a component. Directives are often the right tool for adding visual behavior. Creating a component just to apply a style such as bold text or a background color would be overkill. Typically, plain CSS or an attribute directive handles such cases more cleanly.

A stateless presentational component might simply project content and wrap it in a specific DOM structure for styling. A toolbar that takes projected content and applies layout around it is a good example of this pattern.

Components with internal state

Presentational components can also manage their own state. A checkbox, for instance, has checked and unchecked states. This status is isolated UI state that belongs to the component.

Such a component is stateful, but its state is only valuable if it can be restored. When a user navigates back to a route, the checkbox should typically reflect its previous state.

Emitting state changes

For this local UI state to be useful, it must synchronize with the broader application state. If the checkbox itself contained logic to persist its status to WebStorage, it would no longer be purely presentational—it would be a mixed component.

To stay presentational, the checkbox communicates state changes through an output property, emitting events that the rest of the application can listen to.

Receiving state updates

A stateful presentational component may have an initial state independent of the application. But to stay in sync, it needs a way to receive external state when it is created. In the case of the checkbox, an input property governs its status.

The user isn't the only actor that can change the checkbox. A timer could set it to checked after a delay. The component would receive that change through its input property, keeping its UI state aligned with the application.

Keeping presentational components lightweight

Following the Model-View-Presenter pattern keeps presentational components lean. This means avoiding logic in both the template and the component class.

Templates should do little more than set up bindings—expressions for displaying data and event bindings for user actions.

Real behavior is delegated to presenters, which are component-level dependencies isolated from the rest of the application. This way, the component model only coordinates the binding of inputs, outputs, UI properties, and presenters.

In a Model-View-Presenter-style component, the model contains no business logic beyond glue code that ties together the data binding API, UI properties, event handlers, and presenters.

The role of presentational components

These components are called presentational because they embody the presentation and user interaction layers of an application, as shown in Table 1.

Ideally, user interaction is extracted to component-level services like presenters, as described in the section on lean presentational components.

Reusability of presentational components

Presentational components are often reusable because their data binding API lets them fit into many contexts.

However, one-off presentational components are also common. A logo component used only in the main layout is one example. Primary navigation, top app bars, and side drawers are other components that typically appear in a single parent but still have a presentational side. Depending on complexity, they might be split into container and presentational parts.

A reusable button is a classic example. If an organization builds a design system around it, all teams can use that button without worrying about color, font, or spacing changes. When the design system switches—say from Angular Material to Material UI—the button's implementation becomes the single place where that change happens.

Purity of presentational components

Presentational components are pure in the sense that they have no side effects. State management, persistence, messaging, I/O, and other non-presentational concerns belong in container components.

Their purity makes their rendering and emitted events deterministic.

Figure 1. DOM rendered based on 2 input values.

Figure 1. DOM rendered based on 2 input values.

Figure 1 shows that with input values valueX and valueY, the DOM is always rendered in the composition AxBy.

Figure 2. DOM rendered based on an input value and a user interaction.

Figure 2. DOM rendered based on an input value and a user interaction.

In Figure 2, valueX is received as input, followed by a user interaction captured as Event Y. The result is a DOM composition of AxEy, which is constant whenever Event Y occurs while valueX is the input.

Figure 3. DOM rendered based on an input value. Event emitted based on input value and user interaction.

Figure 3. DOM rendered based on an input value. Event emitted based on input value and user interaction.

The component in Figure 3 has a DOM composition of Ax based on valueX as input. A user interaction tracked as Event Z causes eventZ to be emitted through an output property.

This behavior is consistent whenever Event Z happens with valueX as the input value.

These behaviors must be demonstrable in tests. If not, the component is impure because it depends on external state. In that case, adding an input property for that state makes the component presentational with deterministic behavior and rendering.

Presentational components become dirty—meaning they need dirty checking—for two reasons:

  • An external event, such as a user interaction, is caught by an event binding in the template
  • New data is passed to one or more input properties

These two triggers make the OnPush change detection strategy a sensible default for optimizing performance.

A compact illustration

Picking up from the earlier section of "Container components with Angular", let's examine the current state of the mixed dashboard component from the Tour of Heroes tutorial—or, more precisely, what remains of it after we carved out a container component as demonstrated in Listing 1.

// dashboard.component.ts
import { Component } from '@angular/core';

import { Hero } from '../hero';

@Component({
  selector: 'app-dashboard',
  styleUrls: ['./dashboard.component.css'],
  templateUrl: './dashboard.component.html',
})
export class DashboardComponent {
  heroes: Hero[] = [];
}
Enter fullscreen mode Exit fullscreen mode
Listing 1. Dashboard: Mixed component model after extracting a container component.

To kick things off, we'll rename the selector to 'app-dashboard-ui' as outlined in Listing 3, ensuring it aligns with the HTML element in the template of our dashboard container component (Listing 1). Feel free to adopt any naming, file, folder, or selector strategy that suits your specific scenario or team preferences.

Outlining the component's data binding interface

As shown in Listing 2, the dashboard container component relies on two inputs from the presentational dashboard component: heroes and title.

<!-- dashboard.container.html -->
<app-dashboard-ui
  [heroes]="topHeroes$ | async"
  title="Top Heroes"></app-dashboard-ui>
Enter fullscreen mode Exit fullscreen mode
Listing 2. Dashboard: Container component template.

What's the rationale behind pulling the heading text out of the presentational component? If this component is a one-time addition to our app, we might leave the title embedded within it. However, by externalizing the title, we enhance its reusability. This dashboard offers a snapshot of the top heroes. Perhaps we need a similar view for female Marvel heroes or British villains. With the title extracted, we can now deploy the same presentational component across multiple container components, each supplying different hero datasets with contextually appropriate titles.

Consider also a scenario where our application supports dynamic language switching. In such a case, we could maintain a title observable that emits the title in the currently active language, or rely on a translation pipe backed by localization and internationalization services. Even under these circumstances, shifting the title source to the container component is prudent, keeping our presentational component clean and devoid of side effects.

// dashboard.component.ts
import { Component, Input } from '@angular/core';

import { Hero } from '../hero';

@Component({
  selector: 'app-dashboard-ui',
  styleUrls: ['./dashboard.component.css'],
  templateUrl: './dashboard.component.html',
})
export class DashboardComponent {
  @Input()
  heroes: Hero[];
  @Input()
  title: string;
}
Enter fullscreen mode Exit fullscreen mode
Listing 3. Dashboard: Presentational component model after declaring its data binding API.

We attach an Input decorator to the pre-existing heroes property, and we introduce the missing input, title.

With these additions, our presentational dashboard component now boasts a well-defined data binding API.

Keep the template's presentational logic to a minimum

Our goal is to keep presentational components lightweight. The templates should avoid unnecessary complexity, with any substantive presentation logic relegated to the component model or, even better, a dedicated presenter.

<!-- dashboard.component.html -->
<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>
Enter fullscreen mode Exit fullscreen mode
Listing 4. Dashboard: Presentational component template with minimal presentational logic.

In Listing 4, we observe a template expression bound to the title property, and we loop over the heroes property to generate a master list with a link for each hero.

The presentational logic embedded in this template is intentionally straightforward. It employs template expression bindings to render content, displays a child component (which, if fully refactored, would serve as a container component), and iterates through the heroes to append a link for each entry.

This component's template is juggling quite a bit of work across different categories. We could easily decompose it into multiple smaller components. One piece of genuinely complex logic remains: figuring out the route path for individual heroes.

Given that we're working from an established tutorial app, we won't break this into finer-grained components. Our focus stays on splitting mixed components into distinct container and presentational parts.

Check out "Lean Angular components" for practical examples on tackling similar component issues.

Implement the OnPush change detection strategy

With the mixed dashboard component now transformed into a truly presentational one, we can enable the OnPush change detection strategy to streamline dirty checking and improve rendering efficiency, as illustrated in Listing 5.

// dashboard.component.ts
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';

import { Hero } from '../hero';

@Component({
  changeDetection: ChangeDetectionStrategy.OnPush, // 👈
  selector: 'app-dashboard-ui',
  styleUrls: ['./dashboard.component.css'],
  templateUrl: './dashboard.component.html',
})
export class DashboardComponent {
  @Input() heroes: Hero[];
  @Input() title: string;
}
Enter fullscreen mode Exit fullscreen mode
Listing 5. Dashboard: Presentational component after the `OnPush` change detection strategy is applied.

When Angular traverses this component, it evaluates whether the values delivered to its input properties have shifted since the last change detection pass. If the input values remain unchanged, the dirty checking for this component's bindings—and those of all descendant components within the tree—is bypassed.

Should an event binding within this component's template fire, or should an AsyncPipe in a descendant container component emit a fresh value, both this component and all its ancestors in the component tree are flagged as dirty, ensuring they receive a full dirty check in the next change detection cycle.

Advanced example

In the earlier article on container components, we pulled a significant amount of logic out of the mixed heroes component, specifically the parts related to state management and persistence.

Let's take a look at where the mixed heroes component stands after that extraction. Listing 6 shows the current state.

// heroes.component.ts
import { Component } from '@angular/core';

import { Hero } from '../hero';

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

  add(name: string): void {
    name = name.trim();

    if (!name) {
      return;
    }
  }

  delete(hero: Hero): void {}
}
Enter fullscreen mode Exit fullscreen mode
Listing 6. Heroes: Mixed component model after extracting a container component.

Define the component's data binding contract

The container component requires the following data binding interface from the presentational component we aim to create by refactoring this mixed component:

  • Input: heroes: Hero[]
  • Input: title: string
  • Output: add: EventEmitter<string>
  • Output: remove: EventEmitter<Hero>

We can deduce these requirements from the hero container component's template, which is displayed in Listing 7.

<!-- heroes.container.html -->
<app-heroes-ui
  [heroes]="heroes$ | async"
  title="My Heroes"
  (add)="add($event)"
  (remove)="delete($event)"></app-heroes-ui>
Enter fullscreen mode Exit fullscreen mode
Listing 7. Heroes: Container component template.

The initial move in transitioning a mixed component into a presentational one is to outline its data binding API.

We also switch the element selector from app-heroes to app-heroes-ui, since the container component will take over the app-heroes name.

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

import { Hero } from '../hero';

@Component({
  selector: 'app-heroes-ui',
  templateUrl: './heroes.component.html',
  styleUrls: ['./heroes.component.css']
})
export class HeroesComponent {
  @Input()
  heroes: Hero[];
  @Input()
  title: string;

  @Output()
  add = new EventEmitter<string>();
  @Output()
  remove = new EventEmitter<Hero>();

  addHero(name: string): void {
    name = name.trim();

    if (!name) {
      return;
    }
  }

  delete(hero: Hero): void {}
}
Enter fullscreen mode Exit fullscreen mode
Listing 8. Heroes: Mixed component after declaring its data binding API.

A minor naming conflict emerged here. The output property was labeled add, which collided with the name of one of the component's event handlers.

A common habit is to prefix event handler methods with on, like onAdd. In this situation, we keep things aligned with the rest of the codebase and instead change the handler's name to addHero, as You can see in Listing 8.

It's worth noticing that the delete event handler now has an empty body. Since there’s no logic left, what role does it play? It previously held important logic for state management and persistence, but that responsibility has been shifted to the heroes container component.

The delete event handler is still linked to a user interaction through the component template, as shown in Listing 9.

<!-- heroes.component.html -->
<h2>
  My Heroes
</h2>

<div>
  <label>Hero name:
    <input #heroName>
  </label>

  <!-- (click) passes input value to add() and then clears the input -->
  <button (click)="add(heroName.value); heroName.value=''">
    add
  </button>
</div>

<ul class="heroes">
  <li *ngFor="let hero of heroes">
    <a routerLink="/detail/{{hero.id}}">
      <span class="badge">{{hero.id}}</span>
      {{hero.name}}
    </a>
    <button class="delete" title="delete hero"
      (click)="delete(hero)">x</button>
  </li>
</ul>
Enter fullscreen mode Exit fullscreen mode
Listing 9. Heroes: Initial mixed component template.

The next step is to wire up the component template so it uses the new data binding API.

<!-- heroes.component.html -->
<h2>
  {{title}}
</h2>

<div>
  <label>Hero name:
    <input #heroName />
  </label>

  <!-- (click) passes input value to addHero() and then clears the input -->
  <button (click)="addHero(heroName.value); heroName.value=''">
    add
  </button>
</div>

<ul class="heroes">
  <li *ngFor="let hero of heroes">
    <a routerLink="/detail/{{hero.id}}">
      <span class="badge">{{hero.id}}</span>
      {{hero.name}}
    </a>
    <button class="delete" title="delete hero"
      (click)="remove.emit(hero)">x</button>
  </li>
</ul>
Enter fullscreen mode Exit fullscreen mode
Listing 10. Heroes: Presentational component template after connecting it to the component's data binding API.

First, we swap the hardcoded heading for a template expression that reads from the title input property. As we discussed, this enhances the component's reusability.

Next, we make sure to update the reference to the renamed addHero event handler. Both this change and the title binding are visible in Listing 10.

To wrap up, we opt for an inline event handler that pushes the selected hero through the remote output property when the delete button is clicked.

Alternatively, we could have placed this in the delete event handler. A strict approach might favor that, but we're choosing to keep this straightforward business logic in the template for the moment. We'll come back to this choice shortly.

In the template, the hero name to be added is handed to the addHero event handler. However, we haven't yet linked it to the add output property we've just introduced.

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

import { Hero } from '../hero';

@Component({
  selector: 'app-heroes-ui',
  styleUrls: ['./heroes.component.css'],
  templateUrl: './heroes.component.html',
})
export class HeroesComponent {
  @Input()
  heroes: Hero[];
  @Input()
  title: string;

  @Output()
  add = new EventEmitter<string>();
  @Output()
  remove = new EventEmitter<Hero>();

  addHero(name: string): void {
    name = name.trim();

    if (!name) {
      return;
    }

    this.add.emit(name);
  }
}
Enter fullscreen mode Exit fullscreen mode
Listing 11. Heroes: Presentational component model after connecting the component template to the data binding API.

We removed the delete event handler after replacing it with an inline handler bound directly to an output property.

To finish, we completed the *add hero* flow by emitting the hero name through the add output property after validating it, as shown in Listing 11.

Keep template logic minimal

Our goal is to reduce the amount of logic residing in areas that are difficult to test. Angular-specific artifacts are, by nature, somewhat complex and challenging when it comes to testing.

User interfaces are typically tough and slow to test, and Angular components are no exception, at least not without extra tooling.

By relocating logic to parts of the application that are simpler and quicker to test, we improve the testability of that logic. In parallel, we separate concerns, which boosts maintainability, scalability, and stability.

All four of those *-ilities* are certainly qualities worth striving for!

Let's go back to the heroes component template and check if any non-trivial or intricate presentational logic remains. Refer to Listing 12.

<!-- heroes.component.html -->
<h2>
  {{title}}
</h2>

<div>
  <label>Hero name:
    <input #heroName />
  </label>

  <!-- (click) passes input value to addHero() and then clears the input -->
  <button (click)="addHero(heroName.value); heroName.value=''">
    add
  </button>
</div>

<ul class="heroes">
  <li *ngFor="let hero of heroes">
    <a routerLink="/detail/{{hero.id}}">
      <span class="badge">{{hero.id}}</span>
      {{hero.name}}
    </a>

    <button class="delete" title="delete hero"
      (click)="remove.emit(hero)">x</button>
  </li>
</ul>
Enter fullscreen mode Exit fullscreen mode
Listing 12. Heroes: Presentational component template.

To start with, this component still handles a broad range of use cases. It includes a form for creation, iterates over the list of heroes, displays their names, provides links to them, and shows delete buttons for each.

Typically, we'd break it into smaller, more focused presentational components, but in this series, we're only splitting components to create container components.

For illustrations of splitting components into smaller pieces, check out "Lean Angular components."

We'll set aside the hard-coded route segment in the template and not focus on it for now.

Earlier, we introduced the logic to emit a hero via the remove output property directly in the template. The drawback here is that the component model no longer shows how and when output emissions are triggered.

Additionally, this business logic is now in the template, outside the reach of the component model, making it untestable in unit tests that don't rely on the DOM.

The upside is that we've eliminated a basic event handler that was only serving as a bridge between a user interaction and an output property.

The remove.emit(hero) logic is straightforward enough that it doesn't require isolated testing. If something breaks, it will surface in integration or end-to-end tests.

Figure 4. The remove hero control flow with a presentational component.

Figure 4. The remove hero control flow with a presentational component.

Our delete hero flow now resembles Figure 4.

Returning to the template, there's still a piece of complex presentational logic handling the hero name text field. It even carries a comment to clarify its purpose. Does that ring a bell? Yes, it's a code smell!

Initially, the entered hero name is passed to the addHero event handler, and then the text field is cleared. Recall that the event handler validates the hero name? If the validation did more than just confirm that a non-empty name was submitted, we'd run into issues.

Since the field is cleared after submission and we have no UI property tracking the hero name, showing an error message related to the typed name wouldn't be feasible. We'd also lose the ability to retain the invalid entry in the field to assist with corrections.

These signs suggest we're relying on template-driven Angular forms, where switching to reactive Angular forms could enable testing UI behavior and form validation independent of the DOM.

It's noteworthy that once you venture beyond basic form validation or UI behavior, template-driven forms start to lose their appeal.

Relocate form logic to the component model

Let's adopt reactive forms to shift form validation and UI behavior logic out of the template and into the component model.

<!-- heroes.component.html -->
<h2>
  {{title}}
</h2>

<div>
  <label>Hero name:
    <input [formControl]="nameControl" />
  </label>

  <button (click)="addHero()">
    add
  </button>
</div>

<ul class="heroes">
  <li *ngFor="let hero of heroes">
    <a routerLink="/detail/{{hero.id}}">
      <span class="badge">{{hero.id}}</span>
      {{hero.name}}
    </a>

    <button class="delete" title="delete hero"
      (click)="remove.emit(hero)">x</button>
  </li>
</ul>
Enter fullscreen mode Exit fullscreen mode
Listing 13.1. Heroes: Presentational component template after extracting form validation and UI behaviour.
// heroes.component.ts
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { FormControl } from '@angular/forms';

import { Hero } from '../hero';

@Component({
  selector: 'app-heroes-ui',
  styleUrls: ['./heroes.component.css'],
  templateUrl: './heroes.component.html',
})
export class HeroesComponent {
  @Input()
  heroes: Hero[];
  @Input()
  title: string;

  @Output()
  add = new EventEmitter<string>();
  @Output()
  remove = new EventEmitter<Hero>();

  nameControl = new FormControl('');

  addHero(): void {
    let name = this.nameControl.value;
    this.nameControl.setValue('');
    name = name.trim();

    if (!name) {
      return;
    }

    this.add.emit(name);
  }
}
Enter fullscreen mode Exit fullscreen mode
Listing 13.2. Heroes: Presentational component model with form validation and UI behaviour.

In Listing 13.2, we introduce the UI property nameControl, which serves as a form control holding a text string.

Within the template in Listing 13.1, we use a FormControlDirective to bind the <input> element's value. For this directive to work, we must ensure ReactiveFormsModule is imported from @angular/forms into the module that declares our component.

The logic previously residing in an inline event handler has now been incorporated into the addHero method on the component model.

We capture a snapshot of the current value from the name form control, then reset the control's value. This change reflects in the <input> element after the next change detection cycle, which the form control update triggers.

Just as before, we trim any surrounding whitespace from the entered hero name—this both cleans it up and verifies it contains non-whitespace characters. If it passes, we emit it through the add output property.

Figure 5. The add hero control flow with a presentational component.

Figure 5. The add hero control flow with a presentational component.

Figure 5 illustrates the add hero flow.

There we have it—we've managed to move complex logic out of the template. One could argue the logic isn't all that complex, but it's substantial enough to be a hassle to test, particularly if you're going through the UI.

With the logic now in the component model, we have the flexibility to test it in an isolated unit test, treating the component model like a standard JavaScript class, without any UI involvement.

Keep component model logic to a minimum

This stage primarily involves removing any non-presentational logic from the component model, given that a presentational component should focus solely on presentation and user interaction.

In the previous article, we already shifted persistence and state management responsibilities to a container component. What remains in the component model now is form validation, which falls under user interaction.

Once we're left with only presentation and user interaction concerns, as is the case for our presentational heroes component, we guarantee the logic is as simple as possible. If it grows complicated enough to warrant separate testing, it's time to extract it into a presenter—a topic we'll delve into in an upcoming piece.

For now, we'll keep the form validation where it is in the component model.

Adopt the OnPush change detection strategy

There's just one final step. With the mixed component now a pure presentational component, we'll enable the OnPush change detection strategy to boost change detection performance.

This minor yet crucial change is outlined in Listing 14.

// heroes.component.ts
import {
  ChangeDetectionStrategy,
  Component,
  EventEmitter,
  Input,
  Output,
  } from '@angular/core';
import { FormControl } from '@angular/forms';

import { Hero } from '../hero';

@Component({
  changeDetection: ChangeDetectionStrategy.OnPush, // 👈
  selector: 'app-heroes-ui',
  styleUrls: ['./heroes.component.css'],
  templateUrl: './heroes.component.html',
})
export class HeroesComponent {
  @Input()
  heroes: Hero[];
  @Input()
  title: string;

  @Output()
  add = new EventEmitter<string>();
  @Output()
  remove = new EventEmitter<Hero>();

  nameControl = new FormControl('');

  addHero(): void {
    let name = this.nameControl.value;
    this.nameControl.setValue('');
    name = name.trim();

    if (!name) {
      return;
    }

    this.add.emit(name);
  }
}
Enter fullscreen mode Exit fullscreen mode
Listing 14. Heroes: Presentational component model using the `OnPush` change detection strategy.

The template bindings of the presentational heroes component will only undergo dirty checking when one of its input properties has changed since the last change detection cycle.

This aligns with what's called unidirectional dataflow in Angular. Data cascades down the component tree, originating in a data service, moving through the heroes container component, and finally being passed to an input property of the presentational heroes component.

Conversely, user interactions are captured via event listeners in the template, which then invoke event handlers in the component model. Following validation and processing, these user-driven events are turned into component-level events emitted through the presentational component's output properties.

A container component observes these component-specific events, performs further processing or mapping, and ultimately forwards them to data services. Events thus flow upward through the component tree.

Dynamic presentational components

So far, our examples have focused on use case-specific components. But there's another important category: reusable presentational components that we haven't mentioned.

The data binding API for *dynamic presentational components* doesn't center on application state. Instead, their main strengths lie in content projection or dynamic rendering, whether that's through component outlets, template outlets, or Angular CDK portals.

Consumers of these components provide templates or component types, or perhaps content that gets projected into the main content outlet. It could also be content that matches specific selectors. Alternatively, we might use presentational component wrappers or attribute directives.

Testing presentational components

Presentational components tied to specific use cases rather than generic UI behavior should be designed so they hardly need testing at all.

For use case-related presentational components, isolated unit tests rarely add value. Their logic is so straightforward that there is almost no chance of failure.

Instead, more involved presentational logic and UI behavior gets moved into presenters, which can be tested without Angular or even a DOM.

Because presentational components stay minimal in complexity, static analysis, integration tests, and end-to-end tests can catch basic mistakes like typos, type errors, or mapping issues.

The most valuable place for unit tests is documenting the component API for dynamic presentational components. Storybook serves as an alternative for API documentation, and we can even run end-to-end tests directly on Storybook stories.

Providing fake application state

A key advantage of keeping our components pure as presentational components is that they do not depend on application state. Where the data originates is irrelevant to them. They stay completely separate from application state, aside from local UI state.

This means application state can be supplied from any source, including fabricated data. How does this help? When the backend is still in progress, fake application state lets us continue development without waiting.

Fake data can also be passed to presentational components for testing purposes. For a kitchen sink page in our UI library—say, with Storybook or Angular Playground—we can supply fake data sets to exercise the various states our presentational components support.

Characteristics of presentational components

Presentational components are designed for reuse. When they render application state, they do not care where that state is stored. They may hold state, but only as local UI state, like a Boolean flag or a CSS state rule that marks a dropdown menu as open or closed.

Presentational components are responsible for visually presenting the UI to the user. When the user interacts, either local UI state changes within the component model or template, or a component-specific event is emitted via an output property.

For a given set of input property values and local UI state, a presentational component always produces the same DOM structure. This predictability allows us to use the OnPush change detection strategy, improving performance by running dirty checking only when necessary.

Presentational components can serve multiple use cases. When paired with a container component, they become use case-specific. Often, a single presentational component maps to a single container component, though one container may also connect to several presentational components, whether of the same or different types.

Certain presentational components are built specifically for reuse rather than to address a particular set of application use cases. These correspond to atoms, molecules, and organisms in the Atomic Design methodology. Collections of such components can be organized into UI workspace libraries or publishable UI libraries.

Converting a mixed component to a presentational component

To turn a mixed component into a presentational component, follow these steps:

  1. Extract a container component that handles non-presentational concerns.
  2. Define the presentational component's data binding API, including its input and output properties.
  3. Keep presentational logic in the component model and template minimal. Delegate complex user interaction and presentation logic to one or more presenters, which are component-level services handling UI behavior, form validation, or formatting.
  4. Use the OnPush change detection strategy to boost change detection efficiency.

When extracting a container component, the mixed component's template should remain largely unchanged.

The result is a presentational component with two main responsibilities:

  • Display application state to the user
  • Modify application state in response to user actions

Continue your journey in "Presenters with Angular".

Start with the introductory article “Model-View-Presenter with Angular”. It also contains links to the companion GitHub repository, related articles, and other helpful resources.

Find out how to extract a container component from a mixed component in "Container components with Angular".

Learn how to extract a presenter from a presentational component in "Presenters with Angular".

Peer reviewers