Guía R-004 – React y FullCalendar

Esta biblioteca de JavaScript para definir la información de Calendario es un clásico y la hemos usado mucho en los desarrollos de PHPRunner.

En React, está toda ella disponible y sirve todo lo que hemos aprendido de su uso en PHPRunner y es, todavía, más sencillo explotar todas sus funcionalidades desde el entorno de React.

En este artículo facilito 2 ejemplos (de la propia biblioteca), pero adaptados a la última versión de React.

Aunque espero que ya conozcáis cómo se puede saber las librerías instaladas utilizando el fichero «package.json«, voy a explicar qué es lo que he instalado en cada caso.

package.json
{
  «name»: «fullcalendar-react»,
  «version»: «0.1.0»,
  «private»: true,
  «homepage»: «/fullcalendar-react»,
  «dependencies»: {
    «@emotion/react»: «^11.14.0»,
    «@emotion/styled»: «^11.14.0»,
    «@fullcalendar/core»: «^6.1.15»,
    «@fullcalendar/react»: «^6.1.15»,
    «@mui/icons-material»: «^6.4.1»,
    «@mui/material»: «^6.4.1»,
    «@testing-library/jest-dom»: «^5.17.0»,
    «@testing-library/react»: «^13.4.0»,
    «@testing-library/user-event»: «^13.5.0»,
    «ajv»: «^8.17.1»,
    «axios»: «^1.7.9»,
    «fullcalendar»: «^6.1.15»,
    «react»: «^19.0.0»,
    «react-dom»: «^19.0.0»,
    «react-router-dom»: «^7.1.3»,
    «react-scripts»: «5.0.1»,
    «sweetalert2»: «^11.15.10»,
    «sweetalert2-react-content»: «^5.1.0»,
    «web-vitals»: «^2.1.4»
  },
  «scripts»: {
    «start»: «react-scripts start»,
    «build»: «react-scripts build»,
    «test»: «react-scripts test»,
    «eject»: «react-scripts eject»
  },
  «eslintConfig»: {
    «extends»: [
      «react-app»,
      «react-app/jest»
    ]
  },
  «browserslist»: {
    «production»: [
      «>0.2%»,
      «not dead»,
      «not op_mini all»
    ],
    «development»: [
      «last 1 chrome version»,
      «last 1 firefox version»,
      «last 1 safari version»
    ]
  }
}

Objetivo

Comprobar la integración de FullCalendar en el entorno de React.

Demo1:    https://fhumanes.com/fullcalendar-react/

Instalaciones:
npm install fullcalendar
npm install @fullcalendar/react

Demo2: https://fhumanes.com/scheduler-react/

Instalaciones:
npm install fullcalendar
npm install @fullcalendar/react

npm install @fullcalendar/resource
npm install @fullcalendar/resource-timeline
npm install @fullcalendar/adptive
 

Solución Técnica

Como os he indicado, los ejemplos los he recogido del proyecto que facilitan desde el producto https://github.com/fullcalendar/fullcalendar-examples, lo único que he hecho es ajustarlo a la forma en que construyo TODOS los proyectos de React y actualizar los módulos que utiliza.

En el siguiente artículo que escribiré, utilizaré la misma funcionalidad descrita en el artículo Guía 64 – Utilización de la biblioteca «FullCalendar», pero realizado en React.

Por si queréis consultar el código, os dejo lo más relevante de los ejemplos.

Ejemplo: Fullcalendar, fichero: DemoApp.jsx
import React, { useState } from 'react'
import { formatDate } from '@fullcalendar/core'
import FullCalendar from '@fullcalendar/react'
import dayGridPlugin from '@fullcalendar/daygrid'
import timeGridPlugin from '@fullcalendar/timegrid'
import interactionPlugin from '@fullcalendar/interaction'
import { INITIAL_EVENTS, createEventId } from './event-utils'

export default function DemoApp() {
  const [weekendsVisible, setWeekendsVisible] = useState(true)
  const [currentEvents, setCurrentEvents] = useState([])

  function handleWeekendsToggle() {
    setWeekendsVisible(!weekendsVisible)
  }

  function handleDateSelect(selectInfo) {
    let title = prompt('Please enter a new title for your event')
    let calendarApi = selectInfo.view.calendar

    calendarApi.unselect() // clear date selection

    if (title) {
      calendarApi.addEvent({
        id: createEventId(),
        title,
        start: selectInfo.startStr,
        end: selectInfo.endStr,
        allDay: selectInfo.allDay
      })
    }
  }

  function handleEventClick(clickInfo) {
   
    if (window.confirm("Are you sure you want to delete the event "+clickInfo.event.title)  ) {
      clickInfo.event.remove()
    }
    console.log("informa del clic en evento: ",clickInfo.event.id);
  }

  function handleEvents(events) {
    setCurrentEvents(events)
  }

  return (
    <div className='demo-app'>
      <Sidebar
        weekendsVisible={weekendsVisible}
        handleWeekendsToggle={handleWeekendsToggle}
        currentEvents={currentEvents}
      />
      <div className='demo-app-main'>
        <FullCalendar
          plugins={[dayGridPlugin, timeGridPlugin, interactionPlugin]}
          headerToolbar={{
            left: 'prev,next today',
            center: 'title',
            right: 'dayGridMonth,timeGridWeek,timeGridDay'
          }}
          initialView='dayGridMonth'
          editable={true}
          selectable={true}
          selectMirror={true}
          dayMaxEvents={true}
          weekends={weekendsVisible}
          initialEvents={INITIAL_EVENTS} // alternatively, use the `events` setting to fetch from a feed
          select={handleDateSelect}
          eventContent={renderEventContent} // custom render function
          eventClick={handleEventClick}
          eventsSet={handleEvents} // called after events are initialized/added/changed/removed
          /* you can update a remote database when these fire:
          eventAdd={function(){}}
          eventChange={function(){}}
          eventRemove={function(){}}
          */
        />
      </div>
    </div>
  )
}

function renderEventContent(eventInfo) {
  return (
    <>
      <b>{eventInfo.timeText}</b>
      <i>{eventInfo.event.title}</i>
    </>
  )
}

function Sidebar({ weekendsVisible, handleWeekendsToggle, currentEvents }) {
  return (
    <div className='demo-app-sidebar'>
      <div className='demo-app-sidebar-section'>
        <h2>Instructions</h2>
        <ul>
          <li>Select dates and you will be prompted to create a new event</li>
          <li>Drag, drop, and resize events</li>
          <li>Click an event to delete it</li>
        </ul>
      </div>
      <div className='demo-app-sidebar-section'>
        <label>
          <input
            type='checkbox'
            checked={weekendsVisible}
            onChange={handleWeekendsToggle}
          ></input>
          toggle weekends
        </label>
      </div>
      <div className='demo-app-sidebar-section'>
        <h2>All Events ({currentEvents.length})</h2>
        <ul>
          {currentEvents.map((event) => (
            <SidebarEvent key={event.id} event={event} />
          ))}
        </ul>
      </div>
    </div>
  )
}

function SidebarEvent({ event }) {
  return (
    <li key={event.id}>
      <b>{formatDate(event.start, {year: 'numeric', month: 'short', day: 'numeric'})}</b>
      <i>{event.title}</i>
    </li>
  )
}
Ejemplo: Scheduler, fichero: DemoApp.jsx
import React from 'react'
import { formatDate } from '@fullcalendar/core'
import FullCalendar from '@fullcalendar/react'
import adaptivePlugin from '@fullcalendar/adaptive'
import dayGridPlugin from '@fullcalendar/daygrid'
import timeGridPlugin from '@fullcalendar/timegrid'
import interactionPlugin from '@fullcalendar/interaction'
import resourceTimelinePlugin from '@fullcalendar/resource-timeline'
import { INITIAL_EVENTS, createEventId } from './event-utils'

const RESOURCES = [
  { id: 'a', title: 'Auditorium A' },
  { id: 'b', title: 'Auditorium B', eventColor: 'green' },
  { id: 'c', title: 'Auditorium C', eventColor: 'orange' },
]

export default class DemoApp extends React.Component {

  state = {
    weekendsVisible: true,
    currentEvents: []
  }

  render() {
    return (
      <div className='demo-app'>
        {this.renderSidebar()}
        <div className='demo-app-main'>
          <FullCalendar
            plugins={[
              adaptivePlugin,
              dayGridPlugin,
              timeGridPlugin,
              interactionPlugin,
              resourceTimelinePlugin,
            ]}
            headerToolbar={{
              left: 'prev,next today',
              center: 'title',
              right: 'dayGridMonth,timeGridWeek,resourceTimelineDay'
            }}
            initialView='resourceTimelineDay'
            editable={true}
            selectable={true}
            selectMirror={true}
            dayMaxEvents={true}
            weekends={this.state.weekendsVisible}
            initialEvents={INITIAL_EVENTS} // alternatively, use the `events` setting to fetch from a feed
            resources={RESOURCES}
            select={this.handleDateSelect}
            eventContent={renderEventContent} // custom render function
            eventClick={this.handleEventClick}
            eventsSet={this.handleEvents} // called after events are initialized/added/changed/removed
            /* you can update a remote database when these fire:
            eventAdd={function(){}}
            eventChange={function(){}}
            eventRemove={function(){}}
            */
          />
        </div>
      </div>
    )
  }

  renderSidebar() {
    return (
      <div className='demo-app-sidebar'>
        <div className='demo-app-sidebar-section'>
          <h2>Instructions</h2>
          <ul>
            <li>Select dates and you will be prompted to create a new event</li>
            <li>Drag, drop, and resize events</li>
            <li>Click an event to delete it</li>
          </ul>
        </div>
        <div className='demo-app-sidebar-section'>
          <label>
            <input
              type='checkbox'
              checked={this.state.weekendsVisible}
              onChange={this.handleWeekendsToggle}
            ></input>
            toggle weekends
          </label>
        </div>
        <div className='demo-app-sidebar-section'>
          <button onClick={this.handlePrint}>Print</button>
        </div>
        <div className='demo-app-sidebar-section'>
          <h2>All Events ({this.state.currentEvents.length})</h2>
          <ul>
            {this.state.currentEvents.map(renderSidebarEvent)}
          </ul>
        </div>
      </div>
    )
  }

  handleWeekendsToggle = () => {
    this.setState({
      weekendsVisible: !this.state.weekendsVisible
    })
  }

  handleDateSelect = (selectInfo) => {
    let title = prompt('Please enter a new title for your event')
    let calendarApi = selectInfo.view.calendar

    calendarApi.unselect() // clear date selection

    if (title) {
      calendarApi.addEvent({
        id: createEventId(),
        title,
        start: selectInfo.startStr,
        end: selectInfo.endStr,
        allDay: selectInfo.allDay
      })
    }
  }

  handleEventClick = (clickInfo) => {
    if (window.confirm(`Are you sure you want to delete the event '${clickInfo.event.title}'`)) {
      clickInfo.event.remove()
    }
  }

  handleEvents = (events) => {
    // Ocultar el LOGO de producto no registrado
      var logo = document.getElementsByClassName('fc-license-message');
      // console.log("Valor de 'logo': ",logo);
      // console.log("Evento : ",events);
      if ( logo.length === 1 ) {
        logo[0].style.visibility = 'hidden'; // Especial Hide logo
      }
    this.setState({
      currentEvents: events
    })
  }

  handlePrint = () => {
    window.print()
  }

}

function renderEventContent(eventInfo) {
  return (
    <>
      <b>{eventInfo.timeText}</b>
      <i>{eventInfo.event.title}</i>
    </>
  )
}

function renderSidebarEvent(event) {
  return (
    <li key={event.id}>
      <b>{formatDate(event.start, {year: 'numeric', month: 'short', day: 'numeric'})}</b>
      <i>{event.title}</i>
    </li>
  )
}

Como siembre, os dejo los fuentes para que los podáis instalar en vuestros equipos.

Adjuntos

Archivo Tamaño de archivo Descargas
zip fullcalendar-react 183 KB 28
zip scheduler-react 178 KB 24

Blog personal para facilitar soporte gratuito a usuarios de React y PHPRunner