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.