From 475fd16e695983e1eb163646f9ffdf7dd55a13a0 Mon Sep 17 00:00:00 2001 From: wychoong <67364036+wychoong@users.noreply.github.com> Date: Wed, 29 Jun 2022 15:50:26 +0800 Subject: [PATCH 1/5] feat: lazy loading the events data --- resources/views/fullcalendar.blade.php | 29 +++++++++++++++++++++++--- src/Widgets/Concerns/UsesLazyLoad.php | 13 ++++++++++++ src/Widgets/Contracts/LazyLoading.php | 8 +++++++ src/Widgets/FullCalendarWidget.php | 4 +++- 4 files changed, 50 insertions(+), 4 deletions(-) create mode 100644 src/Widgets/Concerns/UsesLazyLoad.php create mode 100644 src/Widgets/Contracts/LazyLoading.php diff --git a/resources/views/fullcalendar.blade.php b/resources/views/fullcalendar.blade.php index 42ec4aa..af057e7 100644 --- a/resources/views/fullcalendar.blade.php +++ b/resources/views/fullcalendar.blade.php @@ -18,7 +18,7 @@ x-init=' document.addEventListener("DOMContentLoaded", function() { const config = @json($this->getConfig()); - const events = @json($events); + const eventsData = @json($events); const locale = "{{ $locale }}"; const eventClick = function ({ event, jsEvent }) { @@ -39,12 +39,35 @@ @endif } + var initial = true; + const calendar = new FullCalendar.Calendar($el, { ...config, locale, - events, eventClick, - eventDrop + eventDrop, + @if($this->isLazyLoad()) + events: function(fetchInfo, successCallback, failureCallback) { + if(initial){ + initial = false + successCallback(eventsData) + }else{ + return $wire.lazyLoadViewData(fetchInfo) + } + }, + @else + events: eventsData, + @endif + @if( $this::canCreate() ) + dateClick: function(info){ + $wire.onCreateEventClick(info) + }, + @if($this->config('selectable', false)) + select: function(info){ + $wire.onCreateEventClick(info) + }, + @endif + @endif }); calendar.render(); diff --git a/src/Widgets/Concerns/UsesLazyLoad.php b/src/Widgets/Concerns/UsesLazyLoad.php new file mode 100644 index 0000000..dc20224 --- /dev/null +++ b/src/Widgets/Concerns/UsesLazyLoad.php @@ -0,0 +1,13 @@ +with([ - 'events' => $this->getViewData(), + 'events' => $this->isLazyLoad() ? $this->lazyLoadViewData() : $this->getViewData(), ]); } } From 03557ba9fff2dc0e8f67b422a3cd15f039013462 Mon Sep 17 00:00:00 2001 From: wychoong <67364036+wychoong@users.noreply.github.com> Date: Wed, 29 Jun 2022 16:03:33 +0800 Subject: [PATCH 2/5] Update fullcalendar.blade.php --- resources/views/fullcalendar.blade.php | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/resources/views/fullcalendar.blade.php b/resources/views/fullcalendar.blade.php index af057e7..31494ea 100644 --- a/resources/views/fullcalendar.blade.php +++ b/resources/views/fullcalendar.blade.php @@ -58,16 +58,6 @@ @else events: eventsData, @endif - @if( $this::canCreate() ) - dateClick: function(info){ - $wire.onCreateEventClick(info) - }, - @if($this->config('selectable', false)) - select: function(info){ - $wire.onCreateEventClick(info) - }, - @endif - @endif }); calendar.render(); From efb24f2e8fc9cd1fff7626fbb5fcd6553ce22a8f Mon Sep 17 00:00:00 2001 From: wychoong <67364036+wychoong@users.noreply.github.com> Date: Wed, 29 Jun 2022 19:02:55 +0800 Subject: [PATCH 3/5] cache events data to avoid flickering when change month --- resources/views/fullcalendar.blade.php | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/resources/views/fullcalendar.blade.php b/resources/views/fullcalendar.blade.php index 31494ea..bc9637b 100644 --- a/resources/views/fullcalendar.blade.php +++ b/resources/views/fullcalendar.blade.php @@ -20,6 +20,9 @@ const config = @json($this->getConfig()); const eventsData = @json($events); const locale = "{{ $locale }}"; + @if($this->isLazyLoad()) + const cachedEventIds = []; + @endif const eventClick = function ({ event, jsEvent }) { if( event.url ) { @@ -50,9 +53,24 @@ events: function(fetchInfo, successCallback, failureCallback) { if(initial){ initial = false + + if(eventsData[0]?.id){ + eventsData.forEach((event) => cachedEventIds.push(event.id)) + } + successCallback(eventsData) }else{ - return $wire.lazyLoadViewData(fetchInfo) + $wire.lazyLoadViewData(fetchInfo) + .then(result => { + if(result.length == 0) return + + if(result[0].id){ + result.forEach((event) => cachedEventIds.indexOf(event.id) != -1 ? null : cachedEventIds.push(event.id) && eventsData.push(event)) + successCallback(eventsData) + }else{ + successCallback(result) + } + }) } }, @else From 7b7a8c2d6abb7da7554225485ae5cd51a6ef72ee Mon Sep 17 00:00:00 2001 From: wychoong <67364036+wychoong@users.noreply.github.com> Date: Thu, 30 Jun 2022 23:23:28 +0800 Subject: [PATCH 4/5] lazyload: refresh after edit --- resources/views/fullcalendar.blade.php | 8 +++++++- src/Widgets/Concerns/CanRefreshEvents.php | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/resources/views/fullcalendar.blade.php b/resources/views/fullcalendar.blade.php index bc9637b..c158eb9 100644 --- a/resources/views/fullcalendar.blade.php +++ b/resources/views/fullcalendar.blade.php @@ -82,7 +82,13 @@ window.addEventListener("filament-fullcalendar:refresh", (event) => { calendar.removeAllEvents(); - event.detail.data.map(event => calendar.addEvent(event)); + @unless($this->isLazyLoad()) + event.detail.data.map(event => calendar.addEvent(event)); + @else + cachedEventIds.splice(0, cachedEventIds.length) + calendar.refetchEvents() + eventsData.splice(0, eventsData.length) + @endunless }); }) '> diff --git a/src/Widgets/Concerns/CanRefreshEvents.php b/src/Widgets/Concerns/CanRefreshEvents.php index f7a9dc7..8770f03 100644 --- a/src/Widgets/Concerns/CanRefreshEvents.php +++ b/src/Widgets/Concerns/CanRefreshEvents.php @@ -6,6 +6,6 @@ trait CanRefreshEvents { protected function refreshEvents(): void { - $this->dispatchBrowserEvent('filament-fullcalendar:refresh', ['data' => $this->getViewData()]); + $this->dispatchBrowserEvent('filament-fullcalendar:refresh', $this->isLazyLoad() ? null : ['data' => $this->getViewData()]); } } From 0bfa7982424cf2733073a0d79e1e729d9da60542 Mon Sep 17 00:00:00 2001 From: Saade Date: Fri, 1 Jul 2022 03:35:30 -0300 Subject: [PATCH 5/5] wip --- README.md | 79 +++++++++++++++++++++-- resources/views/fullcalendar.blade.php | 68 ++++++++----------- src/Widgets/Concerns/CanFetchEvents.php | 25 +++++++ src/Widgets/Concerns/CanRefreshEvents.php | 2 +- src/Widgets/Concerns/UsesLazyLoad.php | 13 ---- src/Widgets/FullCalendarWidget.php | 6 +- 6 files changed, 130 insertions(+), 63 deletions(-) create mode 100644 src/Widgets/Concerns/CanFetchEvents.php delete mode 100644 src/Widgets/Concerns/UsesLazyLoad.php diff --git a/README.md b/README.md index 6959137..83dab11 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,9 @@ use Saade\FilamentFullCalendar\Widgets\FullCalendarWidget; class CalendarWidget extends FullCalendarWidget { - + /** + * Return events that should be rendered statically on calendar first render. + */ public function getViewData(): array { return [ @@ -84,10 +86,22 @@ class CalendarWidget extends FullCalendarWidget ] ]; } + + /** + * FullCalendar will call this function whenever it needs new event data. + * This is triggered when the user clicks prev/next or switches views on the calendar. + */ + public function fetchEvents(array $fetchInfo, array $ignorableIds): array + { + // You can use $fetchInfo to filter events by date, and $ignorableIds to ignore already displayed events. + return []; + } } ``` -> You should return an array of FullCalendar [EventObject](https://fullcalendar.io/docs/event-object). +You can use one or both methods to fetch events. + +> Both methods should retun an array of [EventObject](https://fullcalendar.io/docs/event-object).
@@ -132,7 +146,7 @@ return [ # Listening for events -The only events supported right now are: [EventClick](https://fullcalendar.io/docs/eventClick) and [EventDrop](https://fullcalendar.io/docs/eventDrop) +The only event-related events supported right now are: [EventClick](https://fullcalendar.io/docs/eventClick) and [EventDrop](https://fullcalendar.io/docs/eventDrop) They're commented out by default so livewire does not spam requests without they being used. You are free to paste them in your `CalendarWidget` class. See: [FiresEvents](https://github.com/saade/filament-fullcalendar/blob/main/src/Widgets/Concerns/FiresEvents.php) @@ -171,7 +185,14 @@ The process of saving and editing the event is up to you, since this plugin does ## Creating Events: -Override the `createEvent` function in your widget class, and you are ready to go! +Events can be created in two ways. + +- Clicking on a day (default) +- Selecting a date range (click and drag across calendar days) (you need to opt-in for this, set `selectable => true` in the config file.) + +This will open the Create Event modal. + +When the create form gets submitted, it will call the `createEvent` function on your widget. Be sure to add the snippet below to your calendar class. ```php public function createEvent(array $data): void @@ -200,7 +221,11 @@ protected static function getCreateEventFormSchema(): array ## Editing Events: -Override the `editEvent` function in your widget class, and you are ready to go! +Events can be edited by clicking on an event on the calendar. + +This will open the Edit Event modal. + +When the edit form gets submitted, it will call the `editEvent` function on your widget. Be sure to add the snippet below to your calendar class. ```php public function editEvent(array $data): void @@ -271,6 +296,50 @@ public function yourMethod(): void
+# Filtering events based on the calendar view + +If you want to filter your events based on the days that are currently shown in the calendar, you can implement the `fetchInfo()` method from the [CanFetchEvents](https://github.com/saade/filament-fullcalendar/blob/main/src/Widgets/Concerns/CanFetchEvents.php) trait. Add the following code to your calendar widget: + +```php +/** + * FullCalendar will call this function whenever it needs new event data. + * This is triggered when the user clicks prev/next or switches views. + * + * @see https://fullcalendar.io/docs/events-function + * @param array $fetchInfo start and end date of the current view + * @param array $ignorableIds ids of the events that are already loaded and should be ignored + */ +public function fetchEvents(array $fetchInfo, array $ignorableIds): array +{ + return []; +} +``` + +you can filter events based on the timespan `$fetchInfo['start']` and `$fetchInfo['end']`. +> **Warning** +> +> Keep in mind that returning events that are already in the calendar, can cause duplicates. You should filter them out using the `$ignorableIds` ids. + +example: +```php +public function fetchEvents(array $fetchInfo, array $ignorableIds): array +{ + $schedules = Appointment::query() + ->where([ + ['start_at', '>=', $fetchInfo['start']], + ['end_at', '<', $fetchInfo['end']], + ]) + ->whereNotIn('id', $ignorableIds) + ->get(); + + $data = $schedules->map( ... ); + + return $data; +} +``` + +
+ ## Testing ```bash diff --git a/resources/views/fullcalendar.blade.php b/resources/views/fullcalendar.blade.php index 8daae89..032f68e 100644 --- a/resources/views/fullcalendar.blade.php +++ b/resources/views/fullcalendar.blade.php @@ -1,4 +1,4 @@ -@php($locale = strtolower(str_replace('_', '-', $this->getConfig()['locale']))) +@php($locale = strtolower(str_replace('_', '-', $this->config('locale', config('app.locale'))))) @@ -7,13 +7,12 @@ x-data="" x-init=' document.addEventListener("DOMContentLoaded", function() { - var initial = true; const config = @json($this->getConfig()); - const eventsData = @json($events); const locale = "{{ $locale }}"; - @if($this->isLazyLoad()) - const cachedEventIds = []; - @endif + const events = @json($events); + const cachedEventIds = [ + ...events.map(event => event.id), + ]; const eventClick = function ({ event, jsEvent }) { if( event.url ) { @@ -45,6 +44,21 @@ @endif } + const fetchEvents = function ({ start, end }, successCallback, failureCallback) { + @if( $this::canFetchEvents() ) + return $wire.fetchEvents({ start, end }, cachedEventIds) + .then(events => { + // Cache fetched events + cachedEventIds.push(...events.map(event => event.id)); + + return successCallback(events); + }) + .catch( failureCallback ); + @else + return successCallback([]); + @endif + } + const calendar = new FullCalendar.Calendar($el, { ...config, locale, @@ -52,46 +66,18 @@ eventDrop, dateClick, select, - @if($this->isLazyLoad()) - events: function(fetchInfo, successCallback, failureCallback) { - if(initial){ - initial = false - - if(eventsData[0]?.id){ - eventsData.forEach((event) => cachedEventIds.push(event.id)) - } - - successCallback(eventsData) - }else{ - $wire.lazyLoadViewData(fetchInfo) - .then(result => { - if(result.length == 0) return - - if(result[0].id){ - result.forEach((event) => cachedEventIds.indexOf(event.id) != -1 ? null : cachedEventIds.push(event.id) && eventsData.push(event)) - successCallback(eventsData) - }else{ - successCallback(result) - } - }) - } - }, - @else - events: eventsData, - @endif + eventSources:[ + { events }, + fetchEvents + ] }); calendar.render(); - window.addEventListener("filament-fullcalendar:refresh", (event) => { + window.addEventListener("filament-fullcalendar:refresh", () => { calendar.removeAllEvents(); - @unless($this->isLazyLoad()) - event.detail.data.map(event => calendar.addEvent(event)); - @else - cachedEventIds.splice(0, cachedEventIds.length) - calendar.refetchEvents() - eventsData.splice(0, eventsData.length) - @endunless + cachedEventIds.length = 0; + calendar.refetchEvents(); }); }) '> diff --git a/src/Widgets/Concerns/CanFetchEvents.php b/src/Widgets/Concerns/CanFetchEvents.php new file mode 100644 index 0000000..9a6b724 --- /dev/null +++ b/src/Widgets/Concerns/CanFetchEvents.php @@ -0,0 +1,25 @@ +dispatchBrowserEvent('filament-fullcalendar:refresh', $this->isLazyLoad() ? null : ['data' => $this->getViewData()]); + $this->dispatchBrowserEvent('filament-fullcalendar:refresh'); } } diff --git a/src/Widgets/Concerns/UsesLazyLoad.php b/src/Widgets/Concerns/UsesLazyLoad.php deleted file mode 100644 index dc20224..0000000 --- a/src/Widgets/Concerns/UsesLazyLoad.php +++ /dev/null @@ -1,13 +0,0 @@ -with([ - 'events' => $this->isLazyLoad() ? $this->lazyLoadViewData() : $this->getViewData(), + 'events' => $this->getViewData(), ]); } }