import { BaseEvent } from '@wlindabla/event_dispatcher'; import { BatchConfirmData, RouteMatch, SpaRequest, CRUDPageType, SpaResponse, SwapContext } from './types.cjs'; import '@wlindabla/form_validator/utils'; /** * @wlindabla/sonata_spa — Events * All SPA event constants and event classes * Inspired by Symfony's KernelEvents and StoppableEventInterface * @author AGBOKOUDJO Franck */ /** * All event name constants for @wlindabla/sonata_spa. * Single source of truth — use these constants everywhere instead of raw strings. * * Naming convention: * spa:* → lifecycle events (navigation, fetch, swap, dom) * crud:* → CRUD page type events dispatched after RouteResolver * These constants define the lifecycle of the Single Page Application. * They are used by the EventDispatcher to orchestrate communication * between the SpaKernel and its Subscribers. * Stoppable events (stopPropagation() cancels the pipeline): * - SpaEvents.REQUEST → cancel navigation (unsaved changes guard) * - SpaEvents.ROUTE_RESOLVED → developer takes full control of navigation * - SpaEvents.SWAP_BEFORE → developer performs custom DOM swap * * Mutable events (payload can be modified before next step): * - SpaEvents.RESPONSE → modify HTML before DOM swap */ declare abstract class SpaEvents { /** Dispatched when a navigation is requested — STOPPABLE */ static readonly REQUEST: "spa:request"; /** Dispatched after RouteResolver resolves the URL — STOPPABLE */ static readonly ROUTE_RESOLVED: "spa:route:resolved"; /** Dispatched when the server response is received — MUTABLE */ static readonly RESPONSE: "spa:response"; /** Dispatched when navigation completes successfully */ static readonly NAVIGATE_COMPLETED: "spa:navigate:completed"; /** Dispatched when a server-managed URL is detected — full page reload */ static readonly SERVER_REDIRECT: "spa:server:redirect"; /** Dispatched just before the HTTP request is sent */ static readonly FETCH_PREPARE: "spa:fetch:prepare"; /** Dispatched when the HTTP request starts */ static readonly FETCH_STARTED: "spa:fetch:started"; /** Dispatched when the server returns a 2xx response */ static readonly FETCH_SUCCEEDED: "spa:fetch:succeeded"; /** Dispatched when the server returns a 4xx/5xx response */ static readonly FETCH_FAILED: "spa:fetch:failed"; /** Dispatched on network error, timeout or abort */ static readonly FETCH_ERRORED: "spa:fetch:errored"; /** Dispatched when the HTTP request finishes (always — success or error) */ static readonly FETCH_FINISHED: "spa:fetch:finished"; /** Dispatched before the DOM swap — STOPPABLE (developer can do custom swap) */ static readonly SWAP_BEFORE: "spa:swap:before"; /** Dispatched after the DOM swap completes */ static readonly SWAP_AFTER: "spa:swap:after"; /** Dispatched after DomManager.reinitialize() — all scripts/BS5/Stimulus ready */ static readonly DOM_READY: "spa:dom:ready"; /** Dispatched when the resolved page type is 'list' */ static readonly CRUD_LIST: "crud:list"; /** Dispatched when the resolved page type is 'show' */ static readonly CRUD_SHOW: "crud:show"; /** Dispatched when the resolved page type is 'create' (client-side only) */ static readonly CRUD_CREATE: "crud:create"; /** Dispatched when the resolved page type is 'edit' (client-side only) */ static readonly CRUD_EDIT: "crud:edit"; /** Dispatched when the resolved page type is 'delete' */ static readonly CRUD_DELETE: "crud:delete"; /** Dispatched when the resolved page type is 'dashboard' */ static readonly DASHBOARD: "spa:dashboard"; /** Dispatched by FormBindingManager when a Sonata form is submitted */ static readonly FORM_SUBMIT: "spa:form:submit"; /** Dispatched when client-side validation fails — form is blocked */ static readonly FORM_INVALID: "spa:form:invalid"; /** Dispatched when form submission succeeds (2xx from server) */ static readonly FORM_SUCCEEDED: "spa:form:succeeded"; /** Dispatched when form submission fails (4xx/5xx from server) */ static readonly FORM_FAILED: "spa:form:failed"; /** Dispatched when delete confirmation modal should be shown */ static readonly DELETE_CONFIRM_REQUESTED: "spa:delete:confirm:requested"; /** Dispatched when the user cancels the delete confirmation */ static readonly DELETE_CONFIRM_CANCELLED: "spa:delete:confirm:cancelled"; /** Dispatched when the user confirms the delete action */ static readonly DELETE_CONFIRM_ACCEPTED: "spa:delete:confirm:accepted"; /** Dispatched when a delete request is in progress */ static readonly DELETE_PROCESSING: "spa:delete:processing"; /** Dispatched when a delete request succeeds (2xx from server) */ static readonly DELETE_SUCCEEDED: "spa:delete:succeeded"; /** Dispatched when a delete request fails (4xx–599 from server) */ static readonly DELETE_FAILED: "spa:delete:failed"; /** Dispatched when a batch confirmation modal should be shown */ static readonly BATCH_CONFIRM_REQUESTED: "spa:batch:confirm:requested"; /** Dispatched when the user cancels the batch confirmation */ static readonly BATCH_CONFIRM_CANCELLED: "spa:batch:confirm:cancelled"; /** Dispatched when the user confirms the batch action */ static readonly BATCH_CONFIRM_ACCEPTED: "spa:batch:confirm:accepted"; /** Dispatched when a batch request is in progress (after user confirmation) */ static readonly BATCH_PROCESSING: "spa:batch:processing"; /** Dispatched when a batch request succeeds (2xx from server) */ static readonly BATCH_SUCCEEDED: "spa:batch:succeeded"; /** Dispatched when a batch request fails (4xx–599 from server) */ static readonly BATCH_FAILED: "spa:batch:failed"; /** Dispatched when the resolved page type is 'batch' */ static readonly CRUD_BATCH: "crud:batch"; } /** * Dispatched when a SPA navigation is requested. * STOPPABLE — call stopPropagation() to cancel navigation. * * Use cases: * - Block navigation when a form has unsaved changes * - Show a confirmation dialog before leaving the page * * @example * ```typescript * dispatcher.addListener(SpaEvents.REQUEST, (event: SpaRequestEvent) => { * if (hasUnsavedChanges()) { * event.stopPropagation(); // navigation cancelled * } * }); * ``` */ declare class SpaRequestEvent extends BaseEvent { private readonly _request; constructor(_request: SpaRequest); /** The navigation request that was requested */ get request(): SpaRequest; } /** * Dispatched after the RouteResolver resolves the URL to a RouteMatch. * STOPPABLE — call stopPropagation() to take full control of this navigation. * * Use cases: * - Handle a custom page type outside Sonata CRUD * - Override routing logic for a specific resource * * @example * ```typescript * dispatcher.addListener(SpaEvents.ROUTE_RESOLVED, (event: SpaRouteResolvedEvent) => { * if (event.routeMatch.resource === 'my-custom-resource') { * event.stopPropagation(); // handle it yourself * myCustomHandler(event.routeMatch); * } * }); * ``` */ declare class SpaRouteResolvedEvent extends BaseEvent { private readonly _request; private readonly _routeMatch; constructor(_request: SpaRequest, _routeMatch: RouteMatch); get request(): SpaRequest; get routeMatch(): RouteMatch; } /** * Dispatched when the server response is received. * MUTABLE — modify response.html before DOM swap. * NOT stoppable — the response is already here. * * Use cases: * - Inject additional content into the HTML before swap * - Modify page title * - Add breadcrumbs * * @example * ```typescript * dispatcher.addListener(SpaEvents.RESPONSE, (event: SpaResponseEvent) => { * event.response.html = event.response.html.replace( * '', ' — My App' * ); * }); * ``` */ declare class SpaResponseEvent extends BaseEvent { private readonly _request; private readonly _response; constructor(_request: SpaRequest, _response: SpaResponse); get request(): SpaRequest; /** Mutable — can be modified before the DOM swap */ get response(): SpaResponse; } /** * Dispatched before the DOM swap. * STOPPABLE — call stopPropagation() to perform a custom DOM swap. * * Use cases: * - Custom DOM swap with animation * - Partial swap for a specific page * * @example * ```typescript * dispatcher.addListener(SpaEvents.SWAP_BEFORE, (event: SpaSwapEvent) => { * if (event.routeMatch.resource === 'dashboard') { * event.stopPropagation(); // handle swap yourself * myCustomDashboardSwap(event.context); * } * }); * ``` */ declare class SpaSwapEvent extends BaseEvent { private readonly _context; constructor(_context: SwapContext); get context(): SwapContext; get routeMatch(): RouteMatch; } /** * Dispatched after the DOM swap completes. * NOT stoppable. */ declare class SpaSwapAfterEvent extends BaseEvent { private readonly _context; constructor(_context: SwapContext); get context(): SwapContext; } /** * Dispatched after DomManager.reinitialize() completes. * NOT stoppable. * All scripts, Bootstrap 5 components and Stimulus controllers are ready. * * Use cases: * - Third-party modules that need to re-initialize after a swap * - BindingManagers that need to rebind event listeners * * @example * ```typescript * dispatcher.addListener(SpaEvents.DOM_READY, (event: SpaDomReadyEvent) => { * myLibrary.init(event.container); * }); * ``` */ declare class SpaDomReadyEvent extends BaseEvent { private readonly _container; private readonly _routeMatch; constructor(_container: HTMLElement, _routeMatch: RouteMatch); /** The swapped container element — use this to scope your re-initialization */ get container(): HTMLElement; get routeMatch(): RouteMatch; } /** * Dispatched when navigation completes successfully. * NOT stoppable. */ declare class SpaNavigateCompletedEvent extends BaseEvent { private readonly _from; private readonly _to; private readonly _routeMatch; readonly newMainContainer: HTMLElement; readonly newMainContentArea: HTMLElement; readonly newMainContentHeader: HTMLElement | null; constructor(_from: string, _to: string, _routeMatch: RouteMatch, newMainContainer: HTMLElement, newMainContentArea: HTMLElement, newMainContentHeader: HTMLElement | null); /** The URL we navigated from */ get from(): string; /** The URL we navigated to */ get to(): string; get routeMatch(): RouteMatch; } /** * Dispatched when a CRUD page event is triggered (crud:list, crud:show, etc.). * This is the event that Page Subscribers listen to. * Carries the full context needed by the subscriber to handle the navigation. */ declare class SpaCrudEvent extends BaseEvent { private readonly _request; private readonly _routeMatch; private readonly _pageType; constructor(_request: SpaRequest, _routeMatch: RouteMatch, _pageType: CRUDPageType); get request(): SpaRequest; get routeMatch(): RouteMatch; get pageType(): CRUDPageType; } /** * Sonata submit button name constants. * Each button name tells Sonata what to do after a successful save. * Sonata reads the submitted button name server-side to determine the redirect. * * Edit page buttons: * btn_update_and_list → save and redirect to list * btn_update_and_edit → save and stay on edit page * btn_preview → save and show preview * * Create page buttons: * btn_create_and_list → create and redirect to list * btn_create_and_edit → create and redirect to edit page * btn_create_and_create → create and show empty create form again * btn_create → simple create (XHR mode) * btn_update → simple update (XHR mode) */ declare const SonataSubmitButton: { readonly UPDATE_AND_LIST: "btn_update_and_list"; readonly UPDATE_AND_EDIT: "btn_update_and_edit"; readonly UPDATE: "btn_update"; readonly PREVIEW: "btn_preview"; readonly CREATE_AND_LIST: "btn_create_and_list"; readonly CREATE_AND_EDIT: "btn_create_and_edit"; readonly CREATE_AND_CREATE: "btn_create_and_create"; readonly CREATE: "btn_create"; }; type SonataSubmitButtonName = typeof SonataSubmitButton[keyof typeof SonataSubmitButton]; /** * Dispatched by FormBindingManager when a Sonata form is submitted. * Listened to by FormSubscriber. * * Carries: * - The form element * - The RouteMatch resolved from the form action URL * - The submitter button element (which button was clicked) * - The submitter button name (btn_create_and_list, btn_update_and_list, etc.) * * The submitter button name is critical — Sonata uses it server-side * to determine the redirect after a successful save. * FormSubscriber includes it in the POST body so Sonata redirects correctly. * * At this point, @wlindabla/form_validator has already validated the form * client-side — the submit button was disabled if invalid. * FormSubmission from @wlindabla/form_validator handles data-iwas-confirm. */ declare class SpaFormSubmitEvent extends BaseEvent { private readonly _form; private readonly _routeMatch; private readonly _submitter; constructor(_form: HTMLFormElement, _routeMatch: RouteMatch, _submitter?: HTMLButtonElement | null); get form(): HTMLFormElement; get routeMatch(): RouteMatch; /** * The button element that triggered the submit. * Use this to identify which Sonata action was requested. */ get submitter(): HTMLButtonElement | null; /** * The name attribute of the clicked submit button. * Sonata reads this server-side to determine the post-save redirect. * * Examples: * 'btn_update_and_list' → redirect to list after edit * 'btn_create_and_list' → redirect to list after create * 'btn_create_and_edit' → redirect to edit page after create */ get submitterName(): SonataSubmitButtonName | null; /** * Whether the form action leads back to the list page after submit. * True for btn_update_and_list and btn_create_and_list. */ get redirectsToList(): boolean; /** * Whether the form action stays on the edit page after submit. * True for btn_update_and_edit and btn_create_and_edit. */ get redirectsToEdit(): boolean; } /** * Dispatched when the fetch request encounters a network error. * NOT stoppable. * The SpaKernel falls back to window.location.href on network errors. */ declare class SpaFetchErrorEvent extends BaseEvent { private readonly _request; private readonly _error; constructor(_request: SpaRequest, _error: Error); get request(): SpaRequest; get error(): Error; } /** * Dispatched when the delete confirmation modal should be shown. * Listened to by the UI confirmation handler (SweetAlert2 or custom modal). * * The handler must call either: * - event.accept() → proceeds with the delete POST request * - event.cancel() → cancels the delete action */ declare class SpaDeleteConfirmRequestedEvent extends BaseEvent { private readonly _title; private readonly _message; private readonly _btnDeleteText; private readonly _routeMatch; private _accepted; private _confirmCallback; private _cancelCallback; constructor(_title: string | null, _message: string | null, _btnDeleteText: string | null, _routeMatch: RouteMatch); get title(): string | null; get message(): string | null; get btnDeleteText(): string | null; get routeMatch(): RouteMatch; get isAccepted(): boolean; /** * Register the callback to execute when the user confirms the delete. * Called internally by DeletePageSubscriber. */ onAccept(callback: () => Promise): void; /** * Register the callback to execute when the user cancels. * Called internally by DeletePageSubscriber. */ onCancel(callback: () => void): void; /** * Call this from your confirmation UI to proceed with the delete. */ accept(): Promise; /** * Call this from your confirmation UI to cancel the delete. */ cancel(): void; } /** * Dispatched when a server-managed URL is detected by RequestMatcher. * Triggers a full page reload via window.location.href. * NOT stoppable. */ declare class SpaServerRedirectEvent extends BaseEvent { private readonly _url; private readonly _reason; constructor(_url: string, _reason: 'server-managed' | 'error-fallback'); get url(): string; /** Reason for the server redirect */ get reason(): 'server-managed' | 'error-fallback'; } /** * Dispatched when a delete request is in progress (after user confirmation). * NOT stoppable — the HTTP request has already been sent. * * Use cases: * - Show a loading spinner or disable the confirm button * - Log the pending deletion for audit purposes * * @example * ```typescript * dispatcher.addListener(SpaEvents.DELETE_PROCESSING, (event: SpaDeleteProcessingEvent) => { * showSpinner(); * }); * ``` */ declare class SpaDeleteProcessingEvent extends BaseEvent { private readonly _routeMatch; readonly title: string; readonly message: string; constructor(_routeMatch: RouteMatch, title: string, message: string); get routeMatch(): RouteMatch; } /** * Dispatched when the delete request completes successfully (2xx response). * NOT stoppable. * * Use cases: * - Remove the deleted row from the DOM without a full page reload * - Show a success toast/notification * - Trigger a list refresh * * @example * ```typescript * dispatcher.addListener(SpaEvents.DELETE_SUCCEEDED, (event: SpaDeleteSucceededEvent) => { * showToast(`Item deleted successfully`); * refreshList(event.routeMatch); * }); * ``` */ declare class SpaDeleteSucceededEvent extends BaseEvent { private readonly _routeMatch; private readonly _messageBody; readonly title: string; constructor(_routeMatch: RouteMatch, _messageBody: string, title: string); get routeMatch(): RouteMatch; /** The server response message received after the successful deletion */ get messageBody(): string; } /** * Dispatched when the delete request fails with an HTTP error status (4xx–599). * NOT stoppable. * * Use cases: * - Show an error message to the user (forbidden, not found, server error) * - Log the failure for monitoring * - Re-enable the confirm button so the user can retry * * @example * ```typescript * dispatcher.addListener(SpaEvents.DELETE_FAILED, (event: SpaDeleteFailedEvent) => { * showErrorToast(`Delete failed: ${event.statusCode}`); * }); * ``` */ declare class SpaDeleteFailedEvent extends BaseEvent { private readonly _routeMatch; private readonly _statusCode; private readonly _statusText; readonly title: string; constructor(_routeMatch: RouteMatch, _statusCode: number, _statusText: string, title: string); get routeMatch(): RouteMatch; /** * The HTTP status code returned by the server. * Expected range: 400–599. */ get statusCode(): number; /** * The HTTP status text returned by the server (e.g. "Not Found", "Forbidden"). */ get statusText(): string; } /** * Dispatched when the batch confirmation modal should be shown. * Listened to by the UI confirmation handler (SweetAlert2 or custom modal). * * The handler must call either: * - event.accept() → proceeds with the batch POST request * - event.cancel() → cancels the batch action * * @example * ```typescript * dispatcher.addListener(SpaEvents.BATCH_CONFIRM_REQUESTED, async (event) => { * const result = await Swal.fire({ * title: event.confirmData.title ?? 'Are you sure?', * text: event.confirmData.message ?? 'This action cannot be undone.', * icon: 'warning', * showCancelButton: true, * }); * if (result.isConfirmed) { await event.accept(); } else { event.cancel(); } * }); * ``` */ declare class SpaBatchConfirmRequestedEvent extends BaseEvent { private readonly _confirmData; private readonly _routeMatch; private _confirmCallback; private _cancelCallback; constructor(_confirmData: BatchConfirmData, _routeMatch: RouteMatch); get confirmData(): BatchConfirmData; get routeMatch(): RouteMatch; /** Register the callback to execute when the user confirms. Called internally by BatchPageSubscriber. */ onAccept(callback: () => Promise): void; /** Register the callback to execute when the user cancels. Called internally by BatchPageSubscriber. */ onCancel(callback: () => void): void; /** Call this from your confirmation UI to proceed with the batch action. */ accept(): Promise; /** Call this from your confirmation UI to cancel the batch action. */ cancel(): void; } /** * Dispatched when a batch request is in progress (after user confirmation). * NOT stoppable — the HTTP request has already been sent. * * Use cases: * - Show a loading spinner or disable the confirm button * - Log the pending batch operation for audit purposes * * @example * ```typescript * dispatcher.addListener(SpaEvents.BATCH_PROCESSING, (event: SpaBatchProcessingEvent) => { * showSpinner(); * }); * ``` */ declare class SpaBatchProcessingEvent extends BaseEvent { private readonly _routeMatch; readonly title: string; readonly message: string; constructor(_routeMatch: RouteMatch, title: string, message: string); get routeMatch(): RouteMatch; } /** * Dispatched when the batch request completes successfully (2xx response). * NOT stoppable. * * Use cases: * - Show a success toast/notification * - Trigger a list refresh * * @example * ```typescript * dispatcher.addListener(SpaEvents.BATCH_SUCCEEDED, (event: SpaBatchSucceededEvent) => { * showToast(event.message); * }); * ``` */ declare class SpaBatchSucceededEvent extends BaseEvent { private readonly _routeMatch; readonly message: string; readonly title: string; constructor(_routeMatch: RouteMatch, message: string, title: string); get routeMatch(): RouteMatch; } /** * Dispatched when the batch request fails with an HTTP error status (4xx–599). * NOT stoppable. * * Use cases: * - Show an error message to the user (forbidden, not found, server error) * - Re-enable the confirm button so the user can retry * * @example * ```typescript * dispatcher.addListener(SpaEvents.BATCH_FAILED, (event: SpaBatchFailedEvent) => { * showErrorToast(`Batch action failed: ${event.statusCode}`); * }); * ``` */ declare class SpaBatchFailedEvent extends BaseEvent { private readonly _routeMatch; private readonly _statusCode; private readonly _statusText; constructor(_routeMatch: RouteMatch, _statusCode: number, _statusText: string); get routeMatch(): RouteMatch; /** * The HTTP status code returned by the server. * Expected range: 400–599. */ get statusCode(): number; /** * The HTTP status text returned by the server (e.g. "Forbidden", "Internal Server Error"). */ get statusText(): string; } export { SonataSubmitButton, type SonataSubmitButtonName, SpaBatchConfirmRequestedEvent, SpaBatchFailedEvent, SpaBatchProcessingEvent, SpaBatchSucceededEvent, SpaCrudEvent, SpaDeleteConfirmRequestedEvent, SpaDeleteFailedEvent, SpaDeleteProcessingEvent, SpaDeleteSucceededEvent, SpaDomReadyEvent, SpaEvents, SpaFetchErrorEvent, SpaFormSubmitEvent, SpaNavigateCompletedEvent, SpaRequestEvent, SpaResponseEvent, SpaRouteResolvedEvent, SpaServerRedirectEvent, SpaSwapAfterEvent, SpaSwapEvent };