Problem/Motivation

When editing a view, the preview was not rendering.

I had this error:

Uncaught TypeError: can't access property "eventDrop", calendarOptions is undefined
      buildCalendars https://calendar.mantra11.ddev.site/modules/contrib/fullcalendar/assets/js/fullcalendar.views.js?tifn4b:162
      buildCalendars https://calendar.mantra11.ddev.site/modules/contrib/fullcalendar/assets/js/fullcalendar.views.js?tifn4b:156
      attach https://calendar.mantra11.ddev.site/modules/contrib/fullcalendar/assets/js/fullcalendar.views.js?tifn4b:255
      attachBehaviors https://calendar.mantra11.ddev.site/core/misc/drupal.js?v=11.4.4:166
      attachBehaviors https://calendar.mantra11.ddev.site/core/misc/drupal.js?v=11.4.4:162
      insert https://calendar.mantra11.ddev.site/core/misc/ajax.js?v=11.4.4:1406
      jQuery 2
      insert https://calendar.mantra11.ddev.site/core/misc/ajax.js?v=11.4.4:1398
      commandExecutionQueue https://calendar.mantra11.ddev.site/core/misc/ajax.js?v=11.4.4:1048
Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

mortona2k created an issue. See original summary.

mortona2k’s picture

Status: Active » Needs review

I used claude to come up with a patch.

I don't completely understand what is happening, but looks like an issue with how drupal_static() is being used.

Otherwise, the change looks clean to me, and has good comments above the new code.

The preview is now working for me.

mandclu’s picture

I asked Claude to explain the proposed code changes:

Symptom: In Views UI preview, the calendar fails to render with a JS error: can't access property 'eventDrop', calendarOptions is undefined, thrown from buildCalendars() in fullcalendar.views.js.

Root cause (confirmed against the current code):

  • FullCalendar::prepareSettings() wraps its work in drupal_static(__METHOD__, []), and only rebuilds when the cache is empty.
  • The nested $settings['options'] array (the actual FullCalendar.js config consumed by calendarOptions = settings.options in the JS) is only populated by FullCalendar::preView() at line 1615, which runs via hook_views_pre_view() — a separate, later step than process().
  • In the Views UI preview flow, validate() calls prepareSettings() before hook_views_pre_view() has fired. At that point $this->style->options is still the raw, untransformed style config, so process() merges it into $settings with no options key at all.
  • Because that first call populates the drupal_static cache, the later render() call (after preView() has correctly transformed the options) sees a non-empty cache and reuses the stale/incomplete settings instead of rebuilding — leaving settings.options undefined and throwing in the JS, which aborts attachBehaviors for every other calendar on the page too (since the throw happens inside a forEach).

The proposed fix (MR !112)

  1. JS (fullcalendar.views.js): adds a guard — if calendarOptions is falsy, return early instead of letting the property access throw.
  2. PHP (FullCalendar.php): removes drupal_static() entirely from prepareSettings(); it now rebuilds $settings fresh on every call, and the old "these side-effects must run every call because the static may be shared" comment/logic is deleted since there's no longer anything to guard against.

Evaluation

This is the right fix, not a workaround. A couple of points worth noting rather than red flags:

  • It also fixes a second, unreported bug. drupal_static(__METHOD__, []) keys the cache by method name only, with no $this-scoping, so the cache was actually shared across every FullCalendar style plugin instance in a request. The old comment even acknowledges this ("a cached $settings value may be shared with a freshly-constructed style plugin instance") and papers over it by re-running a handful of per-instance side effects on every call. Removing the static cache eliminates that class of bug too (multiple calendar views on one page could otherwise leak each other's event data/options). That's a real, if incidental, improvement — not scope creep, since it's the same root mechanism.
  • Performance trade-off is minor and intentional. prepareSettings() (including prepareEvents()) now runs on every call instead of once — in practice up to twice per real render (validate() + render()). The MR's comment explicitly calls this out as an accepted cost ("only called about once per render, so rebuilding is cheap"). For very large views with expensive hook_fullcalendar_event/hook_fullcalendar_classes alter implementations this doubles that cost, but it's bounded and correctness-over-micro-optimization is the right call here.
  • Minor, non-blocking nit: the JS guard silently no-ops when calendarOptions is missing. A console.warn would preserve debuggability for any future cause of missing options, but it's not necessary for correctness — silently skipping one broken calendar while letting the rest of the page's calendars render is strictly better than the current hard failure.
  • Not addressed, but out of scope: getExposedDates() a few lines below (line 662) uses the exact same drupal_static(__METHOD__, []) pattern, with the same instance-sharing risk if two calendars on a page use the same field name. It isn't implicated in this bug and isn't touched by the MR — worth a follow-up issue, but no reason to hold this fix on it.

mandclu’s picture

Status: Needs review » Fixed

Thanks for the work here. I was able to reproduce the problem, and verified that the fix solved it. Merged in.

I considered adding the suggested JS debugging line, but ultimately decided that if included it should probably be kept behind a debugging flag. That likely warrants a broader discussion, so leaving that for a separate issue.

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.