I have 1000+ events spread out across years, and I obviously dont want to load them all everytime I open the calendar view

I dont know if its an actual feature yet and I'm doing something wrong, but I cant seem to get AJAX loading to work out of the box

The ideal would be that when you click back/forth between months, it loads the nodes as necessary.. Is this possible out of the box?

I can see that the pager is highlighted in the readme picture, but I cant seem to figure out what "5" means :)
https://www.drupal.org/files/project-images/FullCalendar%20View%20Settin...

Thanks in advance for any help :)

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:

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

ras-ben created an issue. See original summary.

Mingsong’s picture

Hi ras-ben,

I am intending to deliver this new feature with Version 3.0 as we have to rebuild the structure for this module to implement this functionality.

We need to create a data source for the Ajax calling by a attached view or something external such as JSON feed event source.

ras-ben’s picture

Hey Mingsong

Thanks a lot for the answer!
Do you have any idea/timeframe when Version 3.0 will be ready?

Mingsong’s picture

I will try to make some time for it this weekend.

Hopefully I can deliver it this weekend.

ras-ben’s picture

Wow, that would be awesome - thank you!
If there's something I can do to help you along, let me know :)

ras-ben’s picture

Hey Mingsong

Not trying to be pushy, but do you have any idea when version 3 is gonna be out? :)

Mingsong’s picture

Hi ras-ben,

I have been busy recently. I will commit all I have done so far by git this weekend.

You are welcome to have a close look at it and let me know what you think.

The main idea is that I will create a custom block delivered with this module as the view to represent the FullCalendar to client side that can accept a URL as the event source. See the Doc for details.

We also need to provide an approach to exposing nodes as event sources in JSON.

Currently, my thought is that we can provide a custom view delivered with this module which exports event nodes in JSON as the event sources for that block we created.

What do you think?

ras-ben’s picture

Hey Mingsong

Sorry about the slow reply - I've been sick/away but I'm back now :)

Did you ever get around to committing the code? I dont seem to be able to find it :)

Your idea of a custom view with nodes sounds like an excellent approach to the issue.
If you have any code ready, I'll be happy to test it in my setup and see how it works and give you some feedback?

Mingsong’s picture

Hi Ben,

I just create a new branch of 8.x-3.x and committed two new sub-modules with it.

Cheers,

Ludo.R’s picture

I also have 1500+ events spread over several years and I'm very interested into AJAX loading, otherwise my page is quite heavy.
I see that you have some sort of PoC in 8.x-3.x to have an external source for events.

What are your plans for a 3.x release?

timwood’s picture

Hi. I'm also interested in this feature. I notice the 3.x branch hasn't been updated along with the 2.x branch. Any thoughts on the paging feature?

jdearie’s picture

+1 on interest in this feature. with a large number of events, the page load will be too slow to make the calendar view usable.

jarykohlbern’s picture

Hey, looking to implement this feature as soon as possible. Could use an overview of the work done so far so I know where to start with completing what's left.

Mingsong’s picture

Version: 8.x-2.x-dev » 8.x-3.x-dev
Mingsong’s picture

It is part of the gold for version 3.

Mingsong’s picture

Related issues: +#3095827: 9.x Roadmap
Mingsong’s picture

Version: 8.x-3.x-dev » 8.x-4.0

This will be a new feature for 9.x.

graker’s picture

Hi Mingsong, thanks for you work on this module!

Just subscribing to the issue as I need this feature as well. Will it be available in 5.* branch that works on Drupal 8 though?

Mingsong’s picture

This feature is pretty completed for a view plugin. A potential solution is to use the Ajax filter of the view to reduce the amount of records loaded initially.

If others have a better solution please raise here.

graker’s picture

Thanks. I'm not sure though how to tie ajax filter to the Fullcalendar arrows. I mean, so that the View would load new data when user changes selected month or week.

Mingsong’s picture

Unfortunately not, but The view will load selected data from server once the filter is changed.
A work around is to add a exposed filter for the the range, for example from 2019.01.01 to 2019.12.31. So that the view initially only loads the records in that time period. The client can change the date range by the filter.

graker’s picture

Okay, thanks.
Then I can probably add some JS to the arrows so they would affect the filter.

dww’s picture

The map views display plugin from geolocation does something similar with a hidden exposed filter that it updates via JS when the map is resized. See geolocation/src/Plugin/views/filter/BoundaryFilter.php -- all AJAX, the view only loads the items that are included within the map. A very similar approach could happen here.

Thanks,
-Derek

timwood’s picture

Has anyone made any progress on a working solution to this issue they'd be willing to share? Our users have reported major performance issues with our events calendar. We already limited the result set by date range (not exposed). This results in less than 500 items displayed, but load time is really long when cache is empty, even on fast production environment. Our ideal solution is a views pager which works with the arrow navigation buttons to load more results when clicked. This way the view can load only the current months results initially.

Thanks

robotjox’s picture

Would love to hear some suggestions too. We have a calendar with thousands of events, and it pretty much crashes the site.

Also I'm not sure which ajax filter is referred to in this thread?

dww’s picture

Not yet, but if anyone wanted to sponsor me to work on this, I'd be happy to get it working and contribute the solution here. Just contact me if you need this and have a budget for it. ;)

Cheers,
-Derek

robotjox’s picture

Unfortunately, I don't have the resources to sponsor development, but I would be happy to chip in to a joint pool if anyone is interested?

In the meantime I am looking for a workaround - I see mention of an ajax filter, but I can't figure out what is meant by that. Is it simply an exposed date filter? For that to work I guess the filter would need to be changed automatically with JS for every arrow click (going from month to month)?

I would be very thankful if anyone can maybe share how they approached this problem as it prevents our calendar from working.

timwood’s picture

Just yesterday I tested implementing a workaround with an exposed filter for our date field which allows us to load less results initially (using default values) to help speed up the main calendar page, while still giving users flexibility to view more calendar items. Results set went from over 400 to under 100 setting defaults to -1 - +1 month rather than non-exposed filter which was set to -6 - +9 months. I didn't use JS/AJAX or hook up the filter to the arrow buttons for paging, so it's a lot less elegant than what others have suggested here.

  1. Add an exposed filter for your date field.
  2. Configure the filter with the "is between" operator (or add two separate filters for the same date field and configure one with the "is greater than or equal to" and the other with "is less than or equal to".
  3. Configure the filter(s) to use a value type of "An offset..." with a default offset date (eg. between -1 month and +1 month -- unfortunately when you use offset your users are now stuck with this format rather than a standard date). I could not find a way to use the formatted date value type with the jQuery UI date picker (provided by the better exposed filters module) while providing a default that was relative to today's date. Tokens don't work as a default value for a views filter.
  4. Alternatively, configure the filter(s) as a "Grouped filter" instead of "Single filter" in order to provide a list of default values for the date range such as "One month ago" so your users don't have to mess with this. (You'll need the patch from comment 38 on this issue.)

Views exposed filters showing date range in collapsed fieldset

The collapsed fieldsest is provided by the better exposed filters module.
Views exposed filters showing date range in collapsed fieldset

Fieldset expanded showing range start options

Views exposed filters showing date start and end filters in expanded fieldset

Fieldset expanded showing range end options

Views exposed filters showing date start and end filters in expanded fieldset

Fieldset displays expanded across page loads when defaults not selected

Views exposed filters showing date start and end filters in expanded fieldset

robotjox’s picture

Thanks so much for explaining your approach. I have been fiddling with something similar.

Correct me if I am wrong, but with this setup, the calendar will not show any items when users are navigating outside the date range using the arrows, right?

For my users it is important that they can navigate several years without having to deal with exposed filters.

I have tried to add some query that autosubmits a (hidden) exposed date filter every time the user clicks an arrow.

So for instance:

If you click on the back arrow the jquery subtracts a month from the current input date filter and submits the exposed form automatically.

However the problem I am now facing is that this is only working with a page refresh - an ajax submit causes no results found.

timwood’s picture

Correct me if I am wrong, but with this setup, the calendar will not show any items when users are navigating outside the date range using the arrows, right?

Correct. An unfortunately side effect.

Your approach sounds great. If you don't mind, please share more details / code for what you've done so far. Maybe some one will be able to help. As for ajax, I'm not sure whether this module works with ajax yet. Maybe the maintainer can address that.

mandclu’s picture

AFAIK this module currently puts all the data directly into the page, which is why Mingsong mentioned that it would require a fair bit of work to implement this.

Exposing the data probably isn't super difficult, thought probably easier if we can resolve #2868014: [PP-1] Views Date Filter Datetime Granularity Option. Hoping to spend some time on that in the next few days.

There is some discussion on loading events into Fullcalendar via AJAX at https://stackoverflow.com/questions/12019130/how-can-i-load-all-events-o...

robotjox’s picture

Well, it would be better if it was working :-D

I use an exposed date "between" filter like you do.

Then I use very simple jquery to catch the click on the arrow:

        $('.fc-prev-button').click(function(){

and for every click on the arrow back button I simply subtract one month from the current date in the field (using a counter for every click):

counter++;
$('#edit-mydatefield-value-min').val('- ' + counter + ' months');

Autosubmitting like this:

$('#edit-submit-mycalendar').click();

The main problem with this approach is that as soon as I submit the exposed filter form with ajax I get an empty result.
If I don't use ajax the whole page refreshes which of course puts the user back to the current month as the whole point of fullcalendar is to not refresh the page.
I really suck at jquery so that might explain a lot :-)

I suspect much of this is because what is really needed is a views plugin, but I currently do not have the time to make one - it took me a long time to create a custom decorator.

I wonder why the ajax filtering does not work - this thread makes multiple mentions of an ajax filter?

I really hope someone can take this further!

timwood’s picture

Version: 8.x-4.0 » 5.x-dev

Changed version to latest -dev version.

Mingsong’s picture

For those wondering what view Ajax filter is, the following article might help you to understand it.

http://www.softdecoder.com/blog/filter-content-drupal-view-ajax

According to my test, if I disabled the BigPipe module, the view will work fine with Ajax filter.

So I think the thing we need to work out is how to know the current load is a Ajax call rather than a normal page load.

 // If the BigPipe module is enabled and it is not an AJAX callback,
      if (drupalSettings.calendar  && settings.bigPipePlaceholderIds && is not an Ajax callback) {
        // Rebuild the container.
       ....
     }
     else {
       // Build the calendar.
     }
robotjox’s picture

I think I made a little progress.

I have managed to create a javascript plugin that only loads the items of the current month when the user navigates with the previous/next buttons automatically updating an exposed "between dates" filter.

Unfortunately this only works for the month view - I am somehow unable to make my code detect when the user changes to a week, day or list view, so that needs to be fixed.

Bear in mind that I'm not an expert in JS/Jquery/Ajax, so a lot of the following code definately needs a lot of cleaning up.

Note:
my fullcalendar view has ajax enabled.
my custom module name is "sctmariae_calendar".
my exposed date filters are called "edit-field-tid-value-min" and "edit-field-tid-value-max".

Hope this makes sense, and that someone can help us take this further.

(function ($) {
  var d = new Date();
  d.setMonth(d.getMonth());
  Drupal.behaviors.sctmariae_calendar = {
    attach: function (context, settings) {
      settings.calendar.forEach(function(calendar) {
        calendar.gotoDate(d);
          $(document).once('sctmariae_calendar1').on('click', '.fc-prev-button', function (event) {
            d.setMonth(d.getMonth() - 1);
            month = '' + ("0" + (d.getMonth()+1)).slice(-2);
            dayfirst = '01';
            daylast = new Date(d.getFullYear(), d.getMonth()+1, 0);
            daylasttwodigits = ("0" + daylast.getDate()).slice(-2);
            year = d.getFullYear();
            if (month == '00'){
              month = '12';
              year = year-1;
            }
            var startdate = year + '-' + month + '-' + dayfirst;
            var enddate = year + '-' + month + '-' + daylasttwodigits;
            $('[data-drupal-selector="edit-field-tid-value-min"]').val(startdate);
            $('[data-drupal-selector="edit-field-tid-value-max"]').val(enddate);
            $('input.js-form-submit').click();        
          });
          $(document).once('sctmariae_calendar2').on('click', '.fc-next-button', function (event) {
            d.setMonth(d.getMonth() + 1);
            month = '' + ("0" + (d.getMonth()+1)).slice(-2);
            dayfirst = '01';
            daylast = new Date(d.getFullYear(), d.getMonth()+1, 0);
            daylasttwodigits = ("0" + daylast.getDate()).slice(-2);
            year = d.getFullYear();
            if (month == '13'){
              month = '01';
              year = year+1;
            }
            var startdate = year + '-' + month + '-' + dayfirst;
            var enddate = year + '-' + month + '-' + daylasttwodigits;
            $('[data-drupal-selector="edit-field-tid-value-min"]').val(startdate);
            $('[data-drupal-selector="edit-field-tid-value-max"]').val(enddate);
            $('input.js-form-submit').click();
          });
          $(document).once('sctmariae_calendar3').on('click', '.fc-today-button', function (event) {
            d = new Date();
            month = '' + ("0" + (d.getMonth()+1)).slice(-2);
            dayfirst = '01';
            daylast = new Date(d.getFullYear(), d.getMonth()+1, 0);
            daylasttwodigits = ("0" + daylast.getDate()).slice(-2);
            year = d.getFullYear();
            if (month == '13'){
              month = '01';
              year = year+1;
            }
            var startdate = year + '-' + month + '-' + dayfirst;
            var enddate = year + '-' + month + '-' + daylasttwodigits;
            $('[data-drupal-selector="edit-field-tid-value-min"]').val(startdate);
            $('[data-drupal-selector="edit-field-tid-value-max"]').val(enddate);
            $('input.js-form-submit').click();
          });
      })
    }
  };
})(jQuery);

EDIT: removed some wrong code

robotjox’s picture

Allow me to add: the reason I have a hard time getting the week, day, list views to work with the above is because that as soon as the ajax filter is submitted the fullcalendar view resets to the default month view.

I hope someone who knows more about ajax can chime in here - how can I submit the ajax filter and still keep the chosen calendar view style? Thanks :-)

ras-ben’s picture

I'm not sure if it would help with your problem, but you should probably try to use $(context) instead of $(document) :)

robotjox’s picture

In case somebody wants to take this further here is my complete plugin that loads events as needed in day, week, month and list views.
It does so by manipulating an exposed date filter (which you can hide with CSS, so nobody notices).

This is probably not an ideal solution as the ajax loading of nodes takes some time each time you navigate the calendar, but it is the only way I could make this work, and without it the module was useless, because we have thousands of nodes.

I also hope somebody more knowledgeable in jquery can finetune this script - it is glued together through a lot of trial and error, but it works for me. I think the code can be shortened a lot by using functions, but I have very little time right now to fix this. It is what it is :-)

(function ($) {
  var d = new Date(); //set initial date to today
  d.setMonth(d.getMonth()); //get month
  Drupal.behaviors.sctmariae_calendar = {
    attach: function (context, settings) {
      $('.js-drupal-fullcalendar', context)
        .once("sctmariae_calendar")
        .each(function() {
          settings.calendar.forEach(function(calendar) {
            let viewSettings = settings.fullCalendarView[0];
            var calendarOptions = JSON.parse(viewSettings.calendar_options);
            calendar.gotoDate(d); //go to set date
           
            
            //disable back/next buttons on exposed filter submit to avoid fast clicking
            $(".js-form-submit").click(function(){
                $('button.fc-prev-button').prop("disabled",true);
                $('button.fc-next-button').prop("disabled",true);
            });
            
            if (settings.view === undefined){
              settings.view = "month"; //set settings.view to month
            } else if (settings.view === 'week'){
              calendar.changeView( 'timeGridWeek' ); //load week view if settings.view is week
            } else if (settings.view === 'month'){
              calendar.changeView( 'dayGridMonth' ); //load month view if settings.view is month
            } else if (settings.view === 'day'){
              calendar.changeView( 'timeGridDay' ); //load day view if settings.view is day
            } else if (settings.view === 'list'){
              calendar.changeView( 'listMonth' ); //load day view if settings.view is day
            }
            
            //click on week button
            $(context).once('weekclick').on('click', '.fc-timeGridWeek-button', function (event) {
              //disable the button 
	            $('button.fc-prev-button').prop("disabled",true);
              settings.view = "week"; //set settings.view to week
              var currentDay = new Date(d);
              var firstDayOfWeek = new Date(currentDay.setDate(currentDay.getDate() - currentDay.getDay()+1));
              var lastDayOfWeek = new Date(currentDay.setDate(currentDay.getDate() - currentDay.getDay()+7));
              firstDayOfWeek.setHours(0,0,0,0);
              lastDayOfWeek.setHours(23,59,0,0);
              var FormattedFromFilterDay = new Date(firstDayOfWeek.getTime() - (firstDayOfWeek.getTimezoneOffset() * 60000 ))
                .toISOString()
                .split("T")[0];
              var FormattedToFilterDay = new Date(lastDayOfWeek.getTime() - (lastDayOfWeek.getTimezoneOffset() * 60000 ))
                .toISOString()
                .split("T")[0];            
              $('[data-drupal-selector="edit-field-tid-value-min"]').val(FormattedFromFilterDay);
              $('[data-drupal-selector="edit-field-tid-value-max"]').val(FormattedToFilterDay);  
              $('input.js-form-submit').click();
              $('button.fc-prev-button').prop("disabled",true);             
            });
            
            //click on month button
            $(context).once('monthclick').on('click', '.fc-dayGridMonth-button', function (event) {
              settings.view = "month"; //set settings.view to month
              var FromFilterDay = new Date(d.getFullYear(), d.getMonth());
              var ToFilterDay = new Date(d.getFullYear(), d.getMonth() + 1, 0);
              var FormattedFromFilterDay = new Date(FromFilterDay.getTime() - (FromFilterDay.getTimezoneOffset() * 60000 ))
                .toISOString()
                .split("T")[0];
              var FormattedToFilterDay = new Date(ToFilterDay.getTime() - (ToFilterDay.getTimezoneOffset() * 60000 ))
                .toISOString()
                .split("T")[0];
              $('[data-drupal-selector="edit-field-tid-value-min"]').val(FormattedFromFilterDay);
              $('[data-drupal-selector="edit-field-tid-value-max"]').val(FormattedToFilterDay);  
              $('input.js-form-submit').click();            
            });
            
            //click on list button
            $(context).once('listclick').on('click', '.fc-listMonth-button', function (event) {
              settings.view = "list"; //set settings.view to list
              var FromFilterDay = new Date(d.getFullYear(), d.getMonth());
              var ToFilterDay = new Date(d.getFullYear(), d.getMonth() + 1, 0);
              var FormattedFromFilterDay = new Date(FromFilterDay.getTime() - (FromFilterDay.getTimezoneOffset() * 60000 ))
                .toISOString()
                .split("T")[0];
              var FormattedToFilterDay = new Date(ToFilterDay.getTime() - (ToFilterDay.getTimezoneOffset() * 60000 ))
                .toISOString()
                .split("T")[0];
              $('[data-drupal-selector="edit-field-tid-value-min"]').val(FormattedFromFilterDay);
              $('[data-drupal-selector="edit-field-tid-value-max"]').val(FormattedToFilterDay);  
              $('input.js-form-submit').click();            
            });
            
            //click on day button
            $(context).once('dayclick').on('click', '.fc-timeGridDay-button', function (event) {
              settings.view = "day"; //set settings.view to month
              var formatteddate = new Date(d.getTime() - (d.getTimezoneOffset() * 60000 ))
                    .toISOString()
                    .split("T")[0];
              $('[data-drupal-selector="edit-field-tid-value-min"]').val(formatteddate + " 00:00");
              $('[data-drupal-selector="edit-field-tid-value-max"]').val(formatteddate + " 23:59");  
              $('input.js-form-submit').click();            
            });
          
            //click on back button
            $(context).once('backbutton').on('click', '.fc-prev-button', function (event) { //on click on back/prev button
              //back button for week views
              if (settings.view === 'week'){ //if we are in week mode
                d.setDate(d.getDate() -  7); //subtract 7 days from current date
                var currentDay = new Date(d);
                var firstDayOfWeek = new Date(currentDay.setDate(currentDay.getDate() - currentDay.getDay()+1));
                var lastDayOfWeek = new Date(currentDay.setDate(currentDay.getDate() - currentDay.getDay()+7));
                firstDayOfWeek.setHours(0,0,0,0);
                lastDayOfWeek.setHours(23,59,0,0);
                              console.log(lastDayOfWeek);
                var FormattedFromFilterDay = new Date(firstDayOfWeek.getTime() - (firstDayOfWeek.getTimezoneOffset() * 60000 ))
                  .toISOString()
                  .split("T")[0];
                var FormattedToFilterDay = new Date(lastDayOfWeek.getTime() - (lastDayOfWeek.getTimezoneOffset() * 60000 ))
                  .toISOString()
                  .split("T")[0];            
                $('[data-drupal-selector="edit-field-tid-value-min"]').val(FormattedFromFilterDay + " 00:00");
                $('[data-drupal-selector="edit-field-tid-value-max"]').val(FormattedToFilterDay + " 23:59");  
                $('input.js-form-submit').click();
              } 
              
              //back button for month views
              if (settings.view === 'month' || settings.view == null){ //if we are in month mode
                d.setMonth(d.getMonth() - 1); //subtract one month from active date
                settings.view = "month"; //set settings.view to month
                var FromFilterDay = new Date(d.getFullYear(), d.getMonth());
                var ToFilterDay = new Date(d.getFullYear(), d.getMonth() + 1, 0);
                var FormattedFromFilterDay = new Date(FromFilterDay.getTime() - (FromFilterDay.getTimezoneOffset() * 60000 ))
                    .toISOString()
                    .split("T")[0];
                var FormattedToFilterDay = new Date(ToFilterDay.getTime() - (ToFilterDay.getTimezoneOffset() * 60000 ))
                    .toISOString()
                    .split("T")[0];
                $('[data-drupal-selector="edit-field-tid-value-min"]').val(FormattedFromFilterDay);
                $('[data-drupal-selector="edit-field-tid-value-max"]').val(FormattedToFilterDay);  
                $('input.js-form-submit').click();
              }
              
              //back button for month views
              if (settings.view === 'list'){ //if we are in list mode
                d.setMonth(d.getMonth() - 1); //subtract one month from active date
                var FromFilterDay = new Date(d.getFullYear(), d.getMonth());
                var ToFilterDay = new Date(d.getFullYear(), d.getMonth() + 1, 0);
                var FormattedFromFilterDay = new Date(FromFilterDay.getTime() - (FromFilterDay.getTimezoneOffset() * 60000 ))
                    .toISOString()
                    .split("T")[0];
                var FormattedToFilterDay = new Date(ToFilterDay.getTime() - (ToFilterDay.getTimezoneOffset() * 60000 ))
                    .toISOString()
                    .split("T")[0];
                $('[data-drupal-selector="edit-field-tid-value-min"]').val(FormattedFromFilterDay);
                $('[data-drupal-selector="edit-field-tid-value-max"]').val(FormattedToFilterDay);  
                $('input.js-form-submit').click();
              }     
              
              //back button for day view
              if (settings.view === 'day'){ //if we are in day mode
                d.setDate(d.getDate() - 1); //subtract 1 day from current date
                var formatteddate = new Date(d.getTime() - (d.getTimezoneOffset() * 60000 ))
                    .toISOString()
                    .split("T")[0];
                $('[data-drupal-selector="edit-field-tid-value-min"]').val(formatteddate + " 00:00");
                $('[data-drupal-selector="edit-field-tid-value-max"]').val(formatteddate + " 23:59");  
                $('input.js-form-submit').click();
              } 
            });
            
            //click on next button
            $(context).once('nextbutton').on('click', '.fc-next-button', function (event) {
            
              //next button for week views
              if (settings.view === 'week'){ //if we are in week mode
                var day = d.getDay(), diff = d.getDate() - day + (day == 0 ? -6:1); // adjust when day is sunday
                var firstdayofcurrentweek = new Date(d.setDate(diff)); //get first day of current week
                d.setDate(firstdayofcurrentweek.getDate() +  7); //add 7 days to current first day of week
                var currentDay = new Date(d);
                var firstDayOfWeek = new Date(currentDay.setDate(currentDay.getDate() - currentDay.getDay()+1));
                var lastDayOfWeek = new Date(currentDay.setDate(currentDay.getDate() - currentDay.getDay()+7));
                firstDayOfWeek.setHours(0,0,0,0);
                lastDayOfWeek.setHours(23,59,0,0);
                var FormattedFromFilterDay = new Date(firstDayOfWeek.getTime() - (firstDayOfWeek.getTimezoneOffset() * 60000 ))
                  .toISOString()
                  .split("T")[0];
                var FormattedToFilterDay = new Date(lastDayOfWeek.getTime() - (lastDayOfWeek.getTimezoneOffset() * 60000 ))
                  .toISOString()
                  .split("T")[0];            
                $('[data-drupal-selector="edit-field-tid-value-min"]').val(FormattedFromFilterDay);
                $('[data-drupal-selector="edit-field-tid-value-max"]').val(FormattedToFilterDay);  
                $('input.js-form-submit').click();
              }
              
              //next button for month views
              if (settings.view === 'month' || settings.view == null){ //if we are in month mode
                d.setMonth(d.getMonth() + 1); //add one month to current month
                settings.view = "month"; //set settings.view to month
                var FromFilterDay = new Date(d.getFullYear(), d.getMonth());
                var ToFilterDay = new Date(d.getFullYear(), d.getMonth() + 1, 0);
                var FormattedFromFilterDay = new Date(FromFilterDay.getTime() - (FromFilterDay.getTimezoneOffset() * 60000 ))
                    .toISOString()
                    .split("T")[0];
                var FormattedToFilterDay = new Date(ToFilterDay.getTime() - (ToFilterDay.getTimezoneOffset() * 60000 ))
                    .toISOString()
                    .split("T")[0];
                $('[data-drupal-selector="edit-field-tid-value-min"]').val(FormattedFromFilterDay);
                $('[data-drupal-selector="edit-field-tid-value-max"]').val(FormattedToFilterDay);  
                $('input.js-form-submit').click();
              }
              
              if (settings.view === 'list'){ //if we are in list mode
                d.setMonth(d.getMonth() + 1); //subtract one month from active date
                var FromFilterDay = new Date(d.getFullYear(), d.getMonth());
                var ToFilterDay = new Date(d.getFullYear(), d.getMonth() + 1, 0);
                var FormattedFromFilterDay = new Date(FromFilterDay.getTime() - (FromFilterDay.getTimezoneOffset() * 60000 ))
                    .toISOString()
                    .split("T")[0];
                var FormattedToFilterDay = new Date(ToFilterDay.getTime() - (ToFilterDay.getTimezoneOffset() * 60000 ))
                    .toISOString()
                    .split("T")[0];
                $('[data-drupal-selector="edit-field-tid-value-min"]').val(FormattedFromFilterDay);
                $('[data-drupal-selector="edit-field-tid-value-max"]').val(FormattedToFilterDay);  
                $('input.js-form-submit').click();
              } 
              
              //next button for day view
              if (settings.view === 'day'){ //if we are in day mode
                d.setDate(d.getDate() + 1); //subtract 1 day from current date
                var formatteddate = new Date(d.getTime() - (d.getTimezoneOffset() * 60000 ))
                    .toISOString()
                    .split("T")[0];
                $('[data-drupal-selector="edit-field-tid-value-min"]').val(formatteddate + " 00:00");
                $('[data-drupal-selector="edit-field-tid-value-max"]').val(formatteddate + " 23:59");  
                $('input.js-form-submit').click();
              }
              
            });
            
            //click on today button
            $(context).once('todayclick').on('click', '.fc-today-button', function (event) {
              d = new Date(); //set date back to today
              //for today button I load the whole month - to be fixed
              var FromFilterDay = new Date(d.getFullYear(), d.getMonth());
              var ToFilterDay = new Date(d.getFullYear(), d.getMonth() + 1, 0);
              var FormattedFromFilterDay = new Date(FromFilterDay.getTime() - (FromFilterDay.getTimezoneOffset() * 60000 ))
                .toISOString()
                .split("T")[0];
              var FormattedToFilterDay = new Date(ToFilterDay.getTime() - (ToFilterDay.getTimezoneOffset() * 60000 ))
                .toISOString()
                .split("T")[0];
              $('[data-drupal-selector="edit-field-tid-value-min"]').val(FormattedFromFilterDay);
              $('[data-drupal-selector="edit-field-tid-value-max"]').val(FormattedToFilterDay);  
              $('input.js-form-submit').click(); 
            });
        })
    })
    }
  }
})(jQuery);

EDIT: Added some more info and fixed a few omissions.

Mingsong’s picture

FYI: The Ajax filter issue has been fixed with 5.x-dev.

See #3136683: Allow ajax to be used for filters in ^4.2

DustinYoder’s picture

Can you explain how to configure this to work? Do we need to add some date filters or something to the view? I can't seem to get this working. On latest 5 release.

Mingsong’s picture

As this post still active and is a new feature which hasn't been implemented yet.

So No configuration available for it.

The issue mentioned in #39 is different issue related to Ajax loading, but is not the new feature required.

DustinYoder’s picture

Ok, thanks. Would these hidden exposed filter fields be added programmatically with a view alter or how do you envision this working? I would be pretty happy to help link the next and prev buttons to those exposed filters and trigger the AJAX. I also wonder if anyone knows how to force the full calendar to show the correct month/week/day once the AJAX completes? Thanks

Agogo’s picture

I want to chime in on a solution that I made to resolve the problem with slow loading times on a site with thousands of date values/entities. Inspired by @robotjox above.

Like previous authors in this thread I made exposed filters with from and to dates that I later just hid with CSS (display:none). From date >= and To date <=. Both as machine date format (CCYY-MM-DD HH:MM:SS).

Ajax enabled.

You might have to check your date values for localization.

I hope it helps someone:

  Drupal.behaviors.mymodule = {
    attach: function (context, settings) {
      $(document).ready(function () {
        $('.js-drupal-fullcalendar', context)
          .once('mymodule')
          .each(function() {

            let calendarEl = this;
            let viewIndex = parseInt(calendarEl.getAttribute("calendar-view-index"));

            $(context).once('buttonevent').on('click', '.fc-button-primary', function (event) {
              var cd = settings.calendar[viewIndex].state.dateProfile.currentRange.start;
              var cv = settings.calendar[viewIndex].state.viewType;

              var fd = new Date(cd.toISOString());
              var td = new Date(cd.toISOString());

              if (cv === 'listYear') {
                td.setFullYear(td.getFullYear() + 1);
                td.setMonth(1);
                td.setDate(1);
              } else if (cv === 'dayGridMonth' || cv === 'listMonth') {
                td.setMonth(td.getMonth() + 1);
                td.setDate(1);
              } else if (cv === 'timeGridWeek' || cv === 'listWeek') {
                td.setDate(td.getDate() + 7);
              } else if (cv === 'timeGridDay' || cv === 'listDay') {
                td.setDate(td.getDate() + 1);
              }
              setSubmit(fd, td);
            });

          })
      });
      function setSubmit(fd, td) {
        let df = $('[data-drupal-selector="edit-df"]');
        let dt = $('[data-drupal-selector="edit-dt"]');

        fd.setHours(0,0,0,0);
        td.setHours(0,0,0,0);

        df.val(fd.toISOString());
        dt.val(td.toISOString());

        $('input.js-form-submit').click();
      }
    }
  }
markusa’s picture

Some really great code snippets here ... been trying code from comment 35 .. a few minor modifications.

The problem I have, is that when the submit button is submitted via javascript .. the calendar still resets to the current month.

I have Ajax enabled in the View .. it is updating without a page reload.

Was there some other thing necessary for when I hit the "next" button, and the exposed filters are updated, and apply hit .. that the calendar will not reset to be the current month?

On first page load, I do want the current month to be what loads, just not after changing the exposed filters and the ajax refresh.

Much thanks in advance

markusa’s picture

To get the calendar to refresh via ajax on the month you are filtering on
Modifying the code in 35

(function ($) {
  var d = new Date();
  d.setMonth(d.getMonth());
  Drupal.behaviors.mymodule = {
    attach: function (context, settings) {
      if (typeof(settings.calendar) != 'undefined') {
      settings.calendar.forEach(function(calendar) {
        // set the month for the calendar display on reload
       // Next 3 lines I needed to add
        fullCalendarobj = JSON.parse(settings.fullCalendarView[0].calendar_options);
        fullCalendarobj.defaultDate = d;
        settings.fullCalendarView[0].calendar_options = JSON.stringify(fullCalendarobj);
Mingsong’s picture

I just created a new module FullCalendar Block which uses the FullCalendar 5.

It is a simple module that uses a block to display the calendar rather than a view. Unlike the FullCalendar View module, that module need a JSON feed via a URL as the event data source. It supports loading events via Ajax once needed.

aarondouyere’s picture

Hi Mingsong, I'm using the full calendar view plugin on my site and Ive created the view as a block and placed in on several pages. As the page was timing out I had to limit the number of calendar posts to display. Can you please advise on how I can use the Full Calendar Block module for my use case.

Mingsong’s picture

Hi Aaron Douyere, FullCalendar Block module will only load calendar entries (posts) for current view (month, week or day) by default. It won't load any other entries that don't appear in current view.
Regarding how to use that module, please have a look at the module page or the readme file https://git.drupalcode.org/project/fullcalendar_block/-/blob/1.0.x/READM...

kazah’s picture

I'm trying to use the code from #38 or #43, but I need to set a relative date. Otherwise, the calendar does not display any events.

If I convert date to moment.js format, then when displaying the current month, it will be 'a month' and so on. With this value exposed filters won't work.

Does anyone have a solution?

kazah’s picture

I'm grateful to everyone who posted their solutions...here's mine.

Initial data:

  • Used bootstrap with fullcalendar v4
  • Created two exposed filters:
  • 1. with relative dates (between): min (-1 month), max (+1 month).
  • 2. fixed dates (between): min (empty), max (empty).
  • The default view in the calendar is dayGridMonth.

I only need: dayGridMonth, listMonth and listYear
Also transitions: prev, next and today.

The main problem was: for some reason Drupal.attachBehaviors is loaded before the calendar.
I saw the solution here: https://www.drupal.org/project/fullcalendar_view/issues/3214419 - but it didn't help me :)

It was possible to use setTimeout, but also with varying success, so I used the solution that was in the calendar itself:

  // document.ready event does not work with BigPipe.
  // The workaround is to ckeck the document state
  // every 100 milliseconds until it is completed.
  // @see https://www.drupal.org/project/drupal/issues/2794099#comment-13274828
  var checkReadyState = setInterval(function() {
    if (
        document.readyState === "complete" &&
        $('.js-drupal-fullcalendar').length > 0
        ) {
      clearInterval(checkReadyState);
      // Build calendar objects.
      buildCalendars();
    }
  }, 100);

I could manage to solve the problem of reseting to default view after the update. There was a small curve transition to the desired view and date, but this is not a problem at all. If anyone has a solution I would appreciate seeing it.

Here is my code:

// Update calendar on ajax
var d = new Date(); // set initial date to today
d.setMonth(d.getMonth()); // get month
if (window.innerWidth < 992) {
	var cv = 'listMonth'; // set defaultview for mobile
}
else {
	var cv = 'dayGridMonth'; // set defaultview for pc
}
Drupal.behaviors.CustomAjax = {
	attach: function (context, settings) {
	
	// need to wait while calendar exist
	var checkReadyState = setInterval(function() {
		if (document.readyState === "complete" && $('.js-drupal-fullcalendar').length > 0) {
			$('.ajax-progress').fadeOut("slow", this.remove); // remove loading icon
			clearInterval(checkReadyState);
			// build calendar
			$('.js-drupal-fullcalendar', context).once('CustomAjax').each(function() {	
				settings.calendar.forEach(function(calendar) {

					// main transitions
					calendar.gotoDate(d);
					calendar.changeView(cv);

					// check if any button click
					$('.fc-toolbar .btn', context).once('myButtons').on('click', function (event) {
					
						var cd = settings.calendar[0].state.dateProfile.currentRange.start;
						cv = settings.calendar[0].state.viewType;
						var fd = new Date(cd.toISOString());
						var td = new Date(cd.toISOString());				
					
						if (cv === 'listYear') {
							td.setFullYear(td.getFullYear() + 1);
							td.setMonth(0);
							td.setDate(1);
							d.setFullYear(d.getFullYear());							
							d.setMonth(0);
							d.setDate(1);								
							if ($(this).hasClass('fc-prev-button')) {
								d.setFullYear(d.getFullYear() - 1);							
								d.setMonth(0);
								d.setDate(1);
							}
							else if ($(this).hasClass('fc-next-button')) {
								d.setFullYear(d.getFullYear() + 1);							
								d.setMonth(0);
								d.setDate(1);								
							}
							else if ($(this).hasClass('fc-today-button')) {								
								d = new Date(); // set to current date
							};							
						}
						else if (cv === 'dayGridMonth' || cv === 'listMonth') {							
							fd.setMonth(fd.getMonth());
							fd.setDate(fd.getDate() - 6);
							td.setMonth(td.getMonth() + 1);
							td.setDate(td.getDate() + 6);
							d.setMonth(d.getMonth());
							d.setDate(1);
							if ($(this).hasClass('fc-prev-button')) {
								d.setMonth(d.getMonth() - 1);
								d.setDate(1);
							}
							else if ($(this).hasClass('fc-next-button')) {
								d.setMonth(d.getMonth() + 1);
								d.setDate(1);									
							}
							else if ($(this).hasClass('fc-today-button')) {
								d = new Date();  // set to current date
							};							
						};					
						
						setSubmit(fd, td);
						
					});
						
				})
			})
			function setSubmit(fd, td) {
				$('#edit-reldate-min, #edit-reldate-max').val(''); // remove relative dates filter
				var df = $('[data-drupal-selector="edit-date-min"]');
				var dt = $('[data-drupal-selector="edit-date-max"]');

				// need UTC +3 hours 
				fd.setHours(3);
				td.setHours(3);

				df.val(fd.toISOString());
				dt.val(td.toISOString());

				$('.view-id-calendar .js-form-submit').trigger('click');				
			}				
		}
		else {
			// set loading icon before calendar exist
			$('body').append('<div class="ajax-progress ajax-progress-fullscreen"></div>');	
		}
	}, 100);
	}
};	
kazah’s picture

UPDATE

To get rid of jagged transitions I need to add the code below: (right after attach: function (context, settings) {)

if (drupalSettings.fullCalendarView !== undefined && drupalSettings.fullCalendarView[0]['calendar_options'] !== undefined) {
	let calendarOptions = JSON.parse(drupalSettings.fullCalendarView[0]['calendar_options']);
	calendarOptions['defaultDate'] = d;
	calendarOptions['defaultView'] = cv;
	drupalSettings.fullCalendarView[0]['calendar_options'] = JSON.stringify(calendarOptions);
}
junaidpv’s picture

Status: Active » Needs work
FileSize
32.88 KB

I could make this work with the given code changes in the given patch.

I made this possible by utilizing the feature of the Fullcalendar by providing events a JSON feed as documented at https://fullcalendar.io/docs/v4/events-json-feed

The idea is to generate a list of events as JSON feed by executing that particular view itself. A new route named 'fullcalendar_view.event_source' has been created for this. This new route and its controller has been modeled after the builtin Ajax feature of the views. Instead of rendering the entire HTML for the view, it just provides JSON for the list of events.

The Fullcalendar itself will pass the 'start' and 'end' for the timestamps to be used for filtering the events to be returned.

Now, the events are not supposed to be loaded along with the page load as events now on will be always loaded over Ajax. So, I have implemented hook_views_post_build() to fetch only one entry from the database when the view is being executed in the initial page build. (In fact we don't need to load any entry at all, but I see we can't set to load 0 rows in SQL query.

The controller action CalendarEventSourceController::ajaxView() will dynamically set the filtering for datetime as per the 'start' and 'end' parameters being passed by the Fullcalendar.

How to use the patch

  1. Apply patch to the latest version of the fullcalnedar_view module. (Tested with version 5.1.7)
  2. On the view configuration page. Open 'Settings' for "Full Calendar Display". Set "Date Field for Filtering" with the date field to be used for the filtering.

Note: This patch may need some minor improvements if your events contents has two separate datetime fields for 'start' and 'end' of the events.

TODO

Right now it does not work with the changes in exposed form field values. I already added code to pass the exposed field values to the view similar to how the Ajax feature of the Views module works. But somehow, it is not working as I expected.

junaidpv’s picture

Status: Needs work » Needs review
FileSize
33.25 KB

I could improve the patch I posted in #52. Now it works with views exposed forms. Here is the updated patch.

junaidpv’s picture

I missed to use color option settings in my earlier patches. Here is the one having that corrected.

joshuautley’s picture

Tried using patch #54 and ran into an issue using SmartDate. Has anyone else run into this issue?

The website encountered an unexpected error. Please try again later.
TypeError: Argument 1 passed to Drupal\smart_date\Plugin\FullcalendarViewProcessor\SmartDateProcessor::getIdMappings() must be of the type array, null given, called in /home/swbqszdz/public_html/modules/contrib/smart_date/src/Plugin/FullcalendarViewProcessor/SmartDateProcessor.php on line 53 in Drupal\smart_date\Plugin\FullcalendarViewProcessor\SmartDateProcessor->getIdMappings() (line 94 of modules/contrib/smart_date/src/Plugin/FullcalendarViewProcessor/SmartDateProcessor.php).

Drupal\smart_date\Plugin\FullcalendarViewProcessor\SmartDateProcessor->getIdMappings(NULL) (Line: 53)
Drupal\smart_date\Plugin\FullcalendarViewProcessor\SmartDateProcessor->process(Array) (Line: 39)
template_preprocess_views_view_fullcalendar(Array, 'views_view_fullcalendar', Array)
call_user_func_array('template_preprocess_views_view_fullcalendar', Array) (Line: 287)
Drupal\Core\Theme\ThemeManager->render('views_view_fullcalendar', Array) (Line: 422)
Drupal\Core\Render\Renderer->doRender(Array) (Line: 435)
Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 201)
Drupal\Core\Render\Renderer->render(Array) (Line: 479)
Drupal\Core\Template\TwigExtension->escapeFilter(Object, Array, 'html', NULL, 1) (Line: 110)
__TwigTemplate_8e7f12e2a291d2863492e4f0fad6a1d2982af8828348a229bc45ce46a7d7c81a->doDisplay(Array, Array) (Line: 405)
Twig\Template->displayWithErrorHandling(Array, Array) (Line: 378)
Twig\Template->display(Array) (Line: 390)
Twig\Template->render(Array) (Line: 55)
twig_render_template('themes/contrib/bootstrap/templates/views/views-view.html.twig', Array) (Line: 384)
Drupal\Core\Theme\ThemeManager->render('views_view', Array) (Line: 422)
Drupal\Core\Render\Renderer->doRender(Array) (Line: 435)
Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 201)
Drupal\Core\Render\Renderer->render(Array, ) (Line: 241)
Drupal\Core\Render\MainContent\HtmlRenderer->Drupal\Core\Render\MainContent\{closure}() (Line: 564)
Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 242)
Drupal\Core\Render\MainContent\HtmlRenderer->prepare(Array, Object, Object) (Line: 132)
Drupal\Core\Render\MainContent\HtmlRenderer->renderResponse(Array, Object, Object) (Line: 90)
Drupal\Core\EventSubscriber\MainContentViewSubscriber->onViewRenderArray(Object, 'kernel.view', Object)
call_user_func(Array, Object, 'kernel.view', Object) (Line: 142)
Drupal\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch(Object, 'kernel.view') (Line: 164)
Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 81)
Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 58)
Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 48)
Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 106)
Drupal\page_cache\StackMiddleware\PageCache->pass(Object, 1, 1) (Line: 85)
Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 50)
Drupal\ban\BanMiddleware->handle(Object, 1, 1) (Line: 48)
Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 51)
Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23)
Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 709)
Drupal\Core\DrupalKernel->handle(Object) (Line: 19)

aeski’s picture

I'm getting the same as joshuautley using those patches.

Jelle_S’s picture

This patch does not call all FullcalendarViewProcessor plugins when loading the data (we use a plugin to alter the color of the events based on some properties)

Rahaf Albawab’s picture

I'm getting the same as joshuautley when using those patches.

kazah’s picture

My code working only for 5.1.2

Qusai Taha’s picture

FileSize
35.13 KB

Re-roll the patch to make it work with the smart date module and smartdate_default field formmater.

Qusai Taha’s picture

FileSize
35.13 KB
COBadger’s picture

Status: Needs review » Reviewed & tested by the community

I tested the patch in comment 61 against version 5.1.8 and it worked really well - bravo! Much better user experience.

Mingsong’s picture

Thanks everyone for working or contributing your thought on this issue.

Since there are a lots changes and this move will require more testing, I created a new branch 5.2.x for it and a MR for this issue.

You can use following URL to get the patch from the MR.

https://git.drupalcode.org/project/fullcalendar_view/-/merge_requests/44...

The main concern I am having for the codes suggested is security. I will raise my concern on the MR for further discussion.

It is a significant change and involved a lots of works. I really appreciate your work and thought on such important feature for some sites in which there are a lots of events which are unnecessary to load at once.

Mingsong’s picture

Status: Reviewed & tested by the community » Needs work

There are some unsolved issues (see the MR), so it needs more works.

someshver’s picture

Getting issue after applying the patch #61.

TypeError: Drupal\smart_date\Plugin\FullcalendarViewProcessor\SmartDateProcessor::getIdMappings(): Argument #1 ($entries) must be of type array, null given, called in C:\Users\FWS-22-3\PHPstorm Projects\P-Test\P-D9\drupal\web\modules\contrib\smart_date\src\Plugin\FullcalendarViewProcessor\SmartDateProcessor.php on line 53 in Drupal\smart_date\Plugin\FullcalendarViewProcessor\SmartDateProcessor->getIdMappings() (line 94 of modules\contrib\smart_date\src\Plugin\FullcalendarViewProcessor\SmartDateProcessor.php).

I am using Smart date with Smart Date Recurring module.

maelcorm’s picture

Same error with patch #61

Drupal\smart_date\Plugin\FullcalendarViewProcessor\SmartDateProcessor::getIdMappings(): Argument #1 ($entries) must be of type array, null given

mohammedOdeh’s picture

FileSize
34.36 KB

Reroll the patch to version 5.1.11

mohammedOdeh’s picture

FileSize
54.3 KB

Reroll the patch to version 5.1.11

mohammedOdeh’s picture

FileSize
40.55 KB

Reroll the patch to version 5.1.11

Mingsong’s picture

Status: Needs work » Closed (outdated)

I will put this module into "Bug fixing only' status soon.

A feature like this one requires a lots testing, won't be add to future release of this module.

I recommend using Fullcalendar Block module instead.

mohammedOdeh’s picture

FileSize
36.98 KB

Reroll the patch to version 5.1.12

mohammedOdeh’s picture

FileSize
36.38 KB

Reroll the patch to version 5.1.12

fallenturtle’s picture

Was it ever identified what the "5" in the readme picture was for? I'm having an issue where I'm getting a white screen of death with full calendar, but if I limit the results number, it'll start working again. What's the expected value to be used here? I always assumed show all... but maybe that's what's breaking my view.

mohammedOdeh’s picture

FileSize
36.58 KB

Reroll the patch to version 5.1.13

junaidpv’s picture

We tried using the Fullcalendar Block instead of using this patch. But that module cannot be used as a replacement, especially when we want to have views exposed form. Also we need to configure it in two places, and configuring the date filter is confusing.

As the patch got rejected here. We decide to create a separate module to have this feature. It is Fullcalendar Dynamic. It is mostly a fork of the fullcalendar_view module. Please have look and please feel free to submit patches to fix any issues you encounter.

alex.tiupa’s picture

FileSize
36.6 KB

Updated patch from #74. I made some JS strings translatable.

alex.tiupa’s picture

FileSize
30.34 KB

...

alex.tiupa’s picture

FileSize
36.38 KB

My bad, I fixed the mistake from previous patches.