Bryntum
17 January 2024

Using htmx with Bryntum Scheduler

Bryntum Scheduler is a performant, feature-rich scheduling UI component built with JavaScript. In this tutorial, we’ll show you how to use htmx with […]

Bryntum Scheduler is a performant, feature-rich scheduling UI component built with JavaScript. In this tutorial, we’ll show you how to use htmx with Bryntum Scheduler. The htmx JavaScript library allows you to build complex user interfaces without a frontend framework. With htmx you can add new attributes to HTML to perform AJAX requests, use CSS transitions without JavaScript, establish a WebSocket connection, and respond to server-sent events. The power and simplicity of htmx have led to a recent surge in its popularity.

This tutorial shows you how to do the following:

Here is what we’ll build:

Getting started

As a starting point, we’ll use a basic full-stack Bryntum Scheduler app with a separate backend and frontend. The frontend uses vanilla JavaScript, and the backend server is an Express app.

The backend server has REST API endpoints that we’ll use to perform CRUD operations on the database. We’ll store the scheduler data in a MySQL database.

Set up a MySQL database locally and set up an Express server

Clone the Express server starter GitHub repository and follow the instructions in the README.md file to create a local MySQL database, add example data to it, and connect it to the Express server. The completed-scheduler branch contains the code for the completed tutorial.

The Express server has two API routes, /api/load and /api/sync. The /api/load route will get events and resources from the MySQL database. The /api/sync route syncs changes to the Bryntum Scheduler on the frontend with the MySQL database. To get a detailed explanation of how the /api/sync route works, take a look at our tutorial on migrating from DHTMLX Gantt to Bryntum Gantt where we use a /sync route with similar logic.

Create an API endpoint to get extra information about a resource

Let’s create an API endpoint to get data from the resource_details table in the database. We’ll use htmx to issue an AJAX GET request when an HTML button element is pressed. The request will fetch the data. This extra information will not be used by the scheduler.

Add the following lines of code to the bottom of the server.js file:

app.get("/api/extra-info", async (req, res) => {
  const { id } = req.query;
  try {
    const extraInfo = await db.query(
      `SELECT jobTitle, yearsExperience, yearsWithCompany, age, gender, additionalInfo  FROM resource_details WHERE resourceId in (?)`,
      id
    );
    if (extraInfo[0].length === 0) {
      return res.send(`
        <div>No extra info for this resource</div>
    `);
    }
    const extraInfoObj = extraInfo[0][0];
    const {
      jobTitle,
      yearsExperience,
      yearsWithCompany,
      age,
      gender,
      additionalInfo,
    } = extraInfoObj;
    res.send(
      sanitizeHtml(`
    <div class="extraInfo">
      <h3>Extra Info</h3>
      <ul>
        <li><b>Job Title</b>: ${jobTitle}</li>
        <li><b>Years Experience:</b> ${yearsExperience}</li>
        <li><b>Years With Company:</b> ${yearsWithCompany}</li>
        <li><b>Age:</b> ${age}</li>
        <li><b>Gender:</b> ${gender}</li>
        <li><b>Additional Info:</b> ${additionalInfo}</li>
      </ul>
    </div>
    `)
    );
  } catch (error) {
    console.error({ error });
    res.send({
      success: false,
      message: "There was an getting the extra info for the resource.",
    });
  }
});

We get the resource id from the URL query parameter and use the MySQL2 query method to get extra information about the resource. An HTML string will be returned and displayed in a dialog on the frontend using htmx. To prevent XSS attacks, we use the sanitize-html library to escape any special characters in the resource_details data.

Import the sanitizeHtml library at the top of the server.js file:

import sanitizeHtml from "sanitize-html";

Now let’s work on the frontend of our application.

Create a Bryntum Scheduler

Start by cloning the Bryntum Scheduler with htmx starter GitHub repository. The starter repository uses Vite as a development server and JavaScript bundler. Install the Vite dev dependency by running the following command:

npm install

Now install the Bryntum Scheduler component using npm. First, get access to the Bryntum private npm registry by following the guide in our docs. When you’ve logged in to the registry, install the Bryntum Scheduler component by following this guide.

Add the following CSS styles to the style.css file:

body {
  margin: 0;
  padding: 0;
}
body,
html {
  display: flex;
  flex-direction: column;
  height: 100vh;
  font-family: Poppins, "Open Sans", Helvetica, Arial, sans-serif;
  font-size: 14px;
}

This CSS is imported as a style sheet in the index.html file and makes the Bryntum Scheduler take up the full height of the screen.

In the main.js file, add the following lines of code to define and initialize a basic Bryntum Scheduler:

import { Scheduler } from "@bryntum/scheduler";
import "@bryntum/scheduler/scheduler.stockholm.css";
const scheduler = new Scheduler({
  appendTo: document.body,
  date: new Date(2023, 10, 29),
  viewPreset: "day",
  resources: [
    { id: 1, name: "Dan Stevenson" },
    { id: 2, name: "Talisha Babin" },
  ],
  events: [
    {
      resourceId: 1,
      name: "Intern training",
      startDate: "2023-11-29 13:00:00",
      endDate: "2023-11-29 17:00:00",
    },
    {
      resourceId: 2,
      name: "Product launch webinar",
      startDate: "2023-11-29 14:00:00",
      endDate: "2023-11-29 16:00:00",
    },
  ],
  columns: [{ text: "Name", field: "name", width: 160 }],
});

We import the Bryntum Scheduler component and the Stockholm CSS theme. The CSS theme is required to render the Bryntum Scheduler correctly. You can choose from five Bryntum themes you can modify or create your own theme. Learn more about styling in the styling guide.

The Bryntum Scheduler config has some example resources and events data that we’ll remove later.

Now run the local development server using the following command:

npm run dev

You’ll see a basic Scheduler app:

Let’s get the data from our server into our Bryntum Scheduler.

Set up a Bryntum Scheduler Crud Manager to get data from and sync data changes to the server

Add the following lines of code to the top of the main.js file:

import { CrudManager } from "@bryntum/scheduler";
const crudManager = new CrudManager({
  transport: {
    load: {
      url: "http://localhost:1338/api/load",
    },
    sync: {
      url: "http://localhost:1338/api/sync",
    },
  },
  autoLoad: true,
  autoSync: true,
  // This config enables response validation and dumping of found errors to the browser console.
  // It's meant to be used as a development stage helper only so please set it to false for production systems.
  validateResponse: true,
});

We use the Bryntum Scheduler Crud Manager to simplify fetching data from our API endpoints. The transport property is used to configure the AJAX requests used by the Crud Manager to communicate with a server. The load URL is used to fetch our events and resource data. The Crud Manager expects the data it receives from the load API endpoint to have a specific structure.

The sync URL is used to sync data changes to our /api/sync API endpoint. For more information, you can read the guide in our docs.

Now remove the resourcesevents, and columns config properties in the scheduler to remove the example data. Add the following config properties in their place:

    crudManager, 
    columns: [
      { 
        type: "resourceInfo",
        text: "Name",
        width: 160,
      },
    ],

Start the server in one terminal and the frontend local application in another terminal using the following command:

npm run dev

You should see the MySQL events and resources data in your Bryntum Scheduler:

Any changes you make to the events or resources will be persisted in your MySQL database.

Now let’s use htmx with our scheduler to dynamically get extra information about a resource.

Create a toolbar button to get and display extra information about a resource using htmx

Add the following tbar config to your scheduler below the columns property to get a Toolbar widget added to the top of the scheduler:

  tbar: [
    {
      type: "widget",
      ref: "resourceLabel",
      html: "No resource selected",
    },
    {
      type: "widget",
      ref: "extraInfoButton",
      icon: "b-fa b-fa-plus",
      html: "<button class='b-widget b-button b-text b-box-item b-green' disabled>Get extra info about the resource</button>",
    },
  ],

The toolbar container widget has two widgets. One is a label that will show which resource is selected, the other is a button that we’ll use to get extra information about the selected resource. The button uses classes from the Bryntum Button widget.

Add the following listeners config object below the tbar config:

  listeners: {
    cellClick(props) {
      const selectedResourceName = props.record.data.name;
      scheduler.widgetMap.resourceLabel.html = StringHelper.xss`Selected resource: ${selectedResourceName}`;
    },
  },

We use the cellClick event listener to determine the resource of the cell that’s been clicked in the scheduler. We then use the widgetMap object to select the resource label widget in the toolbar. We get the object using its ref property, which is an identifier by which a widget will be registered in the widgetMap, and set its HTML to include the name of the selected resource. The StringHelper.xss function helps prevent XSS attacks.

Make sure to import the StringHelper class along with the other imports at the top:

import { StringHelper } from "@bryntum/scheduler";

Now click on a cell in your scheduler. You should see the name of the selected resource in the toolbar:

Fetch the extra information for a resource using htmx

The “Get extra info about the resource” button is not yet functional. We’ll use the cellClick event listener to swap out the button for one that uses htmx to fetch extra information about the selected resource.

First, we need to add htmx to our project. For simplicity, we’ll add it using a CDN. Add the following <script> tag to the <head> tag in your index.html file:

    <script src="https://unpkg.com/htmx.org@1.9.9" integrity="sha384-QFjmbokDn2DjBjq+fM+8LUIVrAgqcNW2s0PjAxHETgRn9l4fvX31ZxDxvwQnyMOX" crossorigin="anonymous"></script>

Note that you may want to consider not using CDNs in production.

In the cellClick event handler in your main.js file, add the following lines of code:

      const selectedResourceId = props.record.data.id;
      updateToolbarButton(selectedResourceId);

Here we get the resource ID from the props and pass it to the updateToolbarButton function.

Let’s define the updateToolbarButton function now. Add the following lines of code to the top of your main.js file:

function updateToolbarButton(selectedResourceId) {
  let toolbarButton = scheduler.widgetMap.extraInfoButton;
  if (toolbarButton) {
    toolbarButton.html = `<button hx-get="http://localhost:1338/api/extra-info?id=${selectedResourceId}" class="b-widget b-button b-text b-box-item b-green">Get extra info</button>`;
    // Manually process the updated part of the page with HTMX
    htmx.process(toolbarButton.element);
  }
}

We select the toolbar button widget using the widgetMap object. We then replace the HTML contents of the button to add a button with an hx-get htmx attribute. When this button is clicked, the hx-get attribute causes the button to make a GET request to our api/extra-info API endpoint. The HTML returned from our API endpoint is swapped into the innerHTML of the button. You can control the target of the swap and the swap type using the hx-target and hx-swap attributes. You can also control which events trigger the request by using the hx-trigger attribute.

We call the htmx.process() method to initialize the button element. You need to do this for any content added to the DOM by JavaScript with htmx attributes.

Now select a resource and click the toolbar button. You should see the extra information about the selected resource displayed in the button’s innerHTML:

You can see the GET requests made by htmx in your browser dev tools network tab.

Now let’s create a dialog to better display the extra information about our resources.

Create a dialog to display the extra information about a resource

We’ll use the HTML dialog element to create a dialog containing the extra information about the selected resource.

In your index.html file, add the following <dialog> element in the <body> element:

    <dialog>
      <div class="dialog-info"></div>
    </dialog>

In your main.js file, add the following query selector at the top of the file:

const dialog = document.querySelector("dialog");

At the bottom of the file, add the following event listener:

document.body.addEventListener("htmx:beforeSwap", (e) => {
  dialog.showModal();
});

The htmx:beforeSwap event occurs before htmx swaps new content into the DOM. This event opens the modal window when requesting extra information about a resource. We use the dialog element’s showModal() method to display the dialog.

By default, htmx loads the HTML response into the element that made the request. To load the response into a different element, change the target element using the hx-target attribute. The hx-target attribute takes a CSS selector as a value. We’ll make the target element for the extra resource information the <div> inside the <dialog> element. Add the following htmx attribute to the HTML button string in the updateToolbarButton function:

hx-target=".dialog-info"

Let’s also improve the styling of the extra information HTML returned. Add the following styles to your style.css file:

.dialog-info h3 {
  font-size: 2rem;
  margin: 0;
  margin-left: 1.2rem;
  padding: 0.5rem;
}
.dialog-info ul li {
  padding: 0.25rem;
}

Now when you click on the “Get extra info” button, you’ll see the extra information about the selected resource displayed in a dialog:

Press the escape key to close the dialog. Next, we’ll add a close button to our dialog and make the dialog close when you click outside of it.

Add a close button to the dialog

First, add the following <button> element inside the <dialog> element in your index.html file:

      <button class="dialog-close-button" aria-label="Close">
        <span aria-hidden="true" class="close"></span>
      </button>

Now add the following CSS styles in your style.css file:

dialog::backdrop {
  background-color: hsl(250, 100%, 50%, 0.25);
}
.dialog-close-button {
  position: absolute;
  display: flex;
  justify-content: center;
  align-items: center;
  top: 5px;
  right: 5px;
  height: 20px;
  width: 20px;
  padding: 3px;
  border: none;
  border-radius: 50%;
  background-color: transparent;
}
.dialog-close-button:hover {
  cursor: pointer;
  background-color: rgb(231, 230, 230);
}
.close {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%; 
}
.close::before, .close::after {
  position: absolute;
  content: ' ';
  height: 100%;
  width: 2px;
  background-color: rgb(50, 50, 50);
}
.close::before {
 transform: rotate(45deg);
}
.close::after {
  transform: rotate(-45deg);
}

For styling, we give the dialog background some color using the ::backdrop CSS pseudo-element.

We position the close button at the top right of the dialog and create the “X” close button using ::before and ::after CSS pseudo-elements.

In your main.js file, add the following event listener at the bottom:

document
  .querySelector(".dialog-close-button")
  .addEventListener("click", () => dialog.close());

When the close button is clicked, the dialog element’s close() method is called to close the dialog:

Close the dialog when a user clicks outside of the dialog

To close the dialog when a user clicks outside of the dialog, add the following "click" event listener at the bottom of the main.js file:

dialog.addEventListener("click", (e) => {
  const dialogDimensions = dialog.getBoundingClientRect();
  if (
    e.clientX < dialogDimensions.left ||
    e.clientX > dialogDimensions.right ||
    e.clientY < dialogDimensions.top ||
    e.clientY > dialogDimensions.bottom
  ) {
    dialog.close();
  }
});

We use the getBoundingClientRect() method to determine the size and position of the dialog in the viewport. If a user clicks outside of the dialog, the dialog closes:

Display a loading state in the extra information button using htmx and CSS

Lastly, let’s add a loading indicator to the button for getting extra information about a resource in case of a slow network connection. In the server.js file in your server, add the following line of code at the top of the route handler function for the /api/extra-info route:

  await new Promise((resolve) => setTimeout(resolve, 2000));

This will cause the API response to be two seconds slower.

In the frontend code, add the following <div> element in the updateToolbarButton() function in the main.js file. Place it just after the “Get extra info” text within the button element string:

 <div class="loading-indicator"></div>

In the style.css file, add the following CSS styles to create a spinning loading indicator:

.loading-indicator {
  display: none;
  width: 20px; 
  height: 20px;
  margin-left: 10px;
  border: 4px solid #f3f3f3;
  border-top: 4px solid #747474;
  border-radius: 50%;
  animation: spin 1s linear infinite;
}
@keyframes spin {
  100% { transform: rotate(360deg); }
}
.htmx-request .loading-indicator {
  display: block;
}

The spinning loading indicator is a <div> inside the button to get the extra resource information. It’s initially hidden. When htmx makes a request, it adds the htmx-request class to the loading indicator div to display the loading spinner in the button. You’ll now see a loading spinner when you request extra information about a resource:

Next steps

This tutorial covered the basics of using htmx with a Bryntum scheduler. Take a look at the htmx examples page to see demos of htmx functionality that you could add to your scheduler.

You can also check out the Bryntum Scheduler examples page to see many Bryntum Scheduler features in action, such as:

You can see a full list of the Bryntum Scheduler features here.

Bryntum

Bryntum Scheduler