Arsalan Khattak
17 January 2025

Creating a Bryntum Scheduler component with Refine and Next.js

Bryntum Scheduler is a feature-rich scheduling UI component that’s performant, highly customizable, and easily integrates with any backend or UI […]

Bryntum Scheduler is a feature-rich scheduling UI component that’s performant, highly customizable, and easily integrates with any backend or UI framework.

In this tutorial, we’ll show you how to add a React Bryntum Scheduler component to a Refine application that uses Next.js. Refine is an open-source React meta-framework for building enterprise-grade, CRUD-heavy applications like internal tools, admin panels, and dashboards.

We’ll do the following:

Here’s what we’ll build:

You can find the code for the completed tutorial in this GitHub repository.

Getting started: Clone the starter Refine Next.js app and populate a local SQLite database with example data

We’ll use the Bryntum Scheduler with Refine and Next.js starter template as a starting point. The completed-app branch contains the code for the completed tutorial.

Install the dependencies and populate a local SQLite database with the example data using Sequelize ORM by following the steps in the starter template’s README file.

This starter app uses the Refine Next.js starter template with a REST API data provider and two example pages of blog posts and categories. No UI framework or authentication logic has been added.

The Refine Next.js starter template has the following added code and data to populate a local SQLite database with example data:

For more details on using Bryntum Scheduler with Sequelize and SQLite, take a look at our guide to using Bryntum Scheduler with Express and SQLite.

Creating Next.js API route handlers to connect to the scheduler data

We’ll create four Next.js route handlers to load the data into a Bryntum Scheduler and sync data changes to the database. We’ll use the following HTTP methods and routes for the CRUD operations:

Here, [resource] is the Dynamic Segment for the type of scheduler data: event, resource, or assignment.

Before we create the route handlers, let’s add a Next.js middleware function to allow only certain routes in our API.

Adding middleware for API route validation

Create a middleware.ts file in the src folder and add the following lines of code to it:

import { NextResponse, NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
    const
        url = request.nextUrl,
        resource = url.pathname.split('/')[2], // Extract the resource name from the path (e.g., /api/[resources]/[id])
        allowedResources = ['events', 'resources', 'assignments'];
    // Check if the resource is allowed
    if (!allowedResources.includes(resource)) {
        return NextResponse.json({ error : 'Invalid resource type' }, { status : 400 });
    }
    // Continue the request if the resource is valid
    return NextResponse.next();
}
// Configuration to apply middleware to API routes matching the pattern
export const config = {
    matcher : '/api/:path*' // Apply middleware to all API routes
};

We use the middleware function to check that the dynamic route parameter [resource] is events, resources, or assignments. If it’s not, we return an error response.

Reading records

Create an api folder in the app folder. Add a [resource] folder to it to create a dynamic route segment. Create a route.ts folder inside the [resource] folder and add the following lines of code to it:

import { Assignment, Resource, Event } from '@/models';
import { AllowedAPIResources } from '@types';
import { NextRequest } from 'next/server';
export async function GET(req: NextRequest, { params }: { params: { id: string, resource: AllowedAPIResources } }) {
    const resource = params.resource;
    if (resource === 'resources') {
        try {
            const resources = await Resource.findAll();
            return Response.json(
                resources
            );
        }
        catch (error) {
            return new Response(
                'Loading resources failed',
                {
                    status : 400
                }
            );
        }
    }
    if (resource === 'events') {
        try {
            const events = await Event.findAll();
            return Response.json(
                events
            );
        }
        catch (error) {
            return new Response(
                'Loading events failed',
                {
                    status : 400
                }
            );
        }
    }
    if (resource === 'assignments') {
        try {
            const assignments = await Assignment.findAll();
            return Response.json(
                assignments
            );
        }
        catch (error) {
            return new Response(
                'Loading assignments failed',
                {
                    status : 400
                }
            );
        }
    }
}

We determine the type of resource from the dynamic route segment parameter and then use the Sequelize findAll method on the Resource, Event, or Assignment model to retrieve the records from the corresponding table in the SQLite database.

We need to create the AllowedAPIResources type used in this route handler. In the src folder, create a types.ts file and add the following type definition to it:

export type AllowedAPIResources = 'resources' | 'events' | 'assignments';

Creating records

Add the following POST request handler at the end of the src/app/api/[resource]/route.ts file:

export async function POST(req: NextRequest, { params }: { params: { id: string, resource: AllowedAPIResources } }) {
    const 
        resource = params.resource,
        reqBody = await req.json();
    if (resource === 'resources') {
        try {
            const resource = await Resource.create(reqBody);
            return Response.json({
                data : resource.dataValues
            });
        }
        catch (error) {
            return new Response(
                'Creating resource failed',
                {
                    status : 400
                }
            );
        }
    }
    if (resource === 'events') {
        try {
            const event = await Event.create(reqBody);
            return Response.json({
                data : event.dataValues
            });
        }
        catch (error) {
            return new Response(
                'Creating event failed',
                {
                    status : 400
                }
            );
        }
    }
    if (resource === 'assignments') {
        try {
            const assignment = await Assignment.create(reqBody);
            return Response.json({
                data : assignment.dataValues
            });
        }
        catch (error) {
            return new Response(
                'Creating assignment failed',
                {
                    status : 400
                }
            );
        }
    }
}

We use the Sequelize create() method to create an instance of the data model and save it to the database.

Deleting records

Create an [id] folder in the src/app/api/[resource] folder. Create a route.ts file in the src/app/api/[resource]/[id] folder and add the following route handler to the route.ts file:

import { Assignment, Event, Resource } from '@/models';
import { AllowedAPIResources } from '@types';
import { NextRequest } from 'next/server';
export async function DELETE(req: NextRequest, { params }: { params: { id: string, resource: AllowedAPIResources } }) {
    const 
        resource = params.resource,
        id = params.id;
    if (resource === 'resources') {
        try {
            await Resource.destroy({ where : { id } });
            return Response.json({ success : true });
        }
        catch (error) {
            return new Response('Deleting resource failed', {
                status : 400
            });
        }
    }
    if (resource === 'events') {
        try {
            await Event.destroy({ where : { id } });
            return Response.json({ success : true });
        }
        catch (error) {
            return new Response('Deleting event failed', {
                status : 400
            });
        }
    }
    if (resource === 'assignments') {
        try {
            await Assignment.destroy({ where : { id } });
            return Response.json({ success : true });
        }
        catch (error) {
            return new Response('Deleting assignment failed', {
                status : 400
            });
        }
    }
}

The DELETE request route handler uses the Sequelize destroy method to delete a database record by ID.

Updating records

Add the following PATCH request route handler below the DELETE request route handler in src/app/api/[resource]/[id]/route.ts:

export async function PATCH(req: NextRequest, { params }: { params: { id: string, resource: AllowedAPIResources } }) {
    const 
        resource = params.resource,
        id = params.id,
        reqBody = await req.json();
    if (resource === 'resources') {
        try {
            Resource.update(reqBody, { where : { id } });
            return Response.json({ success : true });
        }
        catch (error) {
            return new Response(
                'Updating resource failed',
                {
                    status : 400
                }
            );
        }
    }
    if (resource === 'events') {
        try {
            Event.update(reqBody, { where : { id } });
            return Response.json({ success : true });
        }
        catch (error) {
            return new Response(
                'Updating event failed',
                {
                    status : 400
                }
            );
        }
    }
    if (resource === 'assignments') {
        try {
            Assignment.update(reqBody, { where : { id } });
            return Response.json({ success : true });
        }
        catch (error) {
            return new Response(
                'Updating assignment failed',
                {
                    status : 400
                }
            );
        }
    }
}

We use the Sequelize update method to update the database records.

Creating a Refine data provider to handle all data fetching and mutation operations

The Refine data provider handles data fetching and data mutation HTTP requests. Data provider methods make requests to API endpoints to perform CRUD operations. Refine provides data hooks to call these methods. The data hooks use TanStack Query to manage data fetching.

Refine has built-in data provider support for popular API providers, such as the simple REST API data provider that the starter app uses. This data provider is designed to be used with REST APIs that follow the standard API design but can be customized. We’ll create another data provider from scratch for the scheduler data with all the required CRUD methods.

Fetching all records

In the src/providers folder, create a folder called scheduler-data-provider. Create an index.ts file in it and add the following lines of code to the file:

'use client';
import type { DataProvider } from '@refinedev/core';
const API_URL = 'http://localhost:3000/api';
export const schedulerDataProvider: DataProvider = {
    getList : async({ resource }) => {
        const response = await fetch(`${API_URL}/${resource}`);
        if (response.status < 200 || response.status > 299) throw response;
        const data = await response.json();
        return {
            data,
            total : data.length
        };
    },
};

We create a data provider and define the getList method. We use the fetch API to get the scheduler data from the Next.js API route, /api/[resource], GET request handler we created.

To use this data provider in our app, we need to add it to the dataProvider prop in the <Refine> component.

Using multiple data providers

In the src/app/layout.tsx file, change the value of the dataProvider prop in the <Refine> component to the following:

dataProvider={{
    default   : dataProvider, 
    scheduler : schedulerDataProvider
}}

We pass in the data providers as key-value pairs in the dataProvider prop. The default key is required for defining the default data provider. Note that the <Refine> component needs to be used in a client component as it uses React context and state.

Import schedulerDataProvider along with the other imports at the top of the src/app/layout.tsx file:

import { schedulerDataProvider } from '@providers/scheduler-data-provider';

Creating a record

Add the following create method to the schedulerDataProvider in the src/providers/scheduler-data-provider/index.ts file below the getList property:

create : async({ resource, variables }) => {
    const response = await fetch(`${API_URL}/${resource}`, {
        method  : 'POST',
        headers : {
            'Content-Type' : 'application/json'
        },
        body : JSON.stringify(variables)
    });
    if (response.status < 200 || response.status > 299) throw response;
    return await response.json();
},

This method will make a POST request to the /api/[resource]/[id] route in the Next.js API.

Deleting a record

Add the following deleteOne method to schedulerDataProvider in the src/providers/scheduler-data-provider/index.ts file:

deleteOne : async({ resource, id, variables }) => {
    const response = await fetch(`${API_URL}/${resource}/${id}`, {
        method : 'DELETE'
    });
    if (response.status < 200 || response.status > 299) throw response;
    return await response.json();
},

This method will make a DELETE request to the /api/[resource]/[id] route in the Next.js API.

Updating a record

Add the following update method to the schedulerDataProvider in the src/providers/scheduler-data-provider/index.ts file:

update : async({ resource, id, variables }) => {
    const response = await fetch(`${API_URL}/${resource}/${id}`, {
        method  : 'PATCH',
        body    : JSON.stringify(variables),
        headers : {
            'Content-Type' : 'application/json'
        }
    });
    if (response.status < 200 || response.status > 299) throw response;
    const data = await response.json();
    return { data };
},

This method will make a PATCH request to the /api/[resource]/[id] route in the Next.js API.

You’ll notice that schedulerDataProvider has a type error. This is because we haven’t defined all the required methods. Add the following methods to schedulerDataProvider:

getOne : () => {
    throw new Error('Not implemented');
},
getApiUrl : () => API_URL

We don’t need these methods for the Bryntum Scheduler we’ll create, but they are required.

Creating a scheduler events page

Let’s add an example scheduler events page. In the src/app/layout.tsx file, add the following resource object to the resources prop array of the <Refine> component:

{
    name : 'events',
    list : '/events',
    meta : {
        dataProviderName : 'scheduler'
    }
}

A Refine resource represents an entity, usually a data entity. The list, create, edit, and show properties are used to define the Next.js routes for the corresponding CRUD actions.

Let’s create an example page for the events resource. In the src/app folder, create an events folder and add a layout.tsx file to it. Paste the following lines of code to it:

import { Layout as BaseLayout } from '@components/layout';
import React from 'react';
export default async function Layout({ children }: React.PropsWithChildren) {
    return <BaseLayout>{children}</BaseLayout>;
}

This creates the base layout, which adds the page menu to the left of the page and the breadcrumbs navigation to the top of the page.

Create a page.tsx file in the src/app/events folder and add the following lines of code to it:

'use client';
import { useList } from '@refinedev/core';
export default function Events() {
    const { data } = useList({
        resource : 'events'
    });
    return (
        <div style={{ height : '100vh', padding : '1rem' }}>
            <h1>Events</h1>
            <ul style={{ padding : '1rem' }}>
                {data?.data?.map((event) => (
                    <li key={event.id}>
                        {event.name}
                    </li>
                ))}
            </ul>
        </div>
    );
}

We use the Refine useList data hook to fetch the events data. This data hook is an extended version of the TanStack Query useQuery hook. The useList data hook uses the getList method we created in the scheduler data provider as the query function. Data is cached using a query key, which is generated from the provided useList properties. You can see the query key using Refine Devtools.

Using Refine Devtools

The @refinedev/devtools package has been added to the starter app, so you don’t need to manually install it. To use it, the <DevtoolsProvider /> component must be wrapped around the <Refine /> component in the RootLayout component in src/app/layout.tsx, which has been done already.

Run the local dev server using the following command:

npm run dev

Open http://localhost:3000/ and you’ll see a small Devtools tab in the footer of the application. Navigate to the events page and click on the Devtools tab to open the Devtools screen, and then open the Monitor screen from the sidebar menu. You will need to sign in to connect Refine with your GitHub or Google account.

In the Monitor screen, you’ll see all the queries and mutations triggered in the application for the current session, including the useList data hook call from the events page that fetched the events data from the API endpoint using the scheduler data provider’s getList method.

Learn more about using Refine Devtools, which is in beta at the time of writing, in the Refine tutorial.

Creating a Bryntum Scheduler component

First, install Bryntum Scheduler by following this guide accessing the Bryntum npm repository. Once you’ve logged in to the registry, install the Bryntum Scheduler packages.

Now let’s create a basic Bryntum Scheduler component.

In the src/components folder, create a BryntumScheduler folder. Add an index.tsx file to it and add the following lines of code to the file:

'use client';
import { BryntumScheduler } from '@bryntum/scheduler-react';
import { useEffect, useRef } from 'react';
import '@bryntum/scheduler/scheduler.stockholm.css';
export default function Scheduler({ ...props }) {
    const schedulerRef = useRef<BryntumScheduler>(null);
    useEffect(() => {
        // Bryntum Scheduler instance
        const scheduler = schedulerRef?.current?.instance;
    }, []);
    return (
        <div id="app">
            <BryntumScheduler
                ref={schedulerRef}
                {...props}
            />
        </div>
    );
}

The schedulerRef allows you to access the Bryntum Scheduler instance. We don’t use this in this tutorial, but it can be useful.

Creating a Bryntum Scheduler configuration file

Next, create a Bryntum Scheduler configuration file. Create a schedulerConfig.ts file in the src/config folder, and add the following lines of code to it:

const schedulerConfig = {
    startDate                 : new Date(2024, 9, 1),
    zoomOnMouseWheel          : false,
    zoomOnTimeAxisDoubleClick : false,
    viewPreset                : 'hourAndDay',
    workingTime : {
        fromHour : 8,
        toHour   : 17
    },
    columns : [
        {
            type      : 'resourceInfo',
            text      : 'Name',
            field     : 'name',
            width     : 150,
            showImage : false
        }
    ]
};
export { schedulerConfig };

The scheduler will have a single column called “Name” and show only the set workingTime. When using the workingTime feature, the Zooming feature is not supported. This is why the zooming controls zoomOnMouseWheel and zoomOnTimeAxisDoubleClick are disabled.

Bryntum components are client-side only and Next.js uses server-side rendering (SSR). To make the Bryntum Scheduler client-side only, we’ll import the BryntumScheduler component dynamically to ensure that it’s only rendered on the client.

Creating a wrapper component to render the Bryntum Scheduler on the client only

Now, let’s create a scheduler wrapper component to dynamically import the BryntumScheduler component. In the src/components folder, create a schedulerWrapper folder. Create an index.tsx file in the src/components/schedulerWrapper folder and add the following lines of code to it:

import dynamic from 'next/dynamic';
import { schedulerConfig } from '../../config/schedulerConfig';
const Scheduler = dynamic(() => import('../BryntumScheduler'), {
    ssr     : false,
    loading : () => {
        return (
            <div
                style={{
                    display        : 'flex',
                    alignItems     : 'center',
                    justifyContent : 'center',
                    height         : '100vh'
                }}
            >
                <p>Loading...</p>
            </div>
        );
    }
});
const SchedulerWrapper = () => {
    return (
        <>
            <Scheduler {...schedulerConfig} />
        </>
    );
};
export { SchedulerWrapper };

We dynamically import the BryntumScheduler component with ssr set to false. While the component is being imported, a loading message is displayed.

Creating a scheduler page

Let’s create a page for our Bryntum Scheduler. Create a scheduler folder in the src/app folder. Create a layout.tsx file in the src/app/scheduler and add the following lines of code to it:

import { Layout as BaseLayout } from '@components/layout';
import React from 'react';
export default async function Layout({ children }: React.PropsWithChildren) {
    return <BaseLayout>{children}</BaseLayout>;
}

Create a page.tsx file in the src/app/scheduler folder and add the following lines of code to it:

'use client';
import { SchedulerWrapper } from '@components/schedulerWrapper';
import { useList } from '@refinedev/core';
export default function SchedulerComponent() {
    return (
        <div>
            <SchedulerWrapper />
        </div>
    );
}

The page is a client component. Client components are prerendered on the server, which is why we need the scheduler wrapper.

Add the following styles to the src/styles/global.css file to make the Bryntum Scheduler take up the full height of the page:

* {
  box-sizing: border-box;
  padding: 0;
  margin: 0;
}
#app {
  margin: 0;
  display: flex;
  flex-direction: column;
  height: 100vh;
  font-size: 14px;
}

In the src/app/layout.tsx file, add the scheduler route to the resources prop of the <Refine> component:

{
    name : 'scheduler',
    list : '/scheduler',
    meta : {
        dataProviderName : 'scheduler'
    }
},

To prevent the “scheduler” menu item from being pluralized to “schedulers”, make the following change to the rendered <Link> text in the menu component in src/components/menu/index.tsx:

- {item.label}
+ {item.name}

Run the local development server and you’ll see an empty Bryntum Scheduler on the scheduler page.

Fetching the scheduler data and adding it to the Bryntum Scheduler

Add the following useList data hook calls to the Scheduler component in the src/components/BryntumScheduler/index.tsx file above the line const schedulerRef = useRef<BryntumScheduler>(null);:

const
    { data: dataResources } = useList({
        resource         : 'resources',
        dataProviderName : 'scheduler'
    }),
    { data: dataEvents } = useList({
        resource         : 'events',
        dataProviderName : 'scheduler'
    }),
    { data: dataAssignments } = useList({
        resource         : 'assignments',
        dataProviderName : 'scheduler'
    });

The useList data hook queries the scheduler data using the getList method in the scheduler data provider.

Add the following variables below the useList data hook calls:

const
    events = useMemo(() => dataEvents?.data || [], [dataEvents]),
    assignments = useMemo(() => dataAssignments?.data || [], [dataAssignments]),
    resources = useMemo(() => dataResources?.data || [], [dataResources]);

Pass the data as props in the <BryntumScheduler> component returned in the src/components/BryntumScheduler/index.tsx file:

events={events}
assignments={assignments}
resources={resources}

We use useMemo to ensure the <BryntumScheduler> component receives the latest updated data. Without it, the scheduler data won’t update when the useList data fetch completes.

Make sure to import useMemo and useList:

import { useMemo } from 'react';
import { useList } from '@refinedev/core';

You’ll now see the events, assignments, and resources data in the scheduler:

Syncing data changes with the Bryntum Scheduler onDataChange event

We’ll use the Bryntum Scheduler onDataChange event to sync data changes in the scheduler UI with the SQLite database.

Creating a syncData function to sync data changes

Add the following onDataChange prop to the BryntumScheduler component returned in the src/components/BryntumScheduler/index.tsx file:

onDataChange={syncData}

When a data change occurs in the Bryntum Scheduler, the dataChange event will be fired and the syncData function will be called.

Let’s define the skeleton of this function within the Scheduler function in src/components/BryntumScheduler/index.tsx:

const syncData = ({ store, action, records }: SyncData) => {
    const storeId = store.id;
    if (storeId === 'resources') {
        if (action === 'add') {
        }
        if (action === 'remove') {
        }
        if (action === 'update') {
        }
    }
    if (storeId === 'events') {
        if (action === 'remove') {
        }
        if (action === 'update') {
        }
    }
    if (storeId === 'assignments') {
        if (action === 'add') {
        }
        if (action === 'update') {
        }
    }
};

We get information about the store, action, and records from the dataChange event. The store is used to determine which data store has been changed, "resources", "events", or "assignments". The action determines the type of data change, "add", "remove", or "update". We won’t use the "add" event when an event is created, we’ll create the event when it’s updated. This is because when an event is created in the Bryntum Scheduler, an "add" event occurs and then the event editor menu is opened. An "update" event occurs when the new scheduler event is edited. We’ll create the event when this "update" occurs.

Now add the following type imports to the top of the src/components/BryntumScheduler/index.tsx file:

import { ResourceModel, EventModel, AssignmentModel, Grid, Store, Model } from '@bryntum/scheduler';

Add the SyncData interface that we use for the syncData function below the imports in the src/components/BryntumScheduler/index.tsx file:

interface SyncData {
  source: Grid;
  store: Store;
  action: 'remove' | 'removeAll' | 'add' | 'clearchanges' | 'filter' | 'update' | 'dataset' | 'replace';
  records: Model[];
  changes: object;
  }[];

Import the Refine data hooks we’ll use to create, update, and delete records:

import { useCreate, useDelete, useUpdate } from '@refinedev/core';

Add the following lines of code in the Scheduler function:

const
    { mutate: mutateCreate } = useCreate(),
    { mutate: mutateDelete } = useDelete(),
    { mutate: mutateUpdate } = useUpdate();

These data hooks are extended versions of the TanStack Query useMutation hook. We’ll use the returned mutate method to create, delete, and update the SQLite database using the CRUD methods we created in the scheduler data provider.

When calling the mutate method, we’ll add the following arguments to indicate which records to mutate:

Creating resources

In the syncData function, where storeId === 'resources' is and the action is 'add', add the following lines of code:

const resourcesIds = resources.map((obj) => obj.id);
for (let i = 0; i < records.length; i++) {
    const record = records[i] as ResourceModel;
    const recordData = (record as any).data as ResourceModel;
    const resourceExists = resourcesIds.includes(recordData.id);
    if (resourceExists) return;
    const { id, ...newResource } = recordData as ResourceModel;
    mutateCreate({
        resource         : 'resources',
        dataProviderName : 'scheduler',
        values           : newResource
    });
}

We loop through the added records objects, and for each of them, we run the mutateCreate method. If the resource already exists in the data from the useList query, we don’t create a new resource.

If you create a new resource by copying and pasting an existing resource in the Bryntum Scheduler UI, all of the resources will be fetched again. You can see this in your browser dev tools Network tab.

After each create, delete, and update operation, the store data is invalidated and refetched. If you need better performance, you can update the data cache on the client instead of refetching data when a mutation is successful. You can use the onSuccess option from mutationOptions and Tanstack Query queryClient to interact with a cache.

Deleting resources

In the syncData function, where storeId === 'resources' is and the action is 'remove', add the following lines of code:

const record = records[0] as ResourceModel;
const recordData = (record as any).data as ResourceModel;
if (`${recordData?.id}`.startsWith('_generated')) return;
records.forEach((rec) => {
    mutateDelete({
        resource         : 'resources',
        dataProviderName : 'scheduler',
        id               : rec.id
    });
});

For each record that's removed, we call the mutateDelete method.

Updating resources

In the syncData function, where storeId === 'resources' is and the action is 'update', add the following lines of code:

for (let i = 0; i < records.length; i++) {
    const record = records[i] as ResourceModel;
    const recordData = (record as any).data as ResourceModel;
    if (`${records[i].id}`.startsWith('_generated')) return;
    const modifiedVariables = (records[i] as any).meta
        .modified as Writable>;
    (Object.keys(modifiedVariables) as Array).forEach(
        (key) => {
            modifiedVariables[key] = (recordData)[
                key
            ];
        }
    );

    mutateUpdate({
        resource         : 'resources',
        dataProviderName : 'scheduler',
        id               : recordData.id,
        values           : {
            ...modifiedVariables
        }
    });
}

For each updated resource, we pass the modified variables to the mutateUpdate mutation method.

Deleting events

In the syncData function, where storeId === 'events' is and the action is 'remove', add the following lines of code:

const record = records[0] as ResourceModel;
const recordData = (record as any).data as ResourceModel;
if (`${recordData?.id}`.startsWith('_generated')) return;
records.forEach((rec) => {
    mutateDelete({
        resource         : 'events',
        dataProviderName : 'scheduler',
        id               : rec.id
    });
});

For each record that's removed, we call the mutateDelete method.

Creating and updating events

We’ll run the mutateEvent mutation when a newly created event is first updated. To do this, we’ll create a disableCreate flag variable. We’ll determine if an event is newly created by checking if its id starts with '_generated'.

We need to disable the mutateCreate mutation when an event is created using the Bryntum Scheduler EventDragCreate feature. This feature allows users to create events by clicking and dragging in the Bryntum Scheduler timeline. We disable creating the event in this case because an 'add' event followed by an 'update' event occurs. We’ll prevent this update from triggering a mutateCreate mutation so that an event is only created when a user clicks the "SAVE" button in the event editor popup menu.

Create a disableCreate variable below the schedulerRef variable and set it to false:

let disableCreate = false;

Add onBeforeDragCreate and onAfterDragCreate event listeners to the BryntumScheduler component:

onBeforeDragCreate={onBeforeDragCreate}
onAfterDragCreate={onAfterDragCreate}

Add the following functions below the disableCreate variable:

function onBeforeDragCreate() {
    disableCreate = true;
}
function onAfterDragCreate() {
    disableCreate = false;
}

This sets the disableCreate variable to true during a drag-create event.

Now add the following code in the syncData function where storeId === 'events' is and the action is 'update':

if (disableCreate) return;
for (let i = 0; i < records.length; i++) {
    const record = records[0] as EventModel;
    const recordData = (record as any).data as EventModel;
    if (`${recordData.id}`.startsWith('_generated')) {
        const eventsIds = events.map((obj) => obj.id);
        for (let i = 0; i < records.length; i++) {
            const eventExists = eventsIds.includes(recordData.id);
            if (eventExists) return;
            const { id, ...newEvent } = recordData as EventModel;
            // get current resource
            const resourceId = (schedulerRef?.current?.instance?.selectedRecords[0] as any).data.id;
            mutateCreate({
                resource         : 'events',
                dataProviderName : 'scheduler',
                values           : newEvent
            }, {
                onSuccess : ({ data }) => {
                    return mutateCreate({
                        resource         : 'assignments',
                        dataProviderName : 'scheduler',
                        values           : { eventId : data.id, resourceId }
                    }, {
                        onSuccess : (response) => {
                            console.log('Assignment created:', response);
                        },
                        onError : (error) => {
                            console.error('Error creating assignment:', error);
                        }
                    });
                },
                onError : (error) => {
                    console.error('Error creating event:', error);
                },
                onSettled : () => {
                    // Handle completion regardless of success/failure
                    console.log('Event creation settled');
                }
            });
        }
    }
    else {
        const modifiedVariables = (records[i] as any).meta
            .modified as Writable>;
        (Object.keys(modifiedVariables) as Array).forEach(
            (key) => {
                modifiedVariables[key] = (recordData)[
                    key
                ];
            }
        );

        mutateUpdate({
            resource         : 'events',
            dataProviderName : 'scheduler',
            id               : recordData.id,
            values           : {
                ...modifiedVariables
            }
        });
    }
}

If an event is a newly created event record and the 'update' is not caused by a drag-create event, we create a new event using the mutateCreate mutation. If the event ID does not start with _generated, we call the mutateUpdate mutation and pass in the modified variables.

Creating assignments

In the syncData function, where storeId === 'assignments' is and the action is 'add', add the following lines of code:

const assignmentIds = assignments.map((obj) => obj.id);
for (let i = 0; i < records.length; i++) {
    const record = records[0] as AssignmentModel;
    const recordData = (record as any).data as AssignmentModel;
    const assignmentExists = assignmentIds.includes(recordData.id);
    if (assignmentExists) return;
    if (disableCreate) return;
    const { eventId, resourceId } = recordData as AssignmentModel;
    if (`${eventId}`.startsWith('_generated') || `${resourceId}`.startsWith('_generated')) return;
    mutateCreate({
        resource         : 'assignments',
        dataProviderName : 'scheduler',
        values           : { eventId, resourceId }
    });
}

For each created assignment, we create an assignment using the mutateCreate mutation.

Updating assignments

In the syncData function, where storeId === 'assignments' is and the action is 'update', add the following lines of code:

for (let i = 0; i < records.length; i++) {
    const record = records[0] as AssignmentModel;
    const recordData = (record as any).data as AssignmentModel;
    if (`${recordData.id}`.startsWith('_generated')) return;
    mutateUpdate({
        resource         : 'assignments',
        dataProviderName : 'scheduler',
        id               : recordData.id,
        values           : {
            eventId    : recordData.eventId,
            resourceId : recordData.resourceId
        }
    });
}

For each updated assignment, we pass the modified variables to the mutateUpdate mutation function method.

Deleting assignments

The Sequelize Assignment data model in src/models/Assignment.ts has an onDelete property for the events and resources foreign keys that deletes all associated assignments when an event or resource is deleted so we don't need an assignments mutateDelete function call.

Now run the local dev server. Data changes in your Bryntum Scheduler will be saved to the local SQLite database:

Next steps

This tutorial gives you a starting point for using a Bryntum Scheduler in a Refine Next.js app. Refine also offers features like authentication, integration with various services (such as data providers), support for real-time projects, and access to multiple community packages.

Our Bryntum Scheduler demo page will give you an idea of what additional features you can add to your scheduler.

You can also learn more about the Bryntum Scheduler from our blog posts:

Arsalan Khattak

Bryntum Scheduler