v7.3.0

Displaying data in the Calendar

Every Bryntum component uses Store data containers for holding data. Store is then further extended to have ResourceStore and EventStore etc.

Bryntum Calendar uses the following Stores to hold data.

Store Description
ResourceStore Holds a collection of resources
EventStore Holds a collection of events
AssignmentStore Holds a collection of assignments
TimeRangeStore Holds a collection of time ranges
ResourceTimeRangeStore Holds a collection of resource time ranges

A store uses a Model as the blueprint for each row (called record) it holds.

Store Model
ResourceStore ResourceModel
EventStore EventModel
AssignmentStore AssignmentModel
TimeRangeStore TimeRangeModel
ResourceTimeRangeStore ResourceTimeRangeModel

Similar to the Store, Model is also extended as ResourceModel, EventModel and so on.

Working with data

Bryntum Calendar offers multiple ways to work with data, depending on your setup:

If you're using inline data, or data already loaded in a custom way, you can input it directly into the Calendar instance. For backend server data, an API call can fetch the data. We’ll cover this in more detail later.

The Calendar project

The Calendar's stores are linked to each other using a project. The project can be thought of as the complete dataset available to the Calendar: all events, resources, and assignments under a single "parent".

The project is responsible for:

  • Making the stores available to the Calendar
  • Calculating dates and durations asynchronously using its calculation engine
  • Keeping references between records up to date (e.g., which resources an event is assigned to)
  • Optionally working as a CrudManager

You will learn more about it in a while studying Using inline or preloaded data.

In normal UI usage, you might not need to interact much with the project, but it's good to know it's there. If needed, you can access it using calendar.project.

Using inline or preloaded data

If you have inline data, or data already loaded in a custom way, you can supply it directly when creating a calendar. It is expected to be an array of JavaScript/JSON objects.

const calendar = new Calendar({
    resources : [
        { id : 1, name : 'Batman' },
        { id : 2, name : 'Wolverine' },
        /*...*/
    ],

    events : [
        { id : 1, resourceId : 1, name : 'Fight crime', startDate : new Date(2018,4,1,9,00), endDate : new Date(2018,4,1,17,00) },
        { id : 2, resourceId : 1, name : 'Attend banquet', startDate : new Date(2018,4,1,20,00), endDate : new Date(2018,4,1,23,00) },
        { id : 3, resourceId : 2, name : 'Drink beer', startDate : new Date(2018,4,1,9,00), duration : 8, durationUnit : 'hour' },
        /*...*/
    ]
});

If you need more control over the created stores, you can supply store config objects (for info on available configs, see API docs):

const calendar = new Calendar({
    resourceStore : {
        sorters : [
            { field : 'name' }      
        ],
        data : [
            { id : 1, name : 'Superman' },
            { id : 2, name : "Batman" },
            { id : 3, name : "Spiderman" },
            { id : 4, name : "Hulk" },
            /*...*/
        ] 
    },
    /*...*/
});

The above example will sort the resources in ascending order. Alternatively, you can supply an already existing store instance:

const resourceStore = new ResourceStore({
    someConfig : "...",
    data       : [
        { id : 1, name : 'Batman' },
        /*...*/
    ]
});

const calendar = new Calendar({
    resourceStore
});

Another option is to use project:

// Inline project data
const calendar = new Calendar({
  project : {
    events      : [/*...*/],
    resources   : [/*...*/],
    assignments : [/*...*/]
  }
});

// - or -

const project = new ProjectModel({
  events      : [/*...*/],
  resources   : [/*...*/],
  assignments : [/*...*/]
});

const calendar = new Calendar({
  project
});

This will create a ResourceStore and an EventStore holding the data. You can access the stores later:

calendar.resourceStore.sort('name');
calendar.eventStore.removeAll();

To view the data, use:

console.log(calendar.resourceStore.toJSON());
console.log(calendar.eventStore.toJSON());

If the data is not available at configuration time, and you do not want to use the remote loading capabilities described below, you can load data any custom way you want and then plug it into the store later:

const calendar = new Calendar({
    /*...*/
});

// Using native fetch to load data
const response = await fetch('backend/loadResources.php');
const data = await response.json();

// Maybe do some custom processing before plugging into calendar's store
data.forEach((row, index) => {
    row.index = index;
    row.someValue = Math.random();
    /*...*/
});

// Plug it in as inline data
calendar.resourceStore.data = data;

Loading remote data over HTTP(S)

Both ResourceStore and EventStore are based on AjaxStore, which can load remote data. There are multiple options to load remote data. You can supply a store config containing a readUrl:

const calendar = new Calendar({
    resourceStore : {
        readUrl : 'backend/loadResources.php', 
        autoLoad : true // Load upon creation
    }
});

Or create the store prior to creating the Calendar:

const resourceStore = new ResourceStore({
   readUrl : 'backend/loadResources.aspx'
});

const calendar = new Calendar({
    resourceStore
});

store.load();

The data returned from the backend is expected to have the following format by default:

{
  "success": true,
  "data": [
    { "id": 1, "name": "Batman" }
  ]
}

Using CrudManager

Calendar ships with a helpful class called CrudManager, that allows you to load (and later sync) multiple stores in a single request to the backend. Set it up like this:

const calendar = new Calendar({
    crudManager : {
        autoLoad : true,
        autoSync : true,
        loadUrl  : 'backend/load.php',
        syncUrl  : 'backend/sync.php'
    }
});

For more information, see the CrudManager guide and the API docs.

Framework 2-way binding

For some framework users, where the data property of the Calendar has been bound to a state-monitored data source, implementing the requestData function is not a viable option. In these cases, it is better to add a listener to the requestData event instead.

The main difference is that a requestData event listener cannot return the data directly. Instead, the data property should be updated (which will be done by the framework), and if the Store is paged, the totalCount property be set (will not be done by the framework).

const store = new Store({
    remoteFilter : true,
    remoteSort   : true,
    remotePaging : true,
    listeners: {
        requestData({
            source,
            filters,
            sorters,
            page,
            pageSize
        }) {
            let filteredRecords = [...allRecords];

            filters?.forEach(filter => {
                const {
                    field,
                    operator,
                    value,
                    caseSensitive
                } = filter;

                if (operator === '=') {
                    filteredRecords = filteredRecords.filter(r => r[field] === value);
                }
                else {
                    /// ... implement other filter operators
                }
            });

            sorters?.forEach(sorter => filteredRecords.sort((a, b) => {
                const {
                    field,
                    ascending
                } = sorter;

                if (!ascending) {
                    ([b, a] = [a, b]);
                }

                return a[field] > b[field] ? 1 : (a[field] < b[field] ? -1 : 0)
            }));

            const start = (page - 1) * pageSize;
            const data = filteredRecords.splice(start, start + pageSize);

            source.data = data;
            source.totalCount = filteredRecords.length;
        }
    }
})
const App = props => {
    const calendarprops = {
        remoteFilter : true,
        remoteSort   : true,
        remotePaging : true,
    }

    function App() {
        const ref = useRef<BryntumCalendar>();

        // Data managed by Redux
        const data = useSelector((state : RootState) => state.data.rows);
        const total = useSelector((state: RootState) => state.data.total);
        const dispatch : AppDispatch = useDispatch();

        useEffect(() => {
            const calendar = gridRef.current.instance;
            const store = calendar.store as Store;

            // Listen to the Store's requestData function to be able to intercept data requests
            store.on({
                requestData({ page, pageSize, sorters, filters } : { page:number; pageSize:number; sorters:Array<any>; filters:Array<any> }) {
                    // dispatch is a Redux thing, and loadData is a Redux data slice
                    dispatch(loadData({ page, pageSize, sorters, filters }));
                }
            });

            store.loadPage(1, {});
        }, []);

        return (
            <BryntumCalendar
                ref={ref}
                {...calendarProps}
                data={data}
            />
        );
    }
}
<template>
    <bryntum-calendar
        ref="calendarRef"
        v-bind="calendarConfig"
        :data="data"
    />
</template>

<script setup lang="ts">
    import { ref, onMounted } from 'vue';
    import { useSelector, useDispatch } from 'vuex';
    import { BryntumCalendar } from '@bryntum/calendar-vue-3';

    const calendarRef = ref(null);
    const calendarConfig = {
        store : {
            remoteFilter: true,
            remoteSort: true,
            remotePaging: true,
    }
};

    // Data managed by Vuex
    const data = useSelector((state) => state.data.rows);
    const dispatch = useDispatch();

    onMounted(() => {
        const calendar = calendarRef.value.instance;
        const store = calendar.store;

        // Listen to the Store's requestData function to be able to intercept data requests
        store.on({
            requestData({ page, pageSize, sorters, filters }) {
            dispatch('loadData', { page, pageSize, sorters, filters });
        },
    });

    store.loadPage(1, {});
});
</script>
import { Component, OnInit, ViewChild } from '@angular/core';
import { BryntumGridComponent } from '@bryntum/grid-angular';
import { Store } from '@ngrx/store';

@Component({
    selector: 'app-my-component',
    template: `
    <bryntum-grid #gridRef [store]="storeConfig!" [data]="data!"></bryntum-grid>
  `,
})
export class MyComponent implements OnInit {
    @ViewChild('calendarRef') calendarRef!: BryntumCalendarComponent;
    data: any[] = []; //
    storeConfig: any = {
        remoteFilter: true,
        remoteSort: true,
        remotePaging: true,
    };

    constructor(private store: Store) {} // Inject NgRx Store

    ngOnInit(): void {
        const calendar = this.calendarRef.instance;
        const bryntumStore = calendar.store;

        // Listen to the Store's requestData function to be able to intercept data requests
        bryntumStore.on({
            requestData({ page, pageSize, sorters, filters }) {
                this.store.dispatch({ type: '[Data] Load Data', payload: { page, pageSize, sorters, filters } });
            },
        });

        bryntumStore.loadPage(1, {});
    }
}

Customizing Calendar stores

There are multiple ways to customize a Calendar store. The easiest way is to pass a configuration object in the Calendar instance:

const calendar = new Calendar({
    // other config
    assignmentStore : {
        allowNoId : false,
        createUrl : '/create.php',
        readUrl   : '/read.php',
        updateUrl : '/update.php',
        deleteUrl : '/delete.php'
    }
});

Another way is to create a new store instance with custom configurations, useful for reusing it in multiple places:

const customAssignmentStore = new AssignmentStore({
    allowNoId : false,
    createUrl : '/create.php',
    readUrl   : '/read.php',
    updateUrl : '/update.php',
    deleteUrl : '/delete.php'
});

Next, assign it in the Calendar instance:

const calendar = new Calendar({
    // other config
    assignmentStore : customAssignmentStore
});

You can subclass it if you are going to use it in multiple places or to organize the code better:

class CustomEventStore extends EventStore {
    static $name = 'CustomEventStore';
    static configurable = {
        allowNoId : false,
        createUrl : '/create.php',
        readUrl   : '/read.php',
        updateUrl : '/update.php',
        deleteUrl : '/delete.php',
        tree         : true
    };
}

Then create a new instance of it and pass it the Calendar instance:

const customEventStore = new CustomEventStore();

const calendar = new Calendar({
    // other config
    eventStore : customEventStore
});

You can confirm it doing console.log(calendar.eventStore).

Populating multiple stores at once

If your app doesn't use CrudManager (nor AjaxStore) to load data, you can still use it to populate all Calendar stores in a single call with data fetched through other means. Depending on your setup, this might be more convenient than populating one store at the time.

To enable this, you need to configure your Calendar with an "inactive" CrudManager, by not supplying any urls for it:

const calendar = new Calendar({
    crudManager : {},
    ...
})

You can then populate all stores at once by calling loadCrudManagerData():

calendar.crudManager.loadCrudManagerData(data);

The data is expected to follow the CrudManager format, with one section per store being populated (can be JSON):

{
   resources : {
      rows : [ ... ]
   },
   events : {
      rows : [ ... ]
  },
  // ... more stores ... 
}

For example:

calendar.crudManager.loadCrudManagerData({
   events : {
      rows : [
         { id : 1, name : 'Important meeting', startDate : '2053-10-23', duration : 1 }, 
         { id : 2, name : 'Travel', startDate : '2053-10-24', duration : 4 }
      ]
   },
   resources : {
      rows : [
         { id : 1, name : 'Hillinghead' },
         { id : 2, name : 'Hasan' }
      ]
   },
   assignments : {
      rows : [
         { id : 1, resourceId : 1, eventId : 1 },
         { id : 2, resourceId : 1, eventId : 2 },
         { id : 3, resourceId : 2, eventId : 2 }
      ]
   }
});

ResourceStore and ResourceModel

As mentioned earlier, a Calendar uses a ResourceStore to hold instances of ResourceModel. In a horizontal schedule this represents the rows. The model describes what data each record contains (fields). By default ResourceModel defines only three fields:

  • name
  • eventColor
  • eventStyle

The name field is what it sounds like, a text field for a resource name. For more information on eventColor and eventStyle, read the guide on Styling.

EventStore and EventModel

A Calendar also requires an EventStore to hold instances of EventModel. Records in this store represents the bars displayed in the schedule. There are multiple predefined fields, the most important ones being ( see EventModel in API docs for a complete list):

Fields Description
resourceId Which resource this event is assigned to. Only valid with single assignment
name Event name, displayed in the event bars by default
startDate Start date, either as a date or a parseable date string
endDate An event should either have an endDate or a duration. The missing one will be calculated
duration Duration, added to startDate to determine endDate. Remember to also specify durationUnit
durationUnit The unit in which the duration is given. Needed to make the calculation correct

Defining additional fields

In many applications you will want to extend the built-in models with additional fields. There is a few different ways of achieving this, and while this section uses ResourceModel for the examples they apply to all models.

Autogenerated fields

The properties of the first record in your data will be turned into fields on the model:

const resourceStore = new ResourceStore({
    data : [
        { name : 'Wolverine', powers : 'Regeneration' },
        { name : 'Deadpool', powers : 'Yes I have, great powers' }   
    ]
});

The code above will create a ResourceStore with two records, based on a generated ResourceModel containing the added powers field (name is already there by default).

Custom Model

If you need more control over the fields a model contains, you have two options. If you do not need to reuse the Model you can simply specify the additional fields when creating the store:

const resourceStore = new ResourceStore({
    fields : ['powers', 'affiliation'],
    data : [
        { name : 'Wolverine', powers : 'Regeneration' },
        /*...*/
    ]
});

You can also create a subclass of a Model and define the fields you need on it:

class SuperHero extends ResourceModel {
    static fields = [
        // New custom fields:
        'powers', 
        'affiliation' 
    ];
}

const resourceStore = new ResourceStore({
    modelClass : SuperHero,
    data : [/*...*/]
}); 

See the API docs for Model for more information on defining and mapping fields.

Models are reactive!

Fields are turned into setters on the records, which makes them reactive. For example:

const calendar = new Calendar({
    events : [
        { id : 3, resourceId : 2, name : 'Drink beer', startDate : new Date(2018,4,1,9,00), duration : 8, durationUnit : 'hour' },
    ]
});

calendar.eventStore.first.duration = 10; 

The above will update the calendar on the fly, giving Wolverine more time to drink beer.

Contents