A DRY Approach To Overflow Menus in Angular Using Portals at Jcs - WTF
A DRY Approach To Overflow Menus in Angular Using Portals at Jcs - WTF
Intro
Let’s pretend you face the following problem:
So let’s get into building this in angular! If you want to jump ahead
and directly look at the full code here is the repo. Also, as it is
considered good practice (at least by me), here is what the final
thing will look like:
The outlet can be defined in many different ways. The official docs of
the cdk portal use the convenient cdkPortalOutlet directive on
another ng-template. To seal the deal, the only missing link is to
create an instance of your portal and set it as the input of the
cdkPortalOutlet directive. Now your template will be rendered
inside the element that has the directive.
1 <ng-template [cdkPortalOutlet]="portal"></ng-template>
2
3 <ng-template #content><span>foobar</span></ng-template>
1
:
2
3 @ViewChild('content') templateContent: TemplateRef<any>
4
5 ...
The notable new players on the field now are an overlay origin and a
position strategy. The origin defines what element of the DOM will be
taken as a reference for where to place the overlay. The
:
positionStrategy defines how the overlay will appear relative to its
origin. I won’t go into too much detail on position strategies because
this could be a whole topic for itself.
The final step to make our template appear inside the overlay now is
to create a TemplatePortal - just like in the first version - but then
attach it to the overlay:
Context-aware rendering
Maybe you noticed already where this is going: To achieve what we
want for our overflow menu, we need to combine the two
approaches into one. Components in our template should be
rendered in a standard outlet when there is still enough space. If
there is not enough space they should be rendered in the overlay
outlet.
The lazy solution could now be to just create all the items in your
toolbar twice - once in the actual toolbar and once in the overlays
template - and then just ngIf them in one or the other place based
on the window width. That’s not what I went for though because its
WET, hard to maintain and bulky to read.
To break down the better, DRY solution, let’s have a look at final API
first:
<jcs-dynamic-overflow-menu>
1
<ng-template>
2
<div class="item" *jcsResponsiveItem="'auto'; breakpoint: 250"> </div>
3
<div class="item" *jcsResponsiveItem="'auto'; breakpoint: 300"> </div>
4
:
5 <div class="item" *jcsResponsiveItem="'auto'; breakpoint: 350"> </div>
6 <div class="item" *jcsResponsiveItem="'auto'; breakpoint: 400"> </div>
7 <div class="item" *jcsResponsiveItem="'auto'; breakpoint: 500"> </div>
8 <div class="item" *jcsResponsiveItem="'auto'; breakpoint: 550"> </div>
9 </ng-template>
10
11 <button jcs-dynamic-overflow-menu-trigger>
12 <span>●</span>
13 <span>●</span>
14 <span>●</span>
15 </button>
16 </jcs-dynamic-overflow-menu>
The inputs of this directive are the item type and the breakpoint. The
item type makes it possible to define if an item should only ever be
displayed in either the host or the overlay or actually use the
breakpoint to determine it’s location.
Next we need to find out if the host element (of the directive) is
inside the host component or the overlay using isInHost. The best
way I could come up with to determine this is to (recursively) test if
nodeName of the parent Element is 'JCS-DYNAMIC-OVERFLOW-MENU'.
There might be better ways of doing it but this does the job.
Content projection
At this point we are basically done with the tricky part. But maybe
you wondered how we were able to pass the elements inside of jcs-
:
dynamic-overflow-menu to the portals. Maybe you did not wonder
about this, then you can skip this section.
The button is easy: We just create a div, add a click listener and
project it inside using the select attribute:
1 <div
2 class="overlay-trigger"
3 *ngIf="showMoreButton$ | async"
4 title="more"
5 (click)="open()"
6 #origin>
7 <ng-content select="[jcs-dynamic-overflow-menu-trigger]"></ng-content>
8 </div>
What I really like about this solution is the fact that it is DRY af. Using
it does not require you to maintain two sets of the same components.
This makes it super readable and easy to use.
Drawbacks
While developing I was mostly bothered by two major drawbacks:
If you made it until here, I can only say thank you for reading the
whole thing!
: