The JavaScript ecosystem is regularly enriched with promising frameworks, but many web developers still prefer to use some time-proven options such as Angular. That’s why our plan for today is to give you a good starting point for using DHTMLX Scheduler with the Angular framework.
Now we’re going to implement an angular Scheduler component for DHTMLX Scheduler (JavaScript Scheduler), add it into the small application and bind it to the RESTful API on a backend. We’ll write this demo in TypeScript since it’s a recommended way, although it also can be done in plain JavaScript or in ES6.
Please mind that in this tutorial we won’t cover server-side logic of data saving. Instead, we’ll emulate the backend using the Angular in-memory-web-api tool.
We also prepared a new video tutorial that visualizes the whole process of building a basic Angular Scheduler app using DHTMLX Scheduler.
You can download a complete Angular Scheduler demo from GitHub.
Preparing App
To set up a development environment, we’ll use angular cli. If you don’t have angular cli you can get it with a package manager (npm install -g @angular/cli) or check its documentation for more details. To create an app skeleton, run the following command:
You’ll be asked to pick a preset (add Angular routing or not, stylesheet type). Select the necessary options to configure the system to your needs.
After the operation finishes, we can go to the app directory and run the application:
ng serve
Now, if we open http://127.0.0.1:4200 we should see the initial page. The ng serve command will watch the source file and, if necessary, will change and rebuild the app.
Creating Scheduler Component
To be able to embed Scheduler into the app, you should get DHTMLX Scheduler code. Run the following command:
To add dhtmlxScheduler to the Angular app we should create a new component. For this, run the following command:
The newly created “scheduler.component.thml” file inside the “scheduler” folder, will contain the template for the scheduler. Let’s add the next lines of code:
<div class="dhx_cal_navline">
<div class="dhx_cal_prev_button"></div>
<div class="dhx_cal_next_button"></div>
<div class="dhx_cal_today_button"></div>
<div class="dhx_cal_date"></div>
<div class="dhx_cal_tab" data-tab="day"></div>
<div class="dhx_cal_tab" data-tab="week"></div>
<div class="dhx_cal_tab" data-tab="month"></div>
</div>
<div class="dhx_cal_header"></div>
<div class="dhx_cal_data"></div>
</div>
We’ll declare scheduler styles in the separate file named “scheduler.component.css”. Default styles can look like the following:
:host {
display: block;
position: relative;
height: 100%;
width: 100%;
}
To make the scheduler container occupy the entire space of the body, you need to add the following styles to the “styles.css” file located in the “src” folder:
html {
width: 100%;
height: 100%;
margin: unset;
}
To proceed, we need to import the required modules and add the necessary code lines to the “scheduler.component.ts” file:
import { scheduler } from "dhtmlx-scheduler";
@Component({
encapsulation: ViewEncapsulation.None,
selector: "scheduler",
styleUrls: ['./scheduler.component.css'],
templateUrl: './scheduler.component.html'
})
export class SchedulerComponent implements OnInit {
@ViewChild("scheduler_here", {static: true}) schedulerContainer!: ElementRef;
ngOnInit() {
scheduler.init(this.schedulerContainer.nativeElement, new Date(2023, 4, 15));
}
}
The “ViewEncapsulation” is needed to correctly apply the styles for the scheduler. By default, Angular encapsulates styles of components by adding unique attributes to their CSS and HTML. As the Scheduler HTML template is created dynamically, using the default technique is impossible. The alternatives are to enable the native Shadow DOM view encapsulation or to disable it at all. Since the Shadow DOM isn’t supported everywhere, we’ll choose to disable encapsulation.
We utilize the OnInit interface to initialize DHTMLX Scheduler when Angular places the component on the page.
Now that the Scheduler module is created and recognized by Angular, you can add the new component to the page. Open “app.component.html” (located in src/app) and insert the scheduler tag in there:
Now if you open the page again, you will see an empty Scheduler on it.
Providing Data
At this stage, an empty Scheduler is good to look at, but I bet you want more than that. Scheduler needs data. To add data loading to the Angular Scheduler, you need to add an event service. But before that, let’s define the event model.
For creating the event model, run the following command:
In the newly created “event.ts” file inside the “models” folder, we will add the next lines of code:
id!: number;
start_date!: string;
end_date!: string;
text!: string;
}
Now let’s create an event service. A service is a class that will be responsible for creating a specific event. Services in Angular can be injected by using the Dependency Injection mechanism. They can include data, functions or some features necessary for the application. You need to create a data service that will be used to provide the scheduler with events.
For creating event service, run the following command:
In the newly created “event.service.ts” file inside the “services” folder, it is required to add the next lines of code:
import { Event } from "../models/event";
@Injectable()
export class EventService {
get(): Promise<Event[]>{
return Promise.resolve([
{ id: 1, start_date: "2023-05-16 09:00", end_date: "2023-05-16 13:00", text: "Event 1" },
{ id: 2, start_date: "2023-05-18 10:00", end_date: "2023-05-18 14:00", text: "Event 2" },
]);
}
}
We’ve added the @Injectable() decorator to our service. It marks a class as available to an injector for instantiation. We’ll inject it into our component further.
Currently, the get method returns a resolved promise with hard coded data. However, you can load data from the server side and also return a promise. The issue will be discussed a little further below.
The scheduler component is supposed to use EventService to get events. To enable this, let’s add EventService to the component. First, import the necessary module for the service in scheduler.component.ts:
You should also specify EventService as a provider in the @Component decorator:
Now every time a new SchedulerComponent is initialized, a fresh instance of the service will be created.
The service should be prepared to be injected into the component. For this purpose, add the following constructor to the SchedulerComponent class:
Modify the ngOnInit function:
▸ it should set the data format for loading events (XML in this case),
▸ call the services to get the function and then wait for a response to put the data to the scheduler.
scheduler.init(this.schedulerContainer.nativeElement, new Date(2023, 4, 15));
this.eventService.get()
.then((data) => {
scheduler.parse(data);
});
scheduler.parse accepts a data object in JSON format.
You can check the complete code for the scheduler.components.ts file.
Now, if you reopen the app page, you should see a scheduler with events.
Saving Data
Creating an actual database and writing a backend is beyond the issues in this tutorial. Instead, our Scheduler component will send all user-made changes to a service, and that service will emulate the communication with the backend. We’ll do it with the help of the angular-in-memory-web-api library. If you’re building an app intended for real use, you can skip the step where we configure angular-in-memory-web-api and proceed to “Step 3. Services for CRUD operations”.
1. Install angular-in-memory-web-api
First, you should install angular-in-memory-web-api. Run the following command:
To define the mock database initialization, open app.module.ts and add InMemoryWebApiModule. The necessary class will be defined on the next step. For now, make the following changes to app.module.ts:
▸ add imports:
import {InMemoryWebApiModule} from 'angular-in-memory-web-api';
import {InMemoryDataService} from './services/in-memory-data.service';
▸ add InMemoryWebApiModule to NgModule declarations:
//unchanged code
imports: [
//other
HttpClientModule,
InMemoryWebApiModule.forRoot(InMemoryDataService)
],
//unchanged code
})
export class AppModule {}
For creating the in-memory-data service, run the following command:
In the newly created “in-memory-data.service.ts” file inside the “services” folder, we will add the next lines of code:
export class InMemoryDataService implements InMemoryDbService {
createDb() {
let events = [
{ id: 1, start_date: "2023-05-16 09:00", end_date: "2023-05-16 13:00", text: "Event 1" },
{ id: 2, start_date: "2023-05-18 10:00", end_date: "2023-05-18 14:00", text: "Event 2" },
];
return {events};
}
}
2. Helper for Requests
Let’s create a helper for errors that will notify users by sending error messages to console when something goes wrong. Do so, create service-helper.ts with the following code:
console.log(error);
return Promise.reject(error);
}
3. Services for CRUD Operations
The services should handle adding, updating and deleting items in the scheduler. Open event.service.ts and import the helper and the module for HTTP requests and promises:
import {Event} from "../models/event";
import {HttpClient} from "@angular/common/http";
import {HandleError} from "./service-helper";
import { firstValueFrom } from 'rxjs';
Next, add a constructor to the EventService class:
export class EventService {
private eventUrl = "api/events";
constructor(private http: HttpClient) {}
//get and other methods here
}
Update the get method so that it got the data from the server:
return firstValueFrom(this.http.get(this.eventUrl))
.catch(HandleError);
}
eventUrl is a private element of the service. It contains the URL to the REST API. In order to send HTTP requests, an HTTP class has been injected into the service.
Finally, add the insert, update and remove methods for operations with events in the scheduler. To insert a new item, you need to send a POST request to the URL with the new event in its body:
return firstValueFrom(this.http.post(this.eventUrl, event))
.catch(HandleError);
}
To update an item, you are to send a PUT request to url/item_id. This request also contains the updated event in its body.
return firstValueFrom(this.http.put(`${this.eventUrl}/${event.id}`, event))
.catch(HandleError);
}
To remove an item, you need to send a delete request to url/item_id.
return firstValueFrom(this.http.delete(`${this.eventUrl}/${id}`))
.catch(HandleError);
}
Feel free to check out the complete code of event.service.ts on GitHub.
In the “scheduler.component.ts” file, we need to add events editing logic. Since we import the scheduler object from the module to avoid unexpected behavior, we should create a data processor and attach handlers via attachEvent only once. To ensure that the data processor is created only once each time the scheduler is initialized (when switching between application pages), you can add a custom property to the scheduler object, for example (scheduler as any).$_initOnce = true.
Next, we check if the scheduler has this property. If it doesn’t, we initialize the scheduler for the first time and create a data processor:
(scheduler as any).$_initOnce = true;
const dp = scheduler.createDataProcessor(...
}
If the property exists, then the scheduler is already configured, and we need to skip creating the data processor.
If using the Commercial/Enterprise/Ultimate licenses of DHTMLX Scheduler, we can create a new scheduler object (const scheduler = Scheduler.getSchedulerInstance()) and configure it when starting the component (ngOnInit), and destroy the created scheduler (scheduler.destructor()) when exiting the component (ngOnDestroy). This way, each launch of the component will start with a clean scheduler, and the check will not be needed.
import { scheduler } from "dhtmlx-scheduler";
import { EventService } from "../services/event.service";
@Component({
encapsulation: ViewEncapsulation.None,
selector: "scheduler",
providers: [ EventService ],
styleUrls: ['./scheduler.component.css'],
templateUrl: './scheduler.component.html'
})
export class SchedulerComponent implements OnInit {
@ViewChild("scheduler_here", {static: true}) schedulerContainer!: ElementRef;
constructor(private eventService: EventService) { }
ngOnInit() {
scheduler.config.date_format = "%Y-%m-%d %H:%i";
scheduler.init(this.schedulerContainer.nativeElement, new Date(2023, 4, 15));
if(!(scheduler as any).$_initOnce){
(scheduler as any).$_initOnce = true;
const dp = scheduler.createDataProcessor({
event: {
create: (data: any) => this.eventService.insert(data),
update: (data: any) => this.eventService.update(data),
delete: (id: number) => this.eventService.remove(id),
}
});
}
this.eventService.get()
.then((data) => {
scheduler.parse(data);
});
}
}
Here we’ve defined a dataProcessor handler that will capture changes made in scheduler by the user and will transfer them to data services.
The handler can be declared either as a function or a router object, we’ve used the latter approach here.
Scheduler accepts Promise response from the handler, so the scheduler will correctly process the completion of an action.
If your service changes event id after creating a new record (which it usually does), make sure that your Promise returns an object with {id: databaseId} or {tid: databaseId} as a result, so Scheduler could apply new database id to the record.
Well, Angular Scheduler is ready, you are welcome to check out the source code. The app can use RESTful API for CRUD operations. Keep in mind that in this demo angular-in-memory-api library was used to emulate the data storage. However, in real life apps, you’ll probably want to save changes to a real database. There are two solutions. First, you can remove in-memory-web-api from the app (in case you’ve skipped that step, you don’t need to do this). Second, you can configure it to pass requests through to the real backend and thus implement data storage. Feel free to send us feedback in the comments and stay tuned for new tutorials.