DEV Community

Cover image for Create a highly customizable component
thomas for This is Angular

Posted on • Edited on • Originally published at Medium

Create a highly customizable component

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.

image of student componentCard component of Student Entity

You will find below the current implementation of CardComponent and ListItemComponent

Issues:

  1. 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.

  2. 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

  3. 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.

  4. Component should be set to OnPush strategy.

  5. 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">
Enter fullscreen mode Exit fullscreen mode
  • 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)

Collapse
 
nvminhtu profile image
Tu Nguyen

Thank you! Did you write about rxJS or ngRX, I am hungry to learn it.

Collapse
 
achtlos profile image
thomas

just publish a new blog post. :)

Collapse
 
hakimio profile image
Tomas Rimkus

A couple of issues with your approach:

  • You are using generic 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 replace ng-template with app-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.

  • There is no need for the "card" component to know anything about your list and you should not pass the student list to the card component.

Suggested solution:

<app-card>
    <app-card-content>
        <app-list-item 
            *ngFor="let student of students"
            [name]="student.firstname"
        ></app-list-item>
    </app-card-content>
</app-card>
Enter fullscreen mode Exit fullscreen mode
Collapse
 
achtlos profile image
thomas • Edited

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.)

Collapse
 
hakimio profile image
Tomas Rimkus

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.