Using an Angular Directive to Extend a Third-Party Component

I added mode-toggling to a PrimeNG Calendar! šŸ¤˜

Michael Jacobson
ITNEXT

--

Photo of cute fluffy white dog pushing a big red button
Image source: PetaPixel

The Challenge

I recently had a requirement to add a datepicker to a page that would allow the user to select a single date or a date range.

In our application we already used the PrimeNG Calendar component, which has a selectionMode property for configuring the datepicker to accept a single date, multiple dates, or a date range.

Unfortunately, though, the component does not provide a built-in way to toggle between modes.

It was time to get creative! šŸ¤”

The Goal

The Calendar component looks like this:

PrimeNG Calendar component with footer button bar

I thought the footer button bar would be a perfect place to put the mode toggle. So my goal became this:

PrimeNG Calendar component with Date and Date Range buttons added to center of footer button bar

The Approach

Since I would need to manipulate DOM, a Directive seemed like the right tool for the job.

This is the template I started with:

The next decision: how and where to define the custom Date and Date Range buttons that I would be inserting into the Calendar?

The Options

Two options occurred to me:

  1. Define them as HTML in the template and have the Directive reference them from there
  2. Create them programmatically within the Directive class using Renderer

With Option #1, it would be cleaner and easier to build the markup for the buttons (in my opinion, anywayā€”when working with markup, I generally prefer working within an .html file).

But with Option #2, the Directive would be more self-contained and you could just attach it to p-calendar instances in multiple templates throughout your app without needing to add the extra button markup in each template to support it.

First Cut: Option #1

For my first cut, I went with Option #1, and thatā€™s what this article will step through.

I chose that option because, in my situation, we had our own custom Angular wrapper component around p-calendar anyway, and that custom component is what gets instantiated throughout the app, not p-calendar, so I could just define the button markup in that custom componentā€™s template and never have to do it again.

I did, however, also implement Option #2 afterward, just for my own edification, to see how it compared. Links to both implementations (as well as a third using a Component and CDK Portal that occurred to me later!) are at the end of the article.

The Implementation

Hereā€™s what the button markup looks like defined in the custom wrapper componentā€™s template right alongside p-calendar:

On line 4, I attached the Directive, calendarModeToggle, to p-calendar and passed a reference to the buttonsā€™ wrapper element, modeToggle, into it.

I wrapped the buttons in adiv that includes a style tag for the necessary styling, keeping the markup and styling together in one place.

I placed that wrapper div inside an HTML template tag so it would be accessible to the Directive via a template reference variable but not rendered on the page.

Finally, I attached template reference variable #modeToggle to the wrapper div so I could pass the reference into the Directive.

The Directive Setup

Hereā€™s what the Directive class looks like, taking the wrapper div as an Input:

In case youā€™re not familiar with this patternā€¦if your Directive uses an attribute selector and you create an @Input() for the Directive with the same name as the selector (in this case, calendarModeToggle), you can attach the Directive to an element and pass something into it in one convenient step, like we do on p-calendar:

[calendarModeToggle]="modeToggle"

Inserting the Custom Buttons

Now that the Directive had a hold of div.toggle-wrapper, the next step was to have it insert those buttons into the Calendarā€™s button bar.

Hereā€™s where DOM manipulation came into play!

This is what the Calendar componentā€™s buttonbar looks like:

To insert my toggle buttons between the Today and Clear buttons, I turned to Angularā€™s Renderer2.

Listening for Datepicker Show

Since the Calendarā€™s datepicker isnā€™t displayed all the timeā€”only when the control is selectedā€”the Directive needed to hook into the Calendarā€™s onShow event and insert the custom buttons every time the datepicker was shown.

To do that, we inject the Calendar instance into the Directiveā€™s constructor and subscribe to its onShow event:

When the datepicker is shown, we call the Directiveā€™s addToggleButtonToButtonBar() method, which looks like this:

Notice that the method first checks to see if this.buttons already exists.

Thatā€™s because, even though the datepicker part of the Calendar component is inserted or removed from the DOM as the Calendar is clicked and dates are selected, the Calendar component itself remains, and therefore so does our Directiveā€”the Directive is not re-instantiated every time the datepicker is displayed.

Because of that, we only need to define the buttons once, but we need to re-insert them into the button bar every time the datepicker is shown.

Empowering the Buttons

Next I needed to wire up the button clicks so they would change the Calendar selection mode.

For that, I used Render2ā€™s listen method:

The listen method returns an ā€œunlistenā€ function for disposing of the handler. I store those ā€œunlistenā€ functions in an array called stopListening so I can invoke them when the Directive is destroyed so as not to cause a memory leak. šŸ‘¼

The click listeners call Directive method setMode, passing in the selected mode and a reference to the clicked button. The setMode method looks like this:

We explicitly set the Calendarā€™s selectionMode to the selected mode, then add a CSS class to the selected button so we can highlight it.

The optional third param, clearSelection, is set to true for the button clicks so that, when we change modes, we clear any current date selection. (Youā€™ll see why thatā€™s an optional flag defaulting to false shortly.)

Tricky Considerations

There were a couple tricky scenarios that popped up during the implementation:

  • Changing modes triggered a user-unfriendly jump to the current month if you had navigated to a different month or year, requiring you to re-navigate back to where you were
  • Clicking the Today button when in Date Range mode didnā€™t work correctly because itā€™s a single date

Unfriendly Jump to Current Month

When changing modes, the Directive triggers a clearing of the current date selection. That clearing, in turn, causes the Calendar component to trigger a jump to the current month, which is not desirable. So I needed to fix that.

The Directiveā€™s clearDateSelection method, called upon mode change, looks like this:

It simply uses the Calendar componentā€™s writeValue method, passing it null to clear any selected value.

That was the simple part!

The tricky part came when I noticed that, if I had changed to a different month and then changed modes, I would automatically be taken back to the current month. Not a very friendly user experience.

So I had to dig into the Calendarā€™s source code to figure out how to prevent this.

Undoing Jump to Current Month

It turned out there was no way to prevent it without modifying the source code, but there was a way to immediately undo it using the Calendarā€™s createMonths method, which sets the current displayed month/year.

So now, before clearing the selected date, I store the currently selected month and year in local vars. I get those from the Calendarā€™s currentMonth and currentYear properties.

Then, after clearing, I see if either of those Calendar properties changed. If either one did, I invoke the Calendarā€™s createMonths method, restoring the previously selected month and year without impacting the user at all. šŸ’Ŗ šŸ¤“

Clicking the Today Button

The final tricky thing that needed to be handled was a click on the Today button.

Screenshot of PrimeNG Calendar component with its Today button pointed to

If the Today button was clicked with the Calendar in Date Range mode, we would need to change to Date mode.

Conveniently, the Calendar component exposes an onTodayClick event that we can subscribe to. I added that in ngOnInit:

And hereā€™s what the handleTodayClick method looks like:

When the Calendar is in ā€œrangeā€ mode, its value property is an array rather than a single value. The first and last selected dates become the first and second elements in the array.

When the Today button is clicked in ā€œrangeā€ mode, todayā€™s date becomes the first element in the array.

So on line 6, we reassign the Calendarā€™s value property, setting it equal to that date and therefore changing it from an array to a single Date value. This is the state we would be in if the Calendar had already been in ā€œsingleā€ mode.

We then hide the overlay/datepicker, which triggers a fade-out animation.

Then, after a small delay (see why below), we set the mode to ā€œsingleā€ using the Directiveā€™s setMode method, but without the optional third clearSelection argument because we just set the Calendarā€™s value property and we donā€™t want to clear it.

Finally, we call the Calendarā€™s writeValue method, passing it the value property we just set, which is todayā€™s date.

Why the Delay?

I added that small delay purely for aesthetic reasons.

It worked fine without the delay but, upon clicking Today, you would immediately see the selection highlight jump from Date Range to Date, which felt a little strange and jarring to me. By adding the small delay to account for the overlay fade animation time, that highlight jump becomes very subtle or even unnoticeable.

The Final Product

Hereā€™s how it turned out:

Animated gif showing the calendar working with the new mode toggle buttons

To prevent the toggle buttons from shifting horizontally when the selection is changed and the selected button is bolded, I had to play with their letter-spacing.

In the styles I set the letter-spacing for the non-bolded and bolded buttons so that their total widths would be the same:

Without that, it would have looked like this, which is a bit janky and unprofessional:

Animated gif showing the two toggle buttons shift horizontally as you select one or the other due to the selected one being bolded

Links

Here are links for the three different implementations of this enhancement.

1. Directive with buttons defined in the template

2. Directive with buttons defined programmatically

3. Component using CDK Portal to portal the buttons into Calendar

The repo for the CDK Portal implementation is an actual runnable Angular app, while the other two are just places to store and share the code.

Final Thoughts and a Caveat

I realize that some of this enhancement is dependent upon internal implementation details of a third party component that could change in the future with only a minor or patch version bump.

In my case, though, this was for an essentially in-house tool with dependency versions strictly controlled and upgrades fully tested. So I was not concerned about PrimeNG possibly changing their internal implementation and breaking this without our noticing and having an opportunity to fix it before it reached production.

Keep that in mind, though, before using an approach like this in your own projects!

Thanks for reading! šŸ˜ŗ

--

--

Writer for

Frontend Developer working with Angular for 10+ years. I love solving problems and building cool stuff. I sweat the details becauseā€¦I love the details.