Implementing Smart and Presentational Components with Angular: Condensed Angular Experiences – Part 4

In this article, we will explore how to apply the concept of smart and presentational components with Angular. We will choose a complex-enough target to see all aspects in action, yet understandable and within the scope of this article. The goal is to teach you how to use this architecture in your way. For that, we will iterate through different development stages, starting with the target selection and implementing it in a naive way. After the first development, we will refactor that naive solution into smart and presentational components that are reusable, refactor-friendly, and testable.

In diesem Artikel:

MS-rund
Max Schulte ist Consultant bei Thinktecture mit dem Schwerpunkt auf Angular und 2D/3D Visualisierung.

This is the fourth part of the article series „Condensed Angular experiences“.
We are exploring concepts in Angular that seem simple but are very powerful and critical for the application architecture.

  1. Understanding Angular’s Async pipe
  2. What is the hype with Angular’s OnPush change detection?
  3. Interlude: About smart and presentational components
  4. Implementing Smart and presentational components with Angular
  5. Different approaches to complex and advanced forms in Angular (coming soon)

If you came here directly without reading the first article on smart and presentational components, you might want to do so before continuing.

Version information: The data and code in this article are based on these technology versions and resources:

A Smart and Presentational Components Example

We will develop a complex-enough dropdown as an example for the component development cycle and see how we benefit from the concept of smart and presentational components.
Most of you probably used a dropdown in some version of some framework at some point.
But how many have thought it through? We will do so now. If you already have thoughts or ideas about it: Great! Then compare your own thoughts and let me know about it.

The Dropdown should:

  1. Display a default state if nothing is selected
  2. Open on click
  3. Display at least two pieces of information about possible selections
  4. Close after selection
  5. Display a selected item differently

Let’s dive into some coding – we start by creating an album cover selection dropdown. We display information about the artist and the song for the third requirement. Displaying the album cover should fulfill the fifth requirement for the dropdown.

You can see and experiment with the final results here:

Creating a Naive Selection Dropdown

For this article, let’s start in a naive way and just care about solving the problem, in a straightforward manner. The created dropdown got an input for the options, an output for the selection, and some state information.

				
					export class NaiveDropdownComponent implements OnChanges {
  @Input() options: CdOption[] = [];
  @Input() selected?: OptionId;
  @Output() optionSelected = new EventEmitter<CdOption>();
​
  open = false;
  selectedOption?: CdOption;
​
  ngOnChanges(): void {
   ...
  }
​
  select(option: CdOption): void {
    ...
  }
}
				
			

The component is still simple, but it also has no smart or presentational aspects. There is no separation of concerns, no clear API. The state is not handled distinctly. The component is not refactor-friendly or extendable. Thus it is going to introduce some problems for us in the future.
Inspecting the HTML makes clear where one problem lies. Take a look at the button and the list: The dropdown logic is intertwined with the component.

				
					<button (click)="open=!open">
  <img [src]="selectedOption?.cover" alt="" class="preview" />
</button>
<ul *ngIf="open" class="panel">
  <li
    *ngFor="let option of options"
    (click)="select(option)"
    [class.selected]="selected === option.id"
  >
    <img [src]="option.cover" alt="" class="preview" />
    <div>{{option.artist}} - {{option.song}}</div>
  </li>
</ul>
				
			

Imagine a project manager or simply a new feature demanding another dropdown – similar enough to copy some code but not enough to reuse this specialized component. That is where the trouble starts. We duplicate code or introduce complex inheritance schemas. Most of these problems originate from the core problem: Separation of concerns, or better the lack of it. The component tries to provide too many features on its own (dropdown state, selection state, UI).

The best start is to separate all this into different components, creating reusable, extendable, and refactor-friendly components.

Making Smart and Presentational Components

As mentioned earlier, we want to create multiple components with only one feature, role, or concern to handle. Besides making it easier for us in the future, one other benefit is that each component is easily tested.

Find more about testing here: End-To-End Testing Of Angular Components With Cypress and here: Unit-Testing in Angular mit Jest (Webinar in german).

So let’s create components for the dropdown, select, and the specialized UI.

 

 

Extracting the Dropdown

The first step is to create a dropdown component.
We can move the existing CSS and logic there to open and close the dropdown – and nothing more. You may reckon the concise code below. It is nearly the same as above. It got an @Input() to set state, and from there on, it handles it on its own. The open property displays and hides the dropdown content in the view.

				
					export class DropdownComponent {
  @Input() open = false;
}
				
			
The component gets its displayed information via Angular’s ng-content content projection from its parent component. We add some basic structure and styling to make the dropdown.
				
					<button (click)="open = !open">
  <ng-content select="[trigger]"></ng-content>
</button>
<div *ngIf="open" class="panel">
  <ng-content select="[content]"></ng-content>
</div>
				
			

The change above may seem small, but it is already one huge step in the right direction.
We have already introduced some key aspects of smart and presentational components with that refactoring.
You can observe two patterns in action:

  1. Inversion of Control: The outer component can define what is displayed by the inner component
  2. Clear Responsibilities and Scopes: The Component does exactly one thing, opening and closing a dropdown


In fact, we created our first presentational component.
Making multiple of those changes will create a nice set of smart and presentational components.

The dropdown cares only about its state and some basic style aspects, not about the content it is displaying. For now, this doesn’t look like much, but it thrives later on – small changes can make a difference.

Now that the dropdown feature is extracted into a reusable and understandable component.
Let’s create a distinct select element.

Creating the Selection Component

The selection component provides all data and the view elements for the dropdown via content projection. It handles only the state required for displaying options and the selection. We have already split the dropdown part from the original code. We can move the remaining code to another component.

				
					export class SelectionComponent implements OnChanges {
  @Input() options: CdOption[] = [];
  @Input() selected?: OptionId;
  @Output() optionSelected = new EventEmitter<CdOption>();
​
  selectedOption?: CdOption;
​
  ngOnChanges(): void {
   ...
  }
​
  select(option: CdOption): void {
    ...
  }
}
				
			

There isn’t anything else to do here, for now. Let’s head over to the selection component template.

Adding an UI to the Selection

By dividing the logic and the UI, we can use Angular’s and the native Web’s composition features. Angular provides us with necessary dependencies, and we can put everything together with its content projection mechanics. With that, we are using one of the principles of smart and presentational components – Inversion of Control (IoC). The outer context defines what the inner context does.

				
					<ng-template #nonSelected>
  Please select a CD
</ng-template>

<app-dropdown #dropdown>
  <img
    *ngIf="selectedOption; else nonSelected"
    [src]="selectedOption?.cover"
    alt=""
    class="preview"
    trigger
  />
​
  <ul content>
    <li
      *ngFor="let option of options"
      (click)="select(option); dropdown.open = false;"
      [class.selected]="selectedOption?.id === option.id"
    >
      <img [src]="option.cover" alt="" class="preview" />
      <div>{{option.artist}} - {{option.song}}</div>
    </li>
  </ul>
</app-dropdown>
				
			

​We successfully split the dropdown component from the select component, laying the path for new components and changing requirements to come.

Creating Another Dropdown Component

We want to add another feature to the application to provide information about movies, not just artists and songs. Therefore, the design department created another dropdown component. The logic is the same – it just looks different. We can provide another UI and reuse all the logic behind the scenes, thanks to the refactoring earlier.

The example below demonstrates how easy it is to reuse the dropdown content. We only change the HTML that is projected into it.

				
					<ng-template #noSelection>
  Please select a DVD
</ng-template>
​
<app-dropdown #dropdown>
  <div trigger class="trigger">
    <ng-container *ngIf="selectedOption; else noSelection">
      <ul>
        <li>{{selectedOption?.title}}</li>
        <li>{{selectedOption?.oskars}} Oskars</li>
        <li> Director: {{selectedOption?.director}}</li>
      </ul>
      <img [src]="selectedOption?.poster" alt="" class="preview" trigger>
    </ng-container>
  </div>
​
  <ul content>
    <li *ngFor="let option of options" (click)="select(option); dropdown.open = false;"
        [class.selected]="selectedOption?.id === option.id">
      <img [src]="option.poster" alt="" class="preview">
      <div>
        {{option.title}} - {{option.year}}
      </div>
    </li>
  </ul>
</app-dropdown>
				
			

Changing only the HTML and copying the typescript part would not solve our problem. The next step is to create the component so that we can reuse the code and pass different UIs to it. This is similar to the dropdown solution, but different. We must respect the component state this time.

Reducing Duplicate Code

Solving this problem by not duplicating the component to change the UI, but providing the UI for the select component from the outside, is relatively simple and can be done in many ways. We will solve this again by providing the UI from the outside, but it is a bit different this time. The select component got a state, and that state is applied to its displayed content. We highlight selected options, display the selection differently from the rest, and react to user interactions. That would not work in the same way as before, where the dropdown is not extensible. The selection must remain somewhat open. This openness is a good example of the principle „Open/Close – open for extension, closed for modification“ of smart and presentational components. Here we introduce the GenericSelectComponent. For this article and the sake of simplicity, we utilize Angular’s ng-template and pass references from the outside. Instead of component inputs, we use content projection to pass those references. With that approach, the @Inputs() are spared for the model and state data.
				
					<app-generic-select [options]="cdOptions">
  <ng-template let-option #trigger>
    {{option?.artist ?? 'Select an option'}}
  </ng-template>
  <ng-template let-option let-selected="selected" #option>
    <div [class.current]="option?.id === selected?.id" class="option">{{option?.artist}}</div>
  </ng-template>
</app-generic-select>
				
			

Inside that component, the property decorator @ContentChild is used to access the reference, and we specifically request the TemplateRef from it.

				
					  @ContentChild('trigger', {read: TemplateRef}) triggerRef!: TemplateRef<T>
  @ContentChild('option', {read: TemplateRef}) optionRef!: TemplateRef<T>
				
			
From there, we can use it in conjunction with the *ngTemplateOutlet structural directive. That enables the GenericSelectComponent to provide the context for the template from the inside. We pass the selected option to the trigger template and provide the options list element data.
				
					<app-dropdown #dropdown>
    <div trigger>
      <ng-container *ngTemplateOutlet="triggerRef; context: {$implicit: selectedOption}"></ng-container>
    </div>

    <div content *ngFor="let option of options" (click)="select(option); dropdown.open = false;">
      <ng-container *ngTemplateOutlet="optionRef; context: {$implicit: option, selected: selectedOption}"></ng-container>
    </div>
</app-dropdown>
				
			

Using this method enables us to create generic components that do everything in their scope, but close that scope to modification. It holds and controls the state (what option is selected, how to choose an option, and how to react to input) and yet is open to modification.
The outside can control which and how information is displayed without touching the select. We can pass specialized templates to generic components without unnecessary duplicates. Everything is separated nicely and cleanly without duplicated code.

Conclusion

As demonstrated in this article, it is possible to break down a complex Component into smaller yet still powerful components. At the same time, these small components enable us, developers, to adapt to new requirements and react quickly to changes. We create refactor-friendly components that are easy to use and test.

Each component solves one problem, having only a single responsibility. Splitting and creating components that only solve one problem is called the „separation of concerns“ and is one of the key benefits of this architecture called „smart and presentational components“.
Creating components and complete applications with this architectural approach can be tricky and requires some practice. Still, you start to see the patterns, and creating components will eventually come naturally.

Now start using this practice, and keep me posted about your solutions. Don’t hesitate to ask if problems emerge or if you simply got further questions.

Kostenloser
Newsletter

Aktuelle Artikel, Screencasts, Webinare und Interviews unserer Experten für Sie

Verpassen Sie keine Inhalte zu Angular, .NET Core, Blazor, Azure und Kubernetes und melden Sie sich zu unserem kostenlosen monatlichen Dev-Newsletter an.

Newsletter Anmeldung
Diese Artikel könnten Sie interessieren
Low-angle photography of metal structure
AI
cl-neu

AI-Funktionen zu Angular-Apps hinzufügen: lokal und offlinefähig

Künstliche Intelligenz (KI) ist spätestens seit der Veröffentlichung von ChatGPT in aller Munde. Wit WebLLM können Sie einen KI-Chatbot in Ihre eigenen Angular-Anwendungen integrieren. Wie das funktioniert und welche Vor- und Nachteile WebLLM hat, lesen Sie hier.
26.02.2024
Angular
SL-rund

Konfiguration von Lazy Loaded Angular Modulen

Die Konfigurierbarkeit unserer Angular-Module ist für den Aufbau einer wiederverwendbaren Architektur unerlässlich. Aber in der jüngsten Vergangenheit hat uns Angular seine neue modullose Zukunft präsentiert. Wie sieht das Ganze jetzt aus? Wie konfigurieren wir jetzt unsere Lazy-Komponenten? Lasst uns gemeinsam einen Blick darauf werfen.
03.08.2023
Angular
yb

Using EntityAdapter with ComponentStore: @ngrx/entity Series – Part 3

As someone who enjoys the ComponentStore on an average level, I have written simple reactive CRUD logic several times. While storing a vast number of entities in the component state might not be a frequent use case, I will briefly illustrate the usage of the EntityAdapter with the @ngrx/component-store.
14.02.2023
Angular
yb

Multiple Entity Collections in the Same Feature State: @ngrx/entity-Series – Part 2

After introducing the @ngrx/entity package, I am often asked how to manage multiple entity types in the same feature state. While I hope that the previous part of this article series has made this more apparent, I will further focus on this question in the following.
07.02.2023
Angular
yb

Managing Your Collections With the EntityAdapter: @ngrx/entity-Series – Part 1

This three-part series of blogposts is targeted at developers who have already gained experience with NgRx but still manage their collections themselves. In the first part I introduce the Entity Adapter, in the second part I show you how to connect it to NgRx and in the third part how to do it with the Component Store as well.
31.01.2023
Angular
SL-rund

Angular OnPush – A Change Detection Strategy Revealing Mistakes in Your Code

When optimizing the performance of Angular applications, many developers directly associate Angular's OnPush change detection strategy with it. But, if you don't know exactly how OnPush works under the hood, it will quickly teach you what you are doing wrong the hard way. In this article, we'll look deeper into how the OnPush strategy affects Angular's change detection mechanism and which pitfalls we should avoid at all costs.
24.10.2022