Published: Tue 08 Jan 2019

Creating an 'Add to Calendar' Widget in Drupal

A simple request: we need an 'Add to Calendar' widget to add our events to Google Calendar, iCal, and Outlook. Simple (once I had completed it!).

There's a module for that. There is, it's called, obviously, addtocalendar. It works very well, if you:

If you don't want to use an external service for something as simple as adding an event to a calendar, then it looks like you'll need a custom solution. Their smallest plan only allows 2 events per month.

The PatternLab Part

Here's the custom solution I came up with (in the future, I'll look at creating a module for this with a settings/UI page for site builders). Note, it's a PatternLab implementation; if you don't use PatternLab and just want to work directly in your Drupal theme, it would be even easier.

Here's the code for the 'Add to Calendar' pattern in PatternLab (some classes and things are removed to make it easier to read):

  1. {%
  2. set classes = [
  3. "add-to-calendar"
  4. ]
  5. %}
  7. {% set ical_link = 'data:text/calendar;charset=utf8,BEGIN:VCALENDAR%0AVERSION:2.0%0ABEGIN:VEVENT%0ADTSTART:' ~ atc_start_date|date("Ymd\\THi00\\Z") ~ '%0ADTEND:' ~ atc_end_date|date("Ymd\\THi00\\Z") ~ '%0ASUMMARY:' ~ atc_title ~ '%0ADESCRIPTION:' ~ atc_details|striptags ~ '%0ALOCATION:' ~ atc_location|replace({'<br />': ' ', '<br>': ' ', '<p>': ' ', '</p>': ''}) ~ '%0AEND:VEVENT%0AEND:VCALENDAR' %}
  9. {% set google_link = '' ~ atc_title ~ '&dates=' ~ atc_start_date|date("Ymd\\THi00\\Z") ~ '/' ~ atc_end_date|date("Ymd\\THi00\\Z") ~ '&details=' ~ atc_details|striptags ~ '&location=' ~ atc_location|replace({'<br />': ' ', '<br>': ' ', '<p>': ' ', '</p>': ''}) %}
  11. <div{{ attributes.addClass(classes) }}>
  13. <a href="{{ google_link }}">Add to Google Calendar</a>
  15. <a href="{{ ical_link }}">Add to iCal</a>
  17. <a href="{{ ical_link }}">Add to Outlook</a>
  19. </div>

What does the above code do?

  • Creates a Google Calendar variable and creates an iCal variable. Outlook will also use iCal.
  • Uses these variables as links to add the event to their respective calendars.

Within the variables, we have some more variables (start date, end date, etc), which we should probably wrap in conditional statements so that their clauses don't print unless they are present in Drupal (some fields might be optional on your event content type, such as end time).

These variables are:

  • atc_start_date: Start Date and time
  • atc_end_date: End Date and time
  • atc_title: the name of the event
  • atc_details: description for the event
  • atc_location: place of event

In our Event pattern in PatternLab, we then have a variable called 'add_to_calendar' so that events have the option to have this widget or not. In event.twig, we simply print:

  1. {% if add_to_calendar %}
  2. {% include '@site-components/add-to-calendar/add-to-calendar.twig' %}
  3. {% endif %}

The Drupal Part

In Drupal we create a boolean field on our event content type field_event_add_to_calendar, if this is ticked, we will display the Add to Calendar widget.

Here's the code from node--event--full.html.twig

  1. {# Set the Add to Calendar Variables #}
  3. {% if node.field_add_to_calendar.value %}
  4. {% set add_to_calendar = true %}
  5. {% endif %}
  7. {% if node.field_event_date.value %}
  8. {% set atc_start_date = node.field_event_date.value %}
  9. {% endif %}
  11. {% if node.field_event_date.end_value %}
  12. {% set atc_end_date = node.field_event_date.end_value %}
  13. {% endif %}
  15. {% if node.title.value %}
  16. {% set atc_title = node.title.value %}
  17. {% endif %}
  19. {% if node.field_event_intro.value %}
  20. {% set atc_details = node.field_event_intro.value %}
  21. {% endif %}
  23. {% if node.field_event_location.value %}
  24. {% set atc_location = node.field_event_location.value %}
  25. {% endif %}
  27. ...
  29. {% include "@content/event/event.twig" %}

To explain:

If the 'Add to Calendar' boolean is on, we set the add to calendar variable as true

This in turn tells patternlab to render the Add to Calendar component.

We then check if each field we might use has a value in it - such as a start date and end date

If so, we map the values from each of those fields to variables in our Add to Calendar component (such as atc_start, atc_title, etc)

Now, when you view a node, you will see your Add to Calendar widget on any nodes that the editors choose to put it. You can see a sample of the Add to Calendar widget in my PatternLab.

Simple, once I figured it out.

Got an improvement for this? The comments are open.



How do you deal with the requirement to wrap the lines at 75 characters?

Hi Mark,

Thanks a lot for this very interesting post.
I would assume previous comment refers to coding standards, which in my opinion is rather minor when looking at your post, there are so many more interesting elements (other than just the syntax).
But just in case, I found these two pages:……

Hope that helps clarifying previous comment.
Thanks again very much for sharing.

Thanks Dave,

I have thought of that. Next iteration will probably be a collection of variables and conditionals as mentioned in the post, so we won't attempt to add the end date if there was no end date. In that case, the line-length shouldn't be an issue, as it would look something like this:

{% set google_link = atc_start ~ atc_end ~ atc_title ~ atc_address %}


First of all, thanks very much for this post. It got me started in the right direction. That along with the comment with the validator links helped me get it 100%.

To keep within the 75-character line limit, your Twig variables need to be set up like this:

{% set atc_details = event_details|replace({' ': ' '})|split('', 75)|join('%0D%0A')|convert_encoding('UTF-8', 'HTML-ENTITIES') %}

(In this case, event_details is a variable with the body field plus a contact field).

The split filter divides the output into 75-character strings, and the join filter puts them back together with whatever argument you pass it as a delimiter between them. iCal requires CRLF line breaks, and because we're building a link, the codes for CR and LF in links are %0D and %0A (it took me a long time to figure that out). The convert_encoding filter ensures they're preserved as HTML entities.

I put this filter on the title, details, and location elements because they could all conceivably be longer than 75 chars.

You also need to replace %0A in the link output with %0D%0A to meet iCal requirements; and put %0D%0A before any of those split elements so that with their name the first line isn't longer than 75 ch.

According to the validator, this code was missing a DTSTAMP and UID; I made these like so:

{% set atc_created = node.getCreatedTime|date("Ymd\\THi00\\Z") %}

{% set uid = now|date(format='Ynjgisvu') %}

It also required a PRODID, which I set to the name of the company.

Final iCal link:

{% set ical_link = 'data:text/calendar;charset=utf8,BEGIN:VCALENDAR%0D%0AVERSION:2.0%0D%0APRODID:Marquette Alger RESA%0D%0ABEGIN:VEVENT%0D%0AUID:' ~ uid ~ '%0D%0ADTSTAMP:' ~ atc_created ~ '%0D%0ADTSTART:' ~ atc_start_date ~ '%0D%0ADTEND:' ~ atc_end_date ~ '%0D%0ASUMMARY:%0D%0A' ~ atc_title ~ '%0D%0ADESCRIPTION:%0D%0A' ~ atc_details ~ '%0D%0ALOCATION:%0D%0A' ~ atc_location ~ '%0D%0AEND:VEVENT%0D%0AEND:VCALENDAR' %}

Sorry if this comment is long, but I hope it might help someone else.

Ignore the "replace" filter in the atc_details variable, it's replacing a non-breaking space with a normal space character, but the nbsp got read as HTML, it looks like. Anyway, not important to the functionality here.

Add new comment

Restricted HTML

  • Allowed HTML tags: <a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>
  • Lines and paragraphs break automatically.
  • Web page addresses and email addresses turn into links automatically.