Features
Spring Boot (3.3.0) application that displays a web page with JavaScript Scheduler component from DayPilot Pro for JavaScript.
The Scheduler displays events for multiple resources.
The Scheduler loads event and resource data from a REST/JSON endpoint.
Basic drag and drop operations supported (creating, moving and resizing events).
Custom event rendering (event bar color is stored in the database, configurable using a context menu)
License
Licensed for testing and evaluation purposes. Please see the license agreement included in the sample project. You can use the source code of the tutorial if you are a licensed user of DayPilot Pro for JavaScript.
Basic Scheduler Configuration (HTML5)
Our web application will display a single static HTML page (index.html
). In order to display the DayPilot Scheduler component we need to add the following code to the HTML:
<!-- placeholder div -->
<div id="dp"></div>
<!-- DayPilot library -->
<script src="js/daypilot/daypilot-all.min.js"></script>
<!-- Scheduler initialization -->
<script>
const dp = new DayPilot.Scheduler("dp", {
startDate: DayPilot.Date.today().firstDayOfMonth(),
days: DayPilot.Date.today().daysInMonth(),
scale: "Day",
timeHeaders: [
{groupBy: "Month"},
{groupBy: "Day", format: "d"}
]
});
dp.init();
</script>
We have added a couple of configuration options to the Scheduler initialization code:
The Scheduler will display the current month on the time axis (startDate, days properties).
The timeline scale is set to one cell per day (scale property).
The time header displays two rows, with months in the first row and days in the second row (timeHeaders property).
Loading Rows from Database
The Scheduler rows (resources) are defined in the database. We e will load them using the rows.load() method from a JSON backend endpoint:
dp.rows.load("/api/resources");
This method loads the resources from the server using a GET request and automatically updates the Scheduler when the response arrives.
The structure of the resource data is described in the resources property API documentation.
Sample JSON response:
[
{"name":"Resource 1","id":1},
{"name":"Resource 2","id":2},
{"name":"Resource 3","id":3},
{"name":"Resource 4","id":4},
{"name":"Resource 5","id":5},
{"name":"Resource 6","id":6},
{"name":"Resource 7","id":7},
{"name":"Resource 8","id":8},
{"name":"Resource 9","id":9}
]
The /api/resources
endpoint is implemented using a standard Spring MVC controller:
package org.daypilot.demo.html5schedulerspring.controller;
// ...
@RestController
public class MainController {
@Autowired
ResourceRepository rr;
@RequestMapping("/api/resources")
Iterable<Resource> resources() {
return rr.findAll();
}
// ...
}
Loading Scheduler Events (Appointments, Reservations)
To load and display events in the Scheduler, you can use the events.load() method:
dp.events.load("/api/events");
The events.load()
method calls the endpoint at the specified URL to get the event data.
The Scheduler automatically appends from
and to
parameters the to URL query string. This way, the start and end of the current schedule is passed to the Spring controller.
In November 2024 the request URL will look like this:
http://localhost:8081/api/events?start=2024-11-01T00:00:00&end=2024-12-01T00:00:00
These query string parameters will help us side to select only the relevant events from the database.
The /api/events
endpoint is implemented in the MainController.java
file:
package org.daypilot.demo.html5schedulerspring.controller;
// ...
@RestController
public class MainController {
@Autowired
EventRepository er;
@GetMapping("/api/events")
@JsonSerialize(using = LocalDateTimeSerializer.class)
Iterable<Event> events(@RequestParam("start") @DateTimeFormat(iso=ISO.DATE_TIME) LocalDateTime start, @RequestParam("end") @DateTimeFormat(iso=ISO.DATE_TIME) LocalDateTime end) {
return er.findBetween(start, end);
}
// ...
}
Sample JSON response:
[
{"id":1,"text":"Reservation","start":"2024-11-03T00:00:00","end":"2024-11-07T00:00:00","color":null,"resource":2}
]
Creating New Events using Drag and Drop
The Scheduler supports drag and drop for creation of new events.
Whenever a user selects a time range, the onTimeRangeSelected event is fired.
We will add an event handler that asks for an event name using a simple modal dialog.
After the user confirms the new name, the app creates a new event in the database using an HTTP call to
/api/events/create
.
The modal dialog is created using DayPilot.Modal.form(). It lets you build a modal form dynamically (see also Modal Builder - an online application you can use to design the modal dialog visually).
This example uses a simple modal dialog with a single field (“Event name”).
<script>
const dp = new DayPilot.Scheduler("dp", {
onTimeRangeSelected: async args => {
const form = [
{
name: "Event name",
id: "name"
}
];
const data = {
name: "Event"
};
const modal = await DayPilot.Modal.form(form, data);
dp.clearSelection();
if (modal.canceled) {
return;
}
const params = {
start: args.start,
end: args.end,
text: modal.result.name,
resource: args.resource
};
const {data: result} = await DayPilot.Http.post("/api/events/create", params);
dp.events.add(result);
dp.message("Event created");
},
// ...
});
dp.init();
</script>
The Spring backend saves the event to the database and returns the event data, including a generated ID:
package org.daypilot.demo.html5schedulerspring.controller;
// ...
@RestController
public class MainController {
@Autowired
EventRepository er;
// ...
@PostMapping("/api/events/create")
@JsonSerialize(using = LocalDateTimeSerializer.class)
@Transactional
Event createEvent(@RequestBody EventCreateParams params) {
Resource r = rr.findById(params.resource).get();
Event e = new Event();
e.setStart(params.start);
e.setEnd(params.end);
e.setText(params.text);
e.setResource(r);
er.save(e);
return e;
}
public static class EventCreateParams {
public LocalDateTime start;
public LocalDateTime end;
public String text;
public Long resource;
}
}
Custom Event Colors
Now, we will let users change the color of the event using a context menu.
First, we define a new context menu using the contextMenu
property:
<script>
const dp = new DayPilot.Scheduler("dp", {
contextMenu: new DayPilot.Menu({
items: [
{
text: "Blue",
icon: "icon icon-blue",
color: "#1155cc",
onClick: (args) => {
updateColor(args.source, args.item.color);
}
},
{
text: "Green",
icon: "icon icon-green",
color: "#6aa84f",
onClick: (args) => {
updateColor(args.source, args.item.color);
}
},
{
text: "Yellow",
icon: "icon icon-yellow",
color: "#f1c232",
onClick: (args) => {
updateColor(args.source, args.item.color);
}
},
{
text: "Red",
icon: "icon icon-red",
color: "#cc0000",
onClick: (args) => {
updateColor(args.source, args.item.color);
}
},
]
}),
// ...
});
dp.init();
</script>
By default, the context menu opens on right click. However, it may be hard for new users to guess that this option is available.
Fortunately, there is a built-in tool to add a hover context menu hint - an active area. The active area will be displayed at the specified position (in the upper-right corner of the event) when a user hovers over the event box. We set the active area action to "ContextMenu"
- this will open the default event context menu on click.
<script>
const dp = new DayPilot.Scheduler("dp", {
onBeforeEventRender: args => {
args.data.areas = [
{
top: 10,
right: 4,
width: 16,
height: 16,
symbol: "icons/daypilot.svg#minichevron-down-4",
fontColor: "#999",
visibility: "Hover",
action: "ContextMenu",
style: "background-color: #f9f9f9; border: 1px solid #ccc; cursor:pointer;"
}
];
},
// ...
});
dp.init();
</script>
Our active area now looks like this:
The context menu items set the event color using an HTTP call to /api/events/setColor
:
async function updateColor(e, color) {
const params = {
id: e.id(),
color: color
};
await DayPilot.Http.post("/api/events/setColor", params);
e.data.color = color;
dp.events.update(e);
dp.message("Color updated");
}
The Spring MVC controller that defines the API endpoint:
package org.daypilot.demo.html5schedulerspring.controller;
// ...
@RestController
public class MainController {
@Autowired
EventRepository er;
// ...
@PostMapping("/api/events/setColor")
@JsonSerialize(using = LocalDateTimeSerializer.class)
@Transactional
Event setColor(@RequestBody SetColorParams params) {
Event e = er.findById(params.id).get();
e.setColor(params.color);
er.save(e);
return e;
}
// ...
public static class SetColorParams {
public Long id;
public String color;
}
}
The last task is to apply the saved color to the event during Scheduler rendering. The color is available in the event data JSON array, in the color
property:
[
{"id":1,"text":"Reservation","start":"2024-11-03T00:00:00","end":"2024-11-07T00:00:00","color":"#6aa84f","resource":2}
]
We need to map it to the barColor
property of the event data object. This can be done using the onBeforeEventRender event.
onBeforeEventRender = (args) => {
args.data.barColor = args.data.color;
// ...
}
See also event customization [doc.daypilot.org].
Complete HTML Source
This is the full source code of the HTML page that displays the Scheduler component (src/main/resources/static/index.html
):
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>Using JavaScript/HTML5 Scheduler in Spring Boot (Java)</title>
<link href="css/main.css" rel="stylesheet" type="text/css">
</head>
<body>
<div class="header">
<h1><a href='https://code.daypilot.org/70997/using-javascript-html5-scheduler-in-spring-boot-java'>Using
JavaScript/HTML5 Scheduler in Spring Boot (Java)</a></h1>
<div><a href="https://javascript.daypilot.org/">DayPilot for JavaScript</a> - HTML5 Calendar/Scheduling Components
for JavaScript/Angular/React/Vue
</div>
</div>
<div class="main">
<div id="dp"></div>
</div>
<!-- DayPilot library -->
<script src="js/daypilot/daypilot-all.min.js"></script>
<style>
.area-menu {
border: 1px solid #999;
color: #999;
border-radius: 10px;
cursor:pointer;
}
.area-menu:hover {
border: 1px solid #666;
color: #666;
}
</style>
<script>
const dp = new DayPilot.Scheduler("dp", {
startDate: DayPilot.Date.today().firstDayOfMonth(),
days: DayPilot.Date.today().daysInMonth(),
scale: "Day",
timeHeaders: [
{groupBy: "Month"},
{groupBy: "Day", format: "d"}
],
eventHeight: 40,
durationBarHeight: 6,
rowHeaderColumns: [
{text: "Name"},
{text: "Type", display: "type"}
],
onTimeRangeSelected: async args => {
const form = [
{
name: "Event name",
id: "name"
}
];
const data = {
name: "Event"
};
const modal = await DayPilot.Modal.form(form, data);
dp.clearSelection();
if (modal.canceled) {
return;
}
const params = {
start: args.start,
end: args.end,
text: modal.result.name,
resource: args.resource
};
const {data: result} = await DayPilot.Http.post("/api/events/create", params);
dp.events.add(result);
dp.message("Event created");
},
onEventMove: async args => {
const params = {
id: args.e.id(),
start: args.newStart,
end: args.newEnd,
resource: args.newResource
};
await DayPilot.Http.post("/api/events/move", params);
dp.message("Event moved");
},
onEventResize: async args => {
const params = {
id: args.e.id(),
start: args.newStart,
end: args.newEnd,
resource: args.e.resource()
};
await DayPilot.Http.post("/api/events/move", params);
dp.message("Event resized");
},
onBeforeEventRender: args => {
const defaultColor = "#3F85A4";
const color = args.data.color || defaultColor;
args.data.barColor = color;
args.data.areas = [
{
top: 12,
right: 6,
width: 18,
height: 18,
symbol: "icons/daypilot.svg#minichevron-down-4",
backColor: "#f9f9f9",
visibility: "Visible",
action: "ContextMenu",
cssClass: "area-menu",
}
];
},
contextMenu: new DayPilot.Menu({
items: [
{
text: "Blue",
icon: "icon icon-blue",
color: "#3F85A4",
onClick: (args) => {
app.updateColor(args.source, args.item.color);
}
},
{
text: "Green",
icon: "icon icon-green",
color: "#6aa84f",
onClick: (args) => {
app.updateColor(args.source, args.item.color);
}
},
{
text: "Yellow",
icon: "icon icon-yellow",
color: "#ecb823",
onClick: (args) => {
app.updateColor(args.source, args.item.color);
}
},
{
text: "Red",
icon: "icon icon-red",
color: "#ee5530",
onClick: (args) => {
app.updateColor(args.source, args.item.color);
}
},
]
})
});
dp.init();
const app = {
async updateColor(e, color) {
const params = {
id: e.id(),
color: color
};
await DayPilot.Http.post("/api/events/setColor", params);
e.data.color = color;
dp.events.update(e);
dp.message("Color updated");
},
init() {
dp.rows.load("/api/resources");
dp.events.load("/api/events");
}
};
app.init();
</script>
</body>
</html>
Complete Spring MVC Controller
Here is the source code of the Spring controller that defines the backend API of our scheduling application (MainController.java
):
package org.daypilot.demo.html5schedulerspring.controller;
import java.time.LocalDateTime;
import jakarta.transaction.Transactional;
import org.daypilot.demo.html5schedulerspring.domain.Event;
import org.daypilot.demo.html5schedulerspring.domain.Resource;
import org.daypilot.demo.html5schedulerspring.repository.EventRepository;
import org.daypilot.demo.html5schedulerspring.repository.ResourceRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.format.annotation.DateTimeFormat.ISO;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
@RestController
public class MainController {
@Autowired
EventRepository er;
@Autowired
ResourceRepository rr;
@RequestMapping("/api")
@ResponseBody
String home() {
return "Welcome!";
}
@RequestMapping("/api/resources")
Iterable<Resource> resources() {
return rr.findAll();
}
@GetMapping("/api/events")
@JsonSerialize(using = LocalDateTimeSerializer.class)
Iterable<Event> events(@RequestParam("start") @DateTimeFormat(iso = ISO.DATE_TIME) LocalDateTime start, @RequestParam("end") @DateTimeFormat(iso = ISO.DATE_TIME) LocalDateTime end) {
return er.findBetween(start, end);
}
@PostMapping("/api/events/create")
@JsonSerialize(using = LocalDateTimeSerializer.class)
@Transactional
Event createEvent(@RequestBody EventCreateParams params) {
Resource r = rr.findById(params.resource).get();
Event e = new Event();
e.setStart(params.start);
e.setEnd(params.end);
e.setText(params.text);
e.setResource(r);
er.save(e);
return e;
}
@PostMapping("/api/events/move")
@JsonSerialize(using = LocalDateTimeSerializer.class)
@Transactional
Event moveEvent(@RequestBody EventMoveParams params) {
Event e = er.findById(params.id).get();
Resource r = rr.findById(params.resource).get();
e.setStart(params.start);
e.setEnd(params.end);
e.setResource(r);
er.save(e);
return e;
}
@PostMapping("/api/events/setColor")
@JsonSerialize(using = LocalDateTimeSerializer.class)
@Transactional
Event setColor(@RequestBody SetColorParams params) {
Event e = er.findById(params.id).get();
e.setColor(params.color);
er.save(e);
return e;
}
public static class EventCreateParams {
public LocalDateTime start;
public LocalDateTime end;
public String text;
public Long resource;
}
public static class EventMoveParams {
public Long id;
public LocalDateTime start;
public LocalDateTime end;
public Long resource;
}
public static class SetColorParams {
public Long id;
public String color;
}
}
Data Layer (Domain Classes)
The Event
class defines the object that stores event data (Event.java
).
Note that we use JSON annotations to format the JSON output (the resource
JSON property will store the resource id).
package org.daypilot.demo.html5schedulerspring.domain;
import java.time.LocalDateTime;
import jakarta.persistence.*;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
@Entity
public class Event {
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
Long id;
String text;
@Column(name = "event_start")
LocalDateTime start;
@Column(name = "event_end")
LocalDateTime end;
@ManyToOne
@JsonIgnore
Resource resource;
String color;
@JsonProperty("resource")
public Long getResourceId() {
return resource.getId();
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public LocalDateTime getStart() {
return start;
}
public void setStart(LocalDateTime start) {
this.start = start;
}
public LocalDateTime getEnd() {
return end;
}
public void setEnd(LocalDateTime end) {
this.end = end;
}
public Resource getResource() {
return resource;
}
public void setResource(Resource resource) {
this.resource = resource;
}
public String getColor() { return color; }
public void setColor(String color) { this.color = color; }
}
The Resource
class defines the details of the resources that will be displayed as rows (Resource.java
).
package org.daypilot.demo.html5schedulerspring.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
@Entity
public class Resource {
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
Long id;
String name;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
Data Access Layer (Repository Classes)
The EventRepository
interface lets us access the Event
data (EventRepository.java
).
In addition to the standard implementation, we need to add a findBetween()
method that lets us select event data for the specified date range.
package org.daypilot.demo.html5schedulerspring.repository;
import java.time.LocalDateTime;
import java.util.List;
import org.daypilot.demo.html5schedulerspring.domain.Event;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.format.annotation.DateTimeFormat.ISO;
public interface EventRepository extends CrudRepository<Event, Long> {
@Query("from Event e where not(e.end < :from and e.start > :to)")
public List<Event> findBetween(@Param("from") LocalDateTime start, @Param("to") @DateTimeFormat(iso=ISO.DATE_TIME) LocalDateTime end);
}
The ResourceRepository
interface provides data access methods for the Resource
data (ResourceRepository.java
):
package org.daypilot.demo.html5schedulerspring.repository;
import org.daypilot.demo.html5schedulerspring.domain.Resource;
import org.springframework.data.repository.CrudRepository;
public interface ResourceRepository extends CrudRepository<Resource, Long> {
}
Maven Project File
pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.0</version>
<relativePath/>
</parent>
<groupId>org.daypilot.demo</groupId>
<artifactId>html5-scheduler-spring</artifactId>
<version>0.0.1-SNAPSHOT</version>
<properties>
<java.version>22</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
</project>