1

I have a lightning web component that receives some data from a user, calculates some stuff in apex, and returns a list of events (not sObject Events, just JSON objects). that are populated into a calendar thanks to FullCalendar.io.

Now I'm trying to export that data (from the client) as a downloadable .ics (calendar) file. Everywhere on the internet seems to suggest this is the most reliable javascript solution for what I'm looking for: https://github.com/nwcell/ics.js

When I load the scripts in my component, I tried instantiating ics() which should result in a new object, but it's returning undefined instead.

Here is a snippet starting from the rendered callback:

renderedCallback(){
        Promise.all([
            loadScript(this, ExportCalendarJS + '/ics/ics.js'),
            loadScript(this, ExportCalendarJS + '/ics/ics.min.js'),
            loadScript(this, ExportCalendarJS + '/ics/ics.deps.min.js'),
            loadScript(this, ExportCalendarJS + '/ics/Gruntfile.js')
        ])
        .then(() => {
            this.calendarForExport = ics();
            console.log(ExportCalendar); // returns the correct path of static resource
            console.log(this.calendarForExport);// returns 'undefined'
                    })
        .catch(err => {console.log(err)});
    }

Console.log shows the path of the static resource, and the helper file for the library says to use ics() to instantiate a new ics object. But ics() is showing up as undefined.


Here is the ics.js file if anyone is interested:

var ics = function(uidDomain, prodId) {
  'use strict';

  if (navigator.userAgent.indexOf('MSIE') > -1 && navigator.userAgent.indexOf('MSIE 10') == -1) {
    console.log('Unsupported Browser');
    return;
  }

  if (typeof uidDomain === 'undefined') { uidDomain = 'default'; }
  if (typeof prodId === 'undefined') { prodId = 'Calendar'; }

  var SEPARATOR = (navigator.appVersion.indexOf('Win') !== -1) ? '\r\n' : '\n';
  var calendarEvents = [];
  var calendarStart = [
    'BEGIN:VCALENDAR',
    'PRODID:' + prodId,
    'VERSION:2.0'
  ].join(SEPARATOR);
  var calendarEnd = SEPARATOR + 'END:VCALENDAR';
  var BYDAY_VALUES = ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'];

  return {
    /**
     * Returns events array
     * @return {array} Events
     */
    'events': function() {
      return calendarEvents;
    },

    /**
     * Returns calendar
     * @return {string} Calendar in iCalendar format
     */
    'calendar': function() {
      return calendarStart + SEPARATOR + calendarEvents.join(SEPARATOR) + calendarEnd;
    },

    /**
     * Add event to the calendar
     * @param  {string} subject     Subject/Title of event
     * @param  {string} description Description of event
     * @param  {string} location    Location of event
     * @param  {string} begin       Beginning date of event
     * @param  {string} stop        Ending date of event
     */
    'addEvent': function(subject, description, location, begin, stop, rrule) {
      // I'm not in the mood to make these optional... So they are all required
      if (typeof subject === 'undefined' ||
        typeof description === 'undefined' ||
        typeof location === 'undefined' ||
        typeof begin === 'undefined' ||
        typeof stop === 'undefined'
      ) {
        return false;
      }

      // validate rrule
      if (rrule) {
        if (!rrule.rrule) {
          if (rrule.freq !== 'YEARLY' && rrule.freq !== 'MONTHLY' && rrule.freq !== 'WEEKLY' && rrule.freq !== 'DAILY') {
            throw "Recurrence rrule frequency must be provided and be one of the following: 'YEARLY', 'MONTHLY', 'WEEKLY', or 'DAILY'";
          }

          if (rrule.until) {
            if (isNaN(Date.parse(rrule.until))) {
              throw "Recurrence rrule 'until' must be a valid date string";
            }
          }

          if (rrule.interval) {
            if (isNaN(parseInt(rrule.interval))) {
              throw "Recurrence rrule 'interval' must be an integer";
            }
          }

          if (rrule.count) {
            if (isNaN(parseInt(rrule.count))) {
              throw "Recurrence rrule 'count' must be an integer";
            }
          }

          if (typeof rrule.byday !== 'undefined') {
            if ((Object.prototype.toString.call(rrule.byday) !== '[object Array]')) {
              throw "Recurrence rrule 'byday' must be an array";
            }

            if (rrule.byday.length > 7) {
              throw "Recurrence rrule 'byday' array must not be longer than the 7 days in a week";
            }

            // Filter any possible repeats
            rrule.byday = rrule.byday.filter(function(elem, pos) {
              return rrule.byday.indexOf(elem) == pos;
            });

            for (var d in rrule.byday) {
              if (BYDAY_VALUES.indexOf(rrule.byday[d]) < 0) {
                throw "Recurrence rrule 'byday' values must include only the following: 'SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'";
              }
            }
          }
        }
      }

      //TODO add time and time zone? use moment to format?
      var start_date = new Date(begin);
      var end_date = new Date(stop);
      var now_date = new Date();

      var start_year = ("0000" + (start_date.getFullYear().toString())).slice(-4);
      var start_month = ("00" + ((start_date.getMonth() + 1).toString())).slice(-2);
      var start_day = ("00" + ((start_date.getDate()).toString())).slice(-2);
      var start_hours = ("00" + (start_date.getHours().toString())).slice(-2);
      var start_minutes = ("00" + (start_date.getMinutes().toString())).slice(-2);
      var start_seconds = ("00" + (start_date.getSeconds().toString())).slice(-2);

      var end_year = ("0000" + (end_date.getFullYear().toString())).slice(-4);
      var end_month = ("00" + ((end_date.getMonth() + 1).toString())).slice(-2);
      var end_day = ("00" + ((end_date.getDate()).toString())).slice(-2);
      var end_hours = ("00" + (end_date.getHours().toString())).slice(-2);
      var end_minutes = ("00" + (end_date.getMinutes().toString())).slice(-2);
      var end_seconds = ("00" + (end_date.getSeconds().toString())).slice(-2);

      var now_year = ("0000" + (now_date.getFullYear().toString())).slice(-4);
      var now_month = ("00" + ((now_date.getMonth() + 1).toString())).slice(-2);
      var now_day = ("00" + ((now_date.getDate()).toString())).slice(-2);
      var now_hours = ("00" + (now_date.getHours().toString())).slice(-2);
      var now_minutes = ("00" + (now_date.getMinutes().toString())).slice(-2);
      var now_seconds = ("00" + (now_date.getSeconds().toString())).slice(-2);

      // Since some calendars don't add 0 second events, we need to remove time if there is none...
      var start_time = '';
      var end_time = '';
      if (start_hours + start_minutes + start_seconds + end_hours + end_minutes + end_seconds != 0) {
        start_time = 'T' + start_hours + start_minutes + start_seconds;
        end_time = 'T' + end_hours + end_minutes + end_seconds;
      }
      var now_time = 'T' + now_hours + now_minutes + now_seconds;

      var start = start_year + start_month + start_day + start_time;
      var end = end_year + end_month + end_day + end_time;
      var now = now_year + now_month + now_day + now_time;

      // recurrence rrule vars
      var rruleString;
      if (rrule) {
        if (rrule.rrule) {
          rruleString = rrule.rrule;
        } else {
          rruleString = 'rrule:FREQ=' + rrule.freq;

          if (rrule.until) {
            var uDate = new Date(Date.parse(rrule.until)).toISOString();
            rruleString += ';UNTIL=' + uDate.substring(0, uDate.length - 13).replace(/[-]/g, '') + '000000Z';
          }

          if (rrule.interval) {
            rruleString += ';INTERVAL=' + rrule.interval;
          }

          if (rrule.count) {
            rruleString += ';COUNT=' + rrule.count;
          }

          if (rrule.byday && rrule.byday.length > 0) {
            rruleString += ';BYDAY=' + rrule.byday.join(',');
          }
        }
      }

      var stamp = new Date().toISOString();

      var calendarEvent = [
        'BEGIN:VEVENT',
        'UID:' + calendarEvents.length + "@" + uidDomain,
        'CLASS:PUBLIC',
        'DESCRIPTION:' + description,
        'DTSTAMP;VALUE=DATE-TIME:' + now,
        'DTSTART;VALUE=DATE-TIME:' + start,
        'DTEND;VALUE=DATE-TIME:' + end,
        'LOCATION:' + location,
        'SUMMARY;LANGUAGE=en-us:' + subject,
        'TRANSP:TRANSPARENT',
        'END:VEVENT'
      ];

      if (rruleString) {
        calendarEvent.splice(4, 0, rruleString);
      }

      calendarEvent = calendarEvent.join(SEPARATOR);

      calendarEvents.push(calendarEvent);
      return calendarEvent;
    },

    /**
     * Download calendar using the saveAs function from filesave.js
     * @param  {string} filename Filename
     * @param  {string} ext      Extention
     */
    'download': function(filename, ext) {
      if (calendarEvents.length < 1) {
        return false;
      }

      ext = (typeof ext !== 'undefined') ? ext : '.ics';
      filename = (typeof filename !== 'undefined') ? filename : 'calendar';
      var calendar = calendarStart + SEPARATOR + calendarEvents.join(SEPARATOR) + calendarEnd;

      var blob;
      if (navigator.userAgent.indexOf('MSIE 10') === -1) { // chrome or firefox
        blob = new Blob([calendar]);
      } else { // ie
        var bb = new BlobBuilder();
        bb.append(calendar);
        blob = bb.getBlob('text/x-vCalendar;charset=' + document.characterSet);
      }
      saveAs(blob, filename + ext);
      return calendar;
    },

    /**
     * Build and return the ical contents
     */
    'build': function() {
      if (calendarEvents.length < 1) {
        return false;
      }

      var calendar = calendarStart + SEPARATOR + calendarEvents.join(SEPARATOR) + calendarEnd;

      return calendar;
    }
  };
}; 
  • What does your static resource look like? What did you actually upload? Just the ics.min.js file or a zipped directory? – stackasaur Apr 20 at 16:29
  • I've uploaded a zip file, but I've retrieved the path of the individual files in my rendered callback. I edited my original post above if you want to take a look at the file I'm trying to reference. – Thomas Reinman Apr 21 at 15:57
  • Please see this workaround: salesforce.stackexchange.com/questions/266435/… – gkolan Apr 21 at 16:02
  • Thanks gkolan, I didn't know about that issue. However I'm not receiving the error 'The first parameter of loadScript() and loadStyle() must be an LWC component.', so I don't think I'm running into that issue. – Thomas Reinman Apr 21 at 16:08
  • So just loadScript on the ics.min.js. Loading all of them will just overwrite stuff. Then try console.log(ics) – stackasaur Apr 22 at 13:10
1

In order to load a static resource you need the following pieces in your LWC:

import { loadScript, loadStyle } from 'lightning/platformResourceLoader';

import D3 from '@salesforce/resourceUrl/d3';

renderedCallback() {
    if (this.d3Initialized) {
        return;
    }
    this.d3Initialized = true;

    Promise.all([
        loadScript(this, D3 + '/d3/d3.v5.min.js'),
        loadStyle(this, D3 + '/d3/style.css')
    ])
        .then(() => {
            this.initialize();
        })
        .catch(error => {
            console.log(JSON.stringify(error));

        });
}
| improve this answer | |
  • Thanks Mitch, I already have all of that in place. I've included a snippet from my code above for more context, as well as the actual file I'm trying to reference. – Thomas Reinman Apr 21 at 15:56
0

It took a bit of trial and error even after I figured out the initial issue, but sticking to answering the original question I posted:

I couldn't access the variables in the static resource because they were private. I followed the example from this Salesforce documentation (https://developer.salesforce.com/docs/atlas.en-us.lightning.meta/lightning/security_share_code.htm), edited the variable names in the js.min file by adding 'window.' to each variable I needed, which made it possible to instantiate them and they were no longer undefined.

As a side note, although I was able to use ics.js, I couldn't get FileSaver.js to work at all, and I tried via ics.js (included in package) and directly via FileSaver. So I ended up going with this workaround:

HTML

<a href={calendarDownload} download="Running Calendar" class="download_btn slds-p-around_medium">
{exportCalendarLabel}
</a>

JS

get exportCalendarLabel(){
        return 'Download ' + this.calculateTimeFrame + ' week Training Calendar'
    }

    get calendarDownload(){
        var calHREF = 'data:text/calendar;charset=utf-8,' + encodeURIComponent(this.calendarForExport.build());
        return calHREF;
    }

thanks for help on previous posts.

| improve this answer | |

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.