I do have one more question regarding event cleanup in Bryntum TaskBoard within Angular. I'm trying to properly destroy event listeners in ngOnDestroy(), but using both .un() and .off() doesn’t seem to remove the events as expected.
Is there a recommended way to clean up TaskBoard events in Angular during component destruction?
async ngAfterViewInit() {
this.taskBoard = this.taskBoardComponent.instance;
this.project = this.projectComponent.instance;
// Initialize TaskBoard with empty state
if (this.taskBoard) {
this.taskBoard.on('taskDragStart', () => {
this.taskApiService.pausePolling();
});
// Resume polling when drag ends
this.taskBoard.on('taskDragEnd', () => {
this.taskApiService.resumePolling();
});
// Also handle drag cancel
this.taskBoard.on('taskDragAbort', () => {
this.taskApiService.resumePolling();
});
}
this.taskBoard.on("taskDrop", this.onTaskDrop.bind(this));
}
ngOnDestroy() {
if(this.taskBoard){
// this.taskBoard.un('taskDragStart');
// this.taskBoard.un('taskDragAbort');
// this.taskBoard.un('taskDragEnd');
}
// Clean up subscriptions and polling
this.subscription.unsubscribe();
this.taskApiService.stopPolling();
}
To properly clean up event listeners in Angular, you can use the detacher function returned by the on() method. When you add a listener, it returns a function that you can call to remove the listener. Here's how you can implement it:
Even though I’ve already added the mentioned methods, I still occasionally encounter the following issue:
The entire screen freezes, and it requires a manual refresh to return to normal.
This doesn’t happen consistently—it occurs only sometimes.
I’ve attached the console errors and the relevant code snippet for reference.
I tried to reproduce the issue by quickly performing drag and drop operations in the UI. The freeze tends to happen when drag and drop is done rapidly across columns. It doesn't occur every time but happens intermittently under quick interactions.
onTaskDrop({ taskRecords, targetColumn, source }: any): void {
// Add null checks for required parameters
if (!taskRecords || !targetColumn || taskRecords.length === 0) {
console.error('Invalid task drop: missing required parameters');
return;
}
// Ensure taskRecords is an array
if (!Array.isArray(taskRecords)) {
console.error('Invalid task drop: taskRecords is not an array');
return;
}
try {
if (this.taskBoard) {
this.taskBoard.mask('loading.........')
}
// Get all tasks in the target column, excluding the ones being moved (to avoid duplicates)
const tasksInColumn = this.projectData.tasks
.filter((t: any) => {
// Add null checks for each property access
if (!t || !t.technicianId) return false;
if (!Array.isArray(taskRecords)) return false;
return t.technicianId === targetColumn.id &&
!taskRecords.some((tr: any) => tr?.dispatchId === t.dispatchId);
})
.filter((t: any) => t?.workAssignmentStatus !== 'C')
.sort((a: any, b: any) => (a?.schedulerSequenceNum ?? 0) - (b?.schedulerSequenceNum ?? 0));
// Update technician assignment in task list
taskRecords.forEach(async (taskRecord: any) => {
try {
const insertIndex = targetColumn.tasks.findIndex((task : any) => task.dispatchId === taskRecord.dispatchId);
const newSequenceNum = this.calculateNewSequenceNum(tasksInColumn, insertIndex, targetColumn.id);
const taskIndex = this.projectData.tasks.findIndex(
(t: any) => t.dispatchId === taskRecord.dispatchId
);
if (taskIndex !== -1) {
// Update the task in the UI first
this.projectData.tasks[taskIndex].technicianId = targetColumn.id;
this.projectData.tasks[taskIndex].schedulerSequenceNum = newSequenceNum;
//taskRecord.schedulerSequenceNum = newSequenceNum;
// Make the API call to update the backend
this.updateWorkAssignment(taskRecord, targetColumn);
this.taskBoard.unmask();
}
} catch (error) {
console.error('Error processing task record:', error);
// Ensure the drag operation completes even if there's an error
if (this.taskBoard) {
this.taskBoard.resumeEvents();
this.taskBoard.unmask();
}
}
});
if (this.taskBoard?.project?.taskStore) {
const taskStore = this.taskBoard.project.taskStore as TaskStore;
taskStore.sort('schedulerSequenceNum', true);
}
this.calculateTechnicianHours();
// Update all column texts without rebuilding the structure
if (this.taskBoard && this.taskBoard.columns) {
this.taskBoard.columns.forEach((column: any) => {
const resource = this.projectData.resources.find(
(r: any) => r.technicianId === column.id
);
if (resource) {
const totalMinutes = this.technicianHours[column.id] || 0;
const formatted = this.formatHours(totalMinutes);
column.text = `${resource.firstName} ${resource.lastName} (${formatted})`;
}
});
}
} catch (error) {
console.error('Error processing task drop:', error);
// Ensure the drag operation completes even if there's an error
if (this.taskBoard) {
this.taskBoard.resumeEvents();
}
}
}
private updateWorkAssignment(task: any, targetColumn: any): void {
const currentUserId = localStorage.getItem('UserID') || '';
const currentTask = this.projectData.tasks.find((t: any) => t.dispatchId === task.data.dispatchId);
if (!currentTask) return;
let workAssignmentStatus = currentTask.workAssignmentStatus;
let technicianId = targetColumn.id;
// Scenario 1: Assigning to a Tech (from Unassigned)
if (targetColumn.id !== 'UnAssigned') {
workAssignmentStatus = 'A';
}
// Scenario 2: Un-assigning from a Tech
else if (targetColumn.id === 'UnAssigned') {
workAssignmentStatus = 'U';
technicianId = null;
}
// Scenario 3: Re-assigning to another Tech
// Keep the same workAssignmentStatus
const payload: WorkAssignmentUpdatePayload = {
dispatchId: task.data.dispatchId,
workOrderId: currentTask.workOrderNumber,
modifiedByUserId: currentUserId,
...
};
try {
// Update UI optimistically before API call
if (this.taskBoard?.project?.taskStore) {
this.taskBoard.suspendEvents();
const taskStore = this.taskBoard.project.taskStore as TaskStore;
const taskToUpdate = taskStore.getById(task.data.dispatchId);
if (taskToUpdate) {
taskToUpdate.set({
technicianId: targetColumn.id,
workAssignmentStatus: workAssignmentStatus
});
}
this.taskBoard.resumeEvents();
}
// Update local data optimistically
const taskIndex = this.projectData.tasks.findIndex((t: any) => t.dispatchId === task.data.dispatchId);
if (taskIndex !== -1) {
this.projectData.tasks[taskIndex] = {
...this.projectData.tasks[taskIndex],
technicianId: targetColumn.id,
workAssignmentStatus: workAssignmentStatus
};
}
// Recalculate hours and update columns optimistically
this.calculateTechnicianHours();
this.updateTaskBoardColumns();
// Make the API call
this.taskApiService.updateWorkAssignment(payload).subscribe({
next: () => {
console.log("API called")
},
error: (error) => {
console.error('Error updating work assignment:', error);
// Revert changes on error
this.processTasks({ result: { workAssignments: [currentTask] } });
}
});
} catch (error) {
console.error('Error updating work assignment:', error);
}
}
Console Error:
Cannot read properties of undefined (reading 'forEach')
at TaskZone.dragMove (taskboard.module.js:96367:28)
at DragContext.updateTarget (taskboard.module.js:62309:14)
at DragContext.set (taskboard.module.js74)
at DragContext.updateTargetElement (taskboard.module.js:62323:26)
at DragContext.set (taskboard.module.js74)
at DragContext.track (taskboard.module.js:62509:21)
at DragContext.move (taskboard.module.js:62458:12)
at TaskZone.onDragPointerMove (taskboard.module.js:62930:38)
at HTMLBodyElement.handler (taskboard.module.js:8769:72)
at _ZoneDelegate.invokeTask (zone.js:409:31)
handleError @ core.mjs:6619Understand this error
3core.mjs:6619 ERROR TypeError: Cannot read properties of undefined (reading 'forEach')
at TaskZone.dragMove (taskboard.module.js:96367:28)
at DragContext.track (taskboard.module.js:62511:40)
at DragContext.move (taskboard.module.js:62458:12)
at TaskZone.onDragPointerMove (taskboard.module.js:62930:38)
at HTMLBodyElement.handler (taskboard.module.js:8769:72)
at _ZoneDelegate.invokeTask (zone.js:409:31)
at core.mjs55
at AsyncStackTaggingZoneSpec.onInvokeTask (core.mjs36)
at _ZoneDelegate.invokeTask (zone.js:408:60)
at Object.onInvokeTask (core.mjs33)
handleError @ core.mjs:6619Understand this error
3core.mjs:6619 ERROR Error: Uncaught (in promise): TypeError: Cannot read properties of undefined (reading 'some')
TypeError: Cannot read properties of undefined (reading 'some')
at hasChanged (taskboard.module.js:96055:53)
at taskboard.module.js:96411:131
at Generator.next (<anonymous>)
at asyncGeneratorStep (asyncToGenerator.js:3:1)
at _next (asyncToGenerator.js:22:1)
at asyncToGenerator.js:27:1
at new ZoneAwarePromise (zone.js21)
at asyncToGenerator.js:19:1
at TaskZone.dragDrop (taskboard.module.js:96517:6)
at taskboard.module.js:62705:18
at hasChanged (taskboard.module.js:96055:53)
at taskboard.module.js:96411:131
at Generator.next (<anonymous>)
at asyncGeneratorStep (asyncToGenerator.js:3:1)
at _next (asyncToGenerator.js:22:1)
at asyncToGenerator.js:27:1
at new ZoneAwarePromise (zone.js21)
at asyncToGenerator.js:19:1
at TaskZone.dragDrop (taskboard.module.js:96517:6)
at taskboard.module.js:62705:18
at resolvePromise (zone.js31)
at zone.js17
at zone.js33
at asyncGeneratorStep (asyncToGenerator.js:6:1)
at _throw (asyncToGenerator.js:25:1)
at _ZoneDelegate.invoke (zone.js:375:26)
at Object.onInvoke (core.mjs33)
at _ZoneDelegate.invoke (zone.js:374:52)
at Zone.run (zone.js:134:43)
at zone.js36
The issue you're experiencing might be related to asynchronous operations or race conditions during rapid drag-and-drop actions. Here are a few suggestions to help address the problem:
Ensure Proper Error Handling: Make sure all asynchronous operations, such as API calls, are properly handled with try-catch blocks and promise handling to prevent unhandled exceptions.
Optimize Event Handling: Consider debouncing or throttling the drag-and-drop operations to prevent rapid consecutive actions that might lead to race conditions.
Check for Undefined Values: The errors indicate that some properties are undefined. Ensure that all objects and arrays are properly initialized and checked before accessing their properties.
Use suspendEvents and resumeEvents Carefully: Ensure that events are resumed correctly after suspension, especially if multiple operations are involved.
Console Logs and Debugging: Add console logs to trace the flow of operations and identify where the undefined values might be occurring.
If the issue persists, please provide a minimal reproducible example to help diagnose the problem further.