3

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;
    }
  };
}; 
6
  • 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, 2020 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. Apr 21, 2020 at 15:57
  • Please see this workaround: salesforce.stackexchange.com/questions/266435/…
    – gkolan
    Apr 21, 2020 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. Apr 21, 2020 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, 2020 at 13:10

2 Answers 2

2

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

        });
}
1
  • 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. Apr 21, 2020 at 15:56
1

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.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .