---
name: syncfusion-scheduler-popup-close-before-action-begin
description: |
  Fix silent data loss when saving custom editor fields in Syncfusion EJ2 Scheduler.
  Use when: (1) custom checkboxes, dropdowns, or fields added to the Scheduler editor
  popup lose their values on save, (2) `actionBegin` handler receives empty/null custom
  data despite user having filled it in, (3) cleanup in `popupClose` wipes state before
  `actionBegin` can read it. Root cause: Syncfusion fires `popupClose` BEFORE
  `actionBegin` when user clicks Save — the call stack is
  `eventSave → hide() → onBeforeClose → processCrudActions → actionBegin`.
author: Claude Code
version: 1.0.0
date: 2026-02-27
---

# Syncfusion EJ2 Scheduler: popupClose Fires BEFORE actionBegin

## Problem

When adding custom fields (checkboxes, dropdowns, task selectors, etc.) to the
Syncfusion EJ2 Scheduler editor popup, the custom data appears to vanish on save.
The `actionBegin` handler sees empty/null values even though the user clearly
filled them in. Any cleanup logic in `popupClose` destroys the data before
`actionBegin` has a chance to read it.

## Context / Trigger Conditions

- You've added custom UI elements to the Syncfusion Scheduler editor popup
- You store custom state (Sets, arrays, form values) that need to survive until save
- You have a `popupClose` handler that calls `cleanup()` or resets state
- Symptom: `actionBegin` handler receives empty/default values for custom fields
- Symptom: Network requests that should fire after save never execute (early return
  due to empty data)
- Symptom: No errors in console — the code silently skips the operation

## Root Cause

Syncfusion's internal event flow when the user clicks "Save" in the editor dialog:

```
eventSave()
  → hide()                          // Popup starts closing
    → onBeforeClose()               // Triggers popupClose event HERE
      → processCrudActions()
        → resourceSaveEvent()
          → saveEvent()
            → actionBegin event     // YOUR save handler runs HERE (too late!)
```

This means `popupClose` fires **synchronously before** `actionBegin`, not
concurrently or after. Any state cleaned up in `popupClose` is gone by the time
`actionBegin` runs.

## Solution

**Pattern: Snapshot in popupClose, read in actionBegin**

1. In your `popupClose` handler, capture custom state BEFORE cleanup:

```javascript
onPopupClose(args) {
    if (args.type === 'Editor' && this.customTab) {
        // Snapshot BEFORE cleanup — actionBegin fires AFTER popupClose
        this._pendingCustomData = this.customTab.getSelectedValues();
        this.customTab.cleanup();  // Now safe to clean up
    }
}
```

2. In your `actionBegin` handler, read the pre-captured snapshot:

```javascript
async onActionBegin(args) {
    // Read the pre-captured snapshot (popupClose already fired)
    const customData = this._pendingCustomData || null;
    this._pendingCustomData = null;  // Clear after reading

    if (args.requestType === 'eventChange') {
        args.cancel = true;
        await this.updateEvent(args.data);
        await this.saveCustomData(args.data, customData);
    }
}
```

**Key principles:**
- Snapshot returns a NEW object/array (spread operator, `[...set]`, etc.)
  so cleanup can't mutate it
- Clear the snapshot after reading to prevent stale data on next save
- The snapshot is a plain class property, not a closure variable

## What Does NOT Work

```javascript
// WRONG: Trying to snapshot at the start of actionBegin
async onActionBegin(args) {
    // By this point, popupClose has ALREADY fired and cleanup() ran
    const data = this.customTab.getSelectedValues(); // EMPTY!
    // ...
}
```

```javascript
// WRONG: Assuming popupClose fires "concurrently" with actionBegin
// The events are synchronous in the same call stack, not async
```

## Verification

1. Add `console.log` in both `popupClose` and `actionBegin` to confirm order
2. Check that the snapshot object contains expected values (not empty arrays/Sets)
3. Verify the network request fires with the correct payload
4. Look for the stack trace in browser DevTools — it shows the full synchronous
   chain from `eventSave` through `hide()` to `actionBegin`

## Example

Real-world case: Task assignment checkboxes in a shift editor popup.

**Before fix**: User checks 7 task checkboxes, clicks Save. `actionBegin`
snapshots `getSelectedTasks()` which returns `{taskIds: [], groupIds: []}`.
The POST to `/assign-tasks` never fires because `taskIds.length === 0`.

**After fix**: `popupClose` snapshots `{taskIds: [2,12,7,14,8,6,3], groupIds: [2]}`
before cleanup. `actionBegin` reads this snapshot. POST fires with all 7 task IDs.
Server responds `{"success":true,"assigned":7}`.

## Notes

- This same pattern applies to Syncfusion Scheduler's `popupClose` event across
  all editor types (Event, RecurrenceEditor, etc.)
- The behavior is consistent across Syncfusion EJ2 versions — it's architectural,
  not a bug
- Similar timing issues exist in other Syncfusion components (Kanban `dragStop`
  mutates data before your handler runs)
- Always verify event ordering with stack traces, never assume from documentation
