Event-data conventions!?
See original GitHub issueFullCalendar has not established a clear convention for React users (nor Vue/Angular users) for providing event data to their calendars, which is a little bit ridiculous. This has led to many improvised approaches which often have shortcomings like double-rendering events or being at odds with Reactās declarative nature.
Iām proposing two patterns that I will eventually add to the docs. They require some additional API tweaks (see proposals below), but hopefully they are immediately readable as pseudocode. The sample code is based off the v5 api, which is currently in beta. The sample code displays a simple month-view calendar with events loaded from a server and also displays a plain text list of events below, along with an āadd eventā button.
Please comment on this issue or at least give a š if you approve.
Technique 1: FullCalendar owns the event data
import * as React from 'react'
import FullCalendar from '@fullcalendar/react'
//
// FullCalendar OWNS THE EVENT DATA, meaning:
// FullCalendar requests the data from a feed,
// FullCalendar manipulates the data for drag-n-drop/resize.
//
class DemoApp extends React.Component {
calendarRef = React.createRef()
state = {
currentRefinedEvents: [] // events parsed by FullCalendar
}
render() {
return (
<div>
<FullCalendar
ref={this.calendarRef}
initialView='dayGridMonth'
editable={true}
events='/events/query' // a remote JSON feed that will receive start/end params
eventsDidUpdate={this.handleEventsDidUpdate} // eventsDidUpdate not-yet-implemented (see proposal)
/>
<h2>Current Event List:</h2>
<ul>
{this.state.currentRefinedEvents.map((refinedEvent) => (
<li>{refinedEvent.title} - {refinedEvent.start.toISOString()}</li>
))}
</ul>
<button onClick={this.handleAddEventToday}>
add an event today!
</button>
</div>
)
}
handleEventsDidUpdate = (arg) => {
if (arg.addedEvent) { // an EventApi object
request('/events/add', JSON.stringify(arg.addedEvent)) // requires EventApi::toJSON (see proposal)
} else if (arg.updatedEvent) { // an EventApi object
request('/events/update', JSON.stringify(arg.updatedEvent)) // requires EventApi::toJSON (see proposal)
} else if (arg.removedEvent) { // an EventApi object
request('/events/remove', { id: arg.removedEvent.id })
}
// There's a final condition for when events are received from the server
// (the arg.loadedEvents array will be populated)
//
// For any of these scenarios (add/update/delete/loaded),
// give DemoApp the parsed event data...
this.setState({ currentRefinedEvents: arg.allEvents })
}
handleAddEventToday = () => {
let calendarApi = this.calendarRef.current.getApi()
calendarApi.addEvent({
title: 'event that happens today',
start: new Date()
})
}
}
Technique 2: DemoApp owns the event data
import * as React from 'react'
import FullCalendar from '@fullcalendar/react'
import { requestJson, request } from './utils' // please imagine these
//
// DemoApp OWNS THE EVENT DATA, meaning:
// DemoApp requests and parses the data data from a feed,
// DemoApp manipulates the data for drag-n-drop/resize.
//
class DemoApp extends React.Component {
state = {
currentRefinedEventMap: {} // an {id:event} hash of parsed events
}
render() {
return (
<div>
<FullCalendar
initialView='dayGridMonth'
editable={true}
datesDidUpdate={this.handleDatesDidUpdate}
events={this.state.currentRefinedEventMap} // accepting a hash not-yet-implemented (see proposal)
eventsDidUpdate={this.handleEventsDidUpdate} // eventsDidUpdate not-yet-implemented (see proposal)
/>
<h2>Current Event List:</h2>
<ul>
{Object.values(this.state.currentRefinedEvents).map((refinedEvent) => (
<li>{refinedEvent.title} - {refinedEvent.start.toISOString()}</li>
))}
</ul>
<button onClick={this.handleAddEventToday}>
add an event today!
</button>
</div>
)
}
// called when the calendar's data are initially set, or when they change
handleDatesDidUpdate = (arg) => {
requestJson('/events/query', {
start: arg.view.activeRange.start.toISOString(),
end: arg.view.activeRange.end.toISOString()
}).then((rawEvents) => {
this.setState({
currentRefinedEventMap: buildRefinedEventMap(rawEvents)
})
})
}
handleEventsDidUpdate = (arg) => {
if (arg.addedEvent) { // an EventApi object
this.addEvent(arg.addedEvent)
} else if (arg.updatedEvent) { // an EventApi object
this.updateEvent(arg.updatedEvent)
} else if (arg.removedEvent) { // an EventApi object
this.removeEvent(arg.removedEvent)
}
// returning false will prevent FullCalendar from recording these changes internally.
// this is desirable because our setState call will modify currentRefinedEventMap,
// which will be passed to FullCalendar anyway.
return false
}
handleAddEventToday = () => {
this.addEvent(
parseRawEvent({
title: 'event that happens today',
start: new Date()
})
)
}
addEvent(event) { // event might have been refined by DemoApp OR FullCalendar
request('/events/add', JSON.stringify(event)) // requires EventApi::toJSON (see proposal)
this.setState({
currentRefinedEventMap: addOrUpdateEventMap(
this.state.currentRefinedEventMap,
event
)
})
}
updateEvent(event) { // event might have been refined by DemoApp OR FullCalendar
request('/events/update', JSON.stringify(event)) // requires EventApi::toJSON (see proposal)
this.setState({
currentRefinedEventMap: addOrUpdateEventMap(
this.state.currentRefinedEventMap,
event
)
})
}
removeEvent(event) { // event might have been refined by DemoApp OR FullCalendar
request('/events/remove', { id: event.id })
this.setState({
currentRefinedEventMap: deleteFromEventMap(
this.state.currentRefinedEventMap,
event
)
})
}
}
// event data-manipulation methods
// (this logic would probably be in your reducer)
// ----------------------------------------------
function buildRefinedEventMap(rawEvents) {
let map = {}
for (let rawEvent of rawEvents) {
let refinedEvent = parseRawEvent(rawEvent)
if (refinedEvent) {
map[refinedEvent.id] = refinedEvent
} else {
console.warn('Invalid event data', rawEvent)
}
}
return map
}
function parseRawEvent(rawEvent) {
if (rawEvent.id && rawEvent.start) { // TODO: could be much more stringent
return {
id: rawEvent.id,
title: rawEvent.title,
start: new Date(rawEvent.start), // parses an ISO8601 date in most browsers
end: rawEvent.end ? new Date(rawEvent.end) : null // (same)
}
}
}
function addOrUpdateEventMap(map, event) {
return {
...map,
[event.id]: event.toJSON() // returns a plain object. not-yet-implemented (see proposal)
}
}
function deleteFromEventMap(map, event) {
let mapCopy = { ...map }
delete mapCopy[event.id]
return mapCopy
}
Apendix
Proposed API changes to make the above code possible:
Issue Analytics
- State:
- Created 3 years ago
- Reactions:11
- Comments:18 (3 by maintainers)
Top GitHub Comments
Technique 2 would be really great. In react you donāt want some children-component to own data, because you always have to sync it with its parent. It would be a great improvement š
pretty sure its better design to handle it via the parent as in use case 2. weād need the data in the parent , and to pass in a function declared in the parent to act on button clicks.