Angular Challenges: A New Series
This series is designed to sharpen your Angular skills through practical, real-world exercises. Each challenge should be submitted via a pull request, allowing for peer review—just like in a professional setting or when contributing to open-source projects.
The inaugural challenge focuses on building a highly customizable component, ready for any unexpected requirements your product team might throw at you.
If you haven't attempted the exercise yet, I recommend starting there. Head over to the Angular Challenges repository, try to solve it yourself, and then return here to compare your approach with mine. You're also welcome to submit a PR for review.
Our goal is to build a dashboard displaying various entities, including Teacher, Student, and City. We've started with basic implementations for Teacher and Student cards, but we need to refactor them to allow for greater flexibility and customization.
Each card must include a background color, an image, a list of removable items, and an add button.
The Student Entity Card Component
Below is the initial implementation of our CardComponent and ListItemComponent.
Identifying the Problems
Proliferation of ngIf conditions: Adding new card types will require adding new conditional logic, making the template increasingly complex and harder to maintain.
Limited flexibility: Your
ItemListComponentcan only accept a singlenameinput. If the product team wants to display an icon, additional properties, or a new button, you'll have to introduce more specific inputs and more conditional checks.Tight coupling: The component's constructor has highly specific imports. We aim for a generic, presentational component that is free of business logic.
Missing optimization: The component should use the
OnPushchange detection strategy.Lack of type safety: The component is not strongly typed.
Let's address each of these issues systematically.
Solving Issue 1: Eliminating Conditional Logic
To remove all the if statements from our HTML, we can project content from the parent component into the card. Angular provides the ng-content tag for this purpose. You can also use a select attribute to be more specific about which content to project.
With this change, the multi-line image block is replaced by a single line. In the parent component, you can now place your desired img tag between the opening and closing tags of the card component.
Solving Issues 1bis and 3: Handling Actions and Logic
To remove the remaining conditional logic, we'll add an @Output() decorator to emit events to the parent component. The parent, being a "smart" component, will handle the specific actions triggered by these events.
Solving Issue 2: Customizing the List Items
This is the most challenging part. One option is to move the ngFor loop to the student component and use ng-content to project the resulting list. However, we want to keep the list iteration logic within the card component to enable future generic features without duplicating logic in every parent.
Simply placing a ng-content inside an ngFor loop won't work, as we'd lose track of the currently rendered item.
Fortunately, Angular provides the NgTemplateOutlet directive. This directive accepts a TemplateRef as an input. We can fetch a custom template from the parent using @ContentChild() and pass it to the outlet. We can also provide a context object to the template, giving it access to the current data.
Key Points
- The first property in the
ngTemplateOutletContextmust be named$implicit. Any additional data properties must be explicitly named.
[ngTemplateOutletContext]={$implicit: item; arg2: index}
<ng-template #rowRef let-teacher let-myIndex="arg2">
The template variable (e.g.,
let-teacher) is not strictly typed. You can add anngTemplateContextGuardto enforce type safety, though that topic is for a future challenge.Our
app-list-itemis still not fully customizable, but you now have the knowledge to fix it. Revisit the solution for Issue 1 and apply the same pattern to this component.
Solving Issue 4: Enabling OnPush
Now that our component consists solely of inputs and outputs, we can safely add changeDetection: ChangeDetectionStrategy.OnPush to the component decorator.
Solving Issue 5: Adding Strong Typing
To add type safety to the list of items in our component, we can use TypeScript generics.
Here is the final, fully customizable version of our CardComponent and ListItemComponent.
Final Notes
The
hostproperty has been added to the decorator, allowing us to remove an unnecessary level of DOM encapsulation.Remember to import
NgIf,NgFor, andNgTemplateOutletin your component's imports, or simply import the entireCommonModule.
For instance, the updated StudentCard component would look like this:
Observations
All business logic is now contained within the 'smart' parent component, making it easier to maintain.
app-list-itemis now fully customizable, making it easy to add icons or change the displayed properties.The component benefits from the
OnPushstrategy, especially when combined with theAsyncPipefor data retrieval.
I trust this first challenge proved both enjoyable and educational. More challenges await at Angular Challenges—come test your skills and submit your work for review.
Follow me on Medium, Twitter, or GitHub to stay updated on upcoming challenges.
