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.
- Understanding Angular’s Async pipe
- What is the hype with Angular’s OnPush change detection?
- Interlude: About smart and presentational components
- Implementing Smart and presentational components with Angular
- 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:
- Angular: 10+
- Source Code
- Demo
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:
- Display a default state if nothing is selected
- Open on click
- Display at least two pieces of information about possible selections
- Close after selection
- 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();
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.
-
{{option.artist}} - {{option.song}}
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;
}
ng-content
content projection from its parent component.
We add some basic structure and styling to make the dropdown.
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:
- Inversion of Control: The outer component can define what is displayed by the inner component
- 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();
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.
Please select a CD
-
{{option.artist}} - {{option.song}}
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.
Please select a DVD
- {{selectedOption?.title}}
- {{selectedOption?.oskars}} Oskars
- Director: {{selectedOption?.director}}
-
{{option.title}} - {{option.year}}
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
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.
{{option?.artist ?? 'Select an option'}}
{{option?.artist}}
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
@ContentChild('option', {read: TemplateRef}) optionRef!: TemplateRef
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.
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.