Welcome to Angular challenges.
The aim of this series of Angular challenges is to increase your skills by practicing on real life exemples. Moreover you will have to submit your work though a PR which I or other can review; as you will do on real work project or if you want to contribute to Open Source Software.
The first challenge is to create an highly customizable component that you can reuse with any crazy ideas that the product team can come up with.
If you haven’t done the challenge yet, I invite you to try it first by going to Angular Challenges and coming back afterward to compare your solution with mine. (You can also submit a PR that I’ll review)
We are going to implement a dashboard of multiples entities. (Teacher, Student and City). A naive working implementation of Teacher card and Student Card has already been coded and we need to refactor it to make it easier to customize.
Each Card must have a background-color, image, a list of removable items and an add button.
Card component of Student Entity
You will find below the current implementation of CardComponent and ListItemComponent
Issues:
Lots of ngIf condition: this will be become harder and harder to implement new cards in the future. Each card will need a new condition.
You can only pass a name as input in your ItemListComponent. What will happen if the product team decides to add an icon, or multiple properties to display, or if you have a new button? You will have to add new specific inputs and new if conditions…
The constructor of the component contains very specific imports. We want a generic component (called a presentational component), which doesn’t have any logic inside.
Component should be set to OnPush strategy.
Component is not strongly typed!
Let’s tackle each issue one by one :
Issue 1:
To delete all ifs inside my html, we will need to project the content of our image from the parent to the card component. To handle this, Angular has a tag called ng-content. (You can add a “select” attribute to be more specific on what you want to project from your parent)
We can now see that l.2–11 from our previous component has been replaced by a simple line. And on your parent component, you can simply add the img tag you want to project between your Card Component tags.
Issue 1bis and 3:
What about all the if conditions inside our component. To resolve this issue, we need to add a @Ouput() decorator to emit an event to the parent component which will take care of doing its specific action.
Issue 2:
This is the most complex part of the exercice. We could move ngFor to our student component and use ng-content to project the result. This will work but we want to keep the list pattern inside card component because we want to add (outside the scope of this exercice) generic logic and we don’t want to copy that logic in all our parent components.
We could add a ng-content inside ngFor. But this won’t work since we are missing the reference of the current item being displayed.
But Angular has us cover. NgTemplateOutlet is a directive which take a TemplateRef as input. We can then retrieve a custom template from the parent using @ContentChild() and pass it to our outlet directive. At this stage we are still missing our current items. And Angular has us cover again. We can pass a context to our custom template.
Remarks:
- The first argument of ngTemplateOutletContext is $implicit. If you want to pass more, you must named them.
[ngTemplateOutletContext]={$implicit: item; arg2: index}
<ng-template #rowRef let-teacher let-myIndex="arg2">
let-teacher in ng-template is not typed. You can add a ngTemplateContextGuard for strong typing, but this will be part of another challenge. (stay tuned!!)
app-list-item is still poorly customizable. But you know how to do it now. Go back to Issue 1 and apply the same strategy to your component.
Issue 4:
Now that our component only have Inputs and Outputs, there is no danger to simply add **changeDetection: ChangeDetectionStrategy.OnPush **in the decorator of our Component.
Issue 5:
To type our list of item in our component, we can use Generic.
And now our final CardComponent and ListItemComponent look like this: (This allows us to customize any dashboard card at will)
Remarks:
I’ve added the host property inside my decorator. Thus I can get rid of one level of encapsulation.
Don’t forget to import NgIf, NgFor and NgTemplateOutlet to your imports array. Or you can simply import CommonModule.
Finally, the code of StudentCard looks like below:
Remarks:
All the logics is now located only inside the smart component. Easy to maintain.
app-list-item is fully customizable. We can easily add an icon, or change the property we want to display
Component use the OnPush strategy since we are using the AsyncPipe to retrieve our data from the store.
I hope you enjoyed this first challenge and learned from it.
Other challenges are waiting for you at Angular Challenges. Come and try them. I’ll be happy to review you!
Follow me on Medium, Twitter or Github to read more about upcoming Challenges!
Top comments (5)
Thank you! Did you write about rxJS or ngRX, I am hungry to learn it.
just publish a new blog post. :)
A couple of issues with your approach:
ng-template
directive with specific ID (#rowRef
). It's not a very good DX for card component consumer.Much better solution is to have a separate
<app-card-content>
component and replaceng-template
withapp-card-content
.You can see how to implement "card-content" component if you check Angular Material source code ("mat-tab" component).
You can also see there an example of how to do it without
ng-content
if you want to lazy load your card content by using user provided template reference.Suggested solution:
Hi Tomas, thanks for your answer and i agree and disagree with you.
1- The goal of this first challenge was to show different way of projection and learn about ngTemplateOutlet and @ContentChild.
2- This example is very basic, but you might have a shared logic on list in card Component. (filtering, ordering, ...)
3- But I should have use a directive instead of a magic string to reference my template.
But you can go to github.com/tomalaforge/angular-cha... and submit this answer. That will be a great way to show a different approach (even if it's not answering this challenge.)
2) There is absolutely no need for card component to know anything about a list. Card is not meant to deal with data like some table or item list component. See how "Angular Material" card or "Taiga UI" island component is made.
3) Yes, using a directive is better than using an id, but still having a separate component is the best solution from DX perspective.
Sure, I'll take a look at that repo when I have some time.