import { EventSubscriberInterface } from '@wlindabla/event_dispatcher'; import { BrowserEventDispatcher } from '@wlindabla/event_dispatcher/browser'; import { SpaCrudEvent, SpaFormSubmitEvent } from './events.cjs'; import { PageFetcherInterface, SpaSubscriberInterface, DomSwapManagerInterface, HistoryManagerInterface, DeleteFetcherInterface, RouteResolverInterface, BatchFetcherInterface } from './contracts.cjs'; import { SpaRequest, RouteMatch, SpaResponse, SpaRouterOptions } from './types.cjs'; import { FetchDelegateInterface, FetchRequestInterface, FetchResponseInterface } from '@wlindabla/http_client/contracts'; import { A as AbstractCRUDPageSubscriber } from './AbstractCRUDPageSubscriber-oJkKwpCi.cjs'; export { D as DefaultBatchSubscriber, a as DefaultDeletionOperationSubscriber } from './AbstractCRUDPageSubscriber-oJkKwpCi.cjs'; import { FormSubmitFailedEvent } from '@wlindabla/form_validator/form-submit'; import { AbstractFormSubmissionSubscriber, HttpRequestSubscriber } from '@wlindabla/form_validator/subscriber'; import '@wlindabla/form_validator/utils'; import '@wlindabla/http_client'; /** * @wlindabla/sonata_spa — FetchDelegateAdapter * Implements FetchDelegateInterface from @wlindabla/http_client. * Bridges the http_client lifecycle callbacks to SPA events. * @author AGBOKOUDJO Franck */ /** * Adapts the @wlindabla/http_client FetchDelegateInterface * to dispatch SPA lifecycle events at each HTTP request stage. * * Each callback dispatches the corresponding spa:fetch:* event * so the developer can hook into any stage of the fetch pipeline. * * Lifecycle: * prepareRequest → spa:fetch:prepare * requestStarted → spa:fetch:started (show loading state) * requestSucceeded → spa:fetch:succeeded * requestFailed → spa:fetch:failed (4xx / 5xx) * requestErrored → spa:fetch:errored (network / timeout) * requestFinished → spa:fetch:finished (always — hide loading state) * * On network error (requestErrored): * Falls back to window.location.href for resilience. * The developer can listen to spa:fetch:errored to handle this case. * @author AGBOKOUDJO Franck */ declare class FetchDelegateAdapter implements FetchDelegateInterface { private readonly dispatcher; /** * The original SPA request that triggered this fetch. * Used to populate event payloads. */ private spaRequest; /** * DOM elements to show loading state on during fetch. * Opacity reduced + pointer events disabled while loading. */ private loadingTargets; constructor(dispatcher: BrowserEventDispatcher); /** * Set the SPA request context for this fetch operation. * Called by PageFetcher before each fetch. * * @param request - The SPA navigation request that triggered this fetch */ setSpaRequest(request: SpaRequest): void; /** * Set the DOM elements to show loading state on. * Called by PageFetcher with mainContentArea and mainContentHeader. * * @param targets - Elements to apply loading styles to */ setLoadingTargets(targets: HTMLElement[]): void; /** * Called just before the HTTP request is sent. * Dispatches spa:fetch:prepare. * Use this to add custom headers dynamically. */ prepareRequest(request: FetchRequestInterface): void; /** * Called when the HTTP request starts. * Dispatches spa:fetch:started and shows loading state. */ requestStarted(request: FetchRequestInterface): void; /** * Called when the server returns a 2xx response. * Dispatches spa:fetch:succeeded. */ requestSucceededWithResponse(request: FetchRequestInterface, fetchResponse: FetchResponseInterface): void; /** * Called when the server returns a 4xx or 5xx response. * Dispatches spa:fetch:failed. */ requestFailedWithResponse(request: FetchRequestInterface, fetchResponse: FetchResponseInterface): void; /** * Called on network error, timeout or abort. * Dispatches spa:fetch:errored and SpaFetchErrorEvent. * Falls back to window.location.href for resilience. */ requestErrored(request: FetchRequestInterface, error: Error): void; /** * Called when the HTTP request finishes — always, success or error. * Dispatches spa:fetch:finished and hides loading state. */ requestFinished(request: FetchRequestInterface): void; /** * Apply or remove loading styles on the registered loading targets. * Reduces opacity and disables pointer events while loading. * * @param loading - true to show loading, false to hide */ private setLoading; requestPreventedHandlingResponse(request: FetchRequestInterface, fetchResponse: FetchResponseInterface): void; } /** * @wlindabla/sonata_spa — PageFetcher * Encapsulates all HTTP fetching logic using @wlindabla/http_client. * Used by Page Subscribers to fetch HTML content from the Symfony server. * @author AGBOKOUDJO Franck */ /** * Handles all HTTP page fetching for the SPA router. * Uses @wlindabla/http_client under the hood. * * Two fetch modes: * * fetchFragment(url) * → Sends X-Requested-With: XMLHttpRequest header * → Used for list pages — server can return a partial HTML fragment * → Faster — less HTML to transfer * * fetchFullPage(url) * → No XHR header — server returns full HTML page * → Used for show and dashboard pages * → The SpaKernel extracts #app-main from the full HTML * * Both methods return a SpaResponse with: * - html: raw HTML string * - virtualDoc: DOMParser result — ready for DOM swap * - routeMatch: the RouteMatch that triggered this fetch * - statusCode: HTTP response status code * @author AGBOKOUDJO Franck */ declare class PageFetcher implements PageFetcherInterface { private readonly dispatcher; private readonly delegate; private readonly fetchRequest; private readonly _defaultFetchRequestOptions; /** * @param dispatcher - The BrowserEventDispatcher for spa:fetch:* events * @param mainContentArea - The main content area element for loading state * @param mainContentHeader - The content header element for loading state (nullable) */ constructor(dispatcher: BrowserEventDispatcher, delegate: FetchDelegateAdapter, mainContentArea: HTMLElement, mainContentHeader?: HTMLElement | null); /** * Fetch a page fragment via AJAX. * * Sends X-Requested-With: XMLHttpRequest so the Symfony controller * can detect AJAX requests and return a partial HTML response. * * Used for: list pages, filtered list, paginated list, sorted list. * * @param url - The URL to fetch * @param spaRequest - The original SPA request (for event payloads) * @param routeMatch - The resolved RouteMatch * @returns SpaResponse with parsed HTML */ fetchFragment(url: string, spaRequest: SpaRequest, routeMatch: RouteMatch): Promise; /** * Fetch a full HTML page. * * No XHR header — server returns the complete HTML page. * Used when we need to replace the entire #app-main container. * * Used for: show pages, dashboard, first navigation from dashboard. * * @param url - The URL to fetch * @param spaRequest - The original SPA request (for event payloads) * @param routeMatch - The resolved RouteMatch * @returns SpaResponse with parsed full HTML */ fetchFullPage(url: string, spaRequest: SpaRequest, routeMatch: RouteMatch): Promise; /** * Build a SpaResponse from raw HTML. * Parses the HTML with DOMParser and constructs the response object. * * @param html - Raw HTML string from the server * @param routeMatch - The RouteMatch that triggered this fetch * @param statusCode - HTTP response status code * @returns SpaResponse ready for DOM swap */ private buildSpaResponse; /** * Update the loading targets when DOM references change after a swap. * Called by Page Subscribers after a full page swap that replaces * mainContentArea and mainContentHeader. * * @param mainContentArea - The new content area element * @param mainContentHeader - The new content header element (nullable) */ updateLoadingTargets(mainContentArea: HTMLElement, mainContentHeader: HTMLElement | null): void; } /** * @wlindabla/sonata_spa — ListPageSubscriber * Handles crud:list events — fetches fragment and performs surgical DOM swap. * @author AGBOKOUDJO Franck */ /** * Handles navigation to Sonata list pages (crud:list). * * Full pipeline on crud:list: * 1. PageFetcher.fetchFragment(url) → GET with X-Requested-With: XMLHttpRequest * 2. dispatch SpaResponseEvent → developer can mutate HTML before swap * 3. DomSwapManager.swap() → ListSwapStrategy (surgical swap) * 4. HistoryManager.push(url) → pushState * 5. DomManager.reinitialize() → scripts, BS5, Stimulus, pagination * 6. dispatch SpaNavigateCompletedEvent * * @example * ```typescript * // Registered automatically by SpaKernel.boot() * dispatcher.addSubscriber(new ListPageSubscriber( * dispatcher, fetcher, swapManager, historyManager, domManager * )); * ``` */ declare class ListPageSubscriber extends AbstractCRUDPageSubscriber implements SpaSubscriberInterface { private readonly dispatcher; private readonly historyManager; constructor(dispatcher: BrowserEventDispatcher, _fetcher: PageFetcher, _swapManager: DomSwapManagerInterface, historyManager: HistoryManagerInterface, _mainContainer: HTMLElement, _mainContentArea: HTMLElement, _mainContentHeader: HTMLElement | null, _options: SpaRouterOptions); getSubscribedEvents(): ReturnType; /** * Handle crud:list event. * Fetches the list fragment and performs a surgical DOM swap. * * @param event - The SpaCrudEvent dispatched by SpaKernel */ onList(event: SpaCrudEvent): Promise; } /** * @wlindabla/sonata_spa — ShowPageSubscriber * Handles crud:show events — fetches full page and replaces #app-main. * @author AGBOKOUDJO Franck */ /** * Handles navigation to Sonata show pages (crud:show). * * Unlike list pages, show pages require a full page fetch * because their layout is completely different from the list. * * Full pipeline on crud:show: * 1. PageFetcher.fetchFullPage(url) → GET full HTML page * 2. dispatch SpaResponseEvent → developer can mutate HTML before swap * 3. DomSwapManager.swap() → ShowSwapStrategy (replaces #app-main) * 4. Update DOM references → mainContentArea + mainContentHeader * 5. HistoryManager.push(url) → pushState * 6. dispatch SpaNavigateCompletedEvent */ declare class ShowPageSubscriber extends AbstractCRUDPageSubscriber implements SpaSubscriberInterface { private readonly dispatcher; private readonly historyManager; constructor(dispatcher: BrowserEventDispatcher, _fetcher: PageFetcherInterface, _swapManager: DomSwapManagerInterface, historyManager: HistoryManagerInterface, _mainContainer: HTMLElement, _mainContentArea: HTMLElement, _mainContentHeader: HTMLElement | null, _options: SpaRouterOptions); getSubscribedEvents(): ReturnType; /** * Handle crud:show event. * Fetches the full page and replaces the entire #app-main container. * * @param event - The SpaCrudEvent dispatched by SpaKernel */ onShow(event: SpaCrudEvent): Promise; /** * Get the current mainContentArea reference. * Used by SpaKernel to keep references in sync after a show swap. */ getMainContentArea(): HTMLElement; /** * Get the current mainContentHeader reference. */ getMainContentHeader(): HTMLElement | null; } /** * @wlindabla/sonata_spa — DeletePageSubscriber * Handles crud:delete events — fetches CSRF token, shows confirmation, * then POSTs the delete request. * @author AGBOKOUDJO Franck */ /** * Handles navigation to Sonata delete pages (crud:delete). * * Full pipeline on crud:delete: * 1. DeleteConfirmFetcher.fetch(url) → GET delete page → extract csrfToken * 2. dispatch SpaDeleteConfirmRequestedEvent → UI shows confirmation modal * The developer's confirmation handler calls: * event.accept() → proceeds with DELETE POST * event.cancel() → dispatches spa:delete:confirm:cancelled * 3. If accepted → POST deleteUrl with csrfToken * 4. On success → navigate to list page (Sonata redirects after delete) * 5. dispatch SpaNavigateCompletedEvent * * The developer provides the confirmation UI by listening to: * SpaEvents.DELETE_CONFIRM_REQUESTED * * @example * ```typescript * // Custom confirmation with SweetAlert2 * dispatcher.addListener(SpaEvents.DELETE_CONFIRM_REQUESTED, async (event) => { * const result = await Swal.fire({ * title: event.title ?? 'Are you sure?', * text: event.message ?? 'This action cannot be undone.', * icon: 'warning', * showCancelButton: true, * confirmButtonText: event.btnDeleteText ?? 'Delete', * }); * * if (result.isConfirmed) { * await event.accept(); * } else { * event.cancel(); * } * }); * ``` */ declare class DeletePageSubscriber implements SpaSubscriberInterface { private readonly dispatcher; private readonly deleteFetcher; private readonly navigate; constructor(dispatcher: BrowserEventDispatcher, deleteFetcher: DeleteFetcherInterface, navigate: (url: string) => Promise); getSubscribedEvents(): ReturnType; /** * Handle crud:delete event. * * @param event - The SpaCrudEvent dispatched by SpaKernel */ onDelete(event: SpaCrudEvent): Promise; /** * Perform the actual DELETE POST request with the CSRF token. * Dispatches the full delete lifecycle: * - {@link SpaEvents.DELETE_PROCESSING} immediately after user confirms * - {@link SpaEvents.DELETE_SUCCEEDED} on 2xx response * - {@link SpaEvents.DELETE_FAILED} on 4xx–599 response * * On success, navigates to the list page after a short delay * so consumers of DELETE_SUCCEEDED have time to display feedback. * * @param deleteUrl - The Sonata delete URL * @param csrfToken - The CSRF token extracted from the delete confirmation page * @param resource - The resource name (used to build the fallback list URL) */ private performDelete; /** * Build the list URL from the delete URL. * Fallback when the server does not redirect. * * Example: * /admin/app/user/42/delete → /admin/app/user/list */ private buildListUrl; } /** * @wlindabla/sonata_spa — DashboardSubscriber * Handles spa:dashboard events — fetches full page and replaces #app-main. * @author AGBOKOUDJO Franck */ /** * Handles navigation to the SonataAdmin dashboard (spa:dashboard). * * The dashboard has a unique layout — widgets, charts, statistics — * completely different from CRUD pages. * Like ShowPageSubscriber, it performs a full #app-main swap. * * Full pipeline on spa:dashboard: * 1. PageFetcher.fetchFullPage(url) → GET full dashboard HTML * 2. dispatch SpaResponseEvent → developer can mutate HTML * 3. DomSwapManager.swap() → ShowSwapStrategy (replaces #app-main) * 4. Update DOM references → mainContentArea + mainContentHeader * 5. HistoryManager.push(url) → pushState * 6. dispatch SpaNavigateCompletedEvent */ declare class DashboardSubscriber extends AbstractCRUDPageSubscriber implements SpaSubscriberInterface { private readonly dispatcher; private readonly historyManager; constructor(dispatcher: BrowserEventDispatcher, _fetcher: PageFetcherInterface, _swapManager: DomSwapManagerInterface, historyManager: HistoryManagerInterface, _mainContainer: HTMLElement, _mainContentArea: HTMLElement, _mainContentHeader: HTMLElement | null, _options: SpaRouterOptions); getSubscribedEvents(): ReturnType; /** * Handle spa:dashboard event. * * @param event - The SpaCrudEvent dispatched by SpaKernel */ onDashboard(event: SpaCrudEvent): Promise; } /** * @wlindabla/sonata_spa — FormSubscriber * Handles spa:form:submit events using @wlindabla/form_validator FormSubmission. * Mirrors the real-world usage pattern from production Sonata projects. * @author AGBOKOUDJO Franck */ /** * Handles Sonata form submissions (spa:form:submit). * * Uses @wlindabla/form_validator FormSubmission exactly as in production: * - showLoadingDialog() → during submission * - showSuccessDialog() → on success (reads title + message from JSON) * - showErrorDialog() → on error (reads title + errorMessage from JSON) * - handleErrorsManyForm() → displays 422 field errors on form fields * - confirmMethodRequest → SweetAlert2 confirmation (from data-iwas-confirm) * * Only forms with class "form-submission-handle-auto" are handled automatically. * This mirrors the exact pattern from production Sonata projects. * * Sonata form response contract (JSON): * ```json * // Success * { "title": "Saved!", "message": "Record saved successfully." } * * // Error 422 (validation) * { "title": "Validation Error", "violations": { "fieldName": "error message" } } * * // Error 4xx/5xx * { "title": "Error", "errorMessage": "Something went wrong." } * ``` * * @example * ```typescript * // Registered automatically by SpaKernel.boot() * // The developer customizes behavior by listening to SpaEvents: * * dispatcher.addListener(SpaEvents.FORM_SUCCEEDED, (event) => { * console.log('Form saved:', event.redirectUrl); * }); * * dispatcher.addListener(SpaEvents.FORM_FAILED, (event) => { * console.log('Form failed:', event.error); * }); * ``` * @author AGBOKOUDJO Franck */ declare class FormSubscriber extends AbstractFormSubmissionSubscriber implements SpaSubscriberInterface { private readonly dispatcher; private readonly swapManager; private readonly routeResolver; private readonly navigate; private readonly mainContainer; private readonly mainContentArea; private readonly mainContentHeader; constructor(dispatcher: BrowserEventDispatcher, swapManager: DomSwapManagerInterface, routeResolver: RouteResolverInterface, navigate: (url: string) => Promise, mainContainer: HTMLElement, mainContentArea: HTMLElement, mainContentHeader: HTMLElement | null); getSubscribedEvents(): ReturnType; /** * Handle spa:form:submit event. * * Only processes forms with class "form-submission-handle-auto". * This mirrors the exact production pattern. * * @param event - The SpaFormSubmitEvent dispatched by FormBindingManager */ onFormSubmit(event: SpaFormSubmitEvent): Promise; /** * Register listeners on FormSubmission lifecycle events. * Mirrors the FormSubmissionSubscriber pattern from production. * * Uses { once: true } so listeners are automatically removed * after each form submission. */ private registerFormSubmissionListeners; onFormSubmitFailed(event: FormSubmitFailedEvent): Promise; /** * Resolve the redirect URL after a successful form submission. * * Priority: * 1. URL returned by server in JSON response * 2. Derived from the submitter button name (Sonata convention) * 3. Fallback to list page URL * * Sonata button → redirect mapping: * btn_update_and_list → list page * btn_create_and_list → list page * btn_update_and_edit → edit page (server provides URL) * btn_create_and_edit → edit page (server provides URL) * btn_create_and_create → create page (same URL) * * @param event - The SpaFormSubmitEvent * @param data - The JSON response data from server */ private resolveRedirectUrl; /** * Build the list URL from a create/edit form action URL. * * Examples: * /admin/app/user/create → /admin/app/user/list * /admin/app/user/42/edit → /admin/app/user/list */ private buildListUrl; /** * Swap .sonata-ba-form to display server-side validation errors. * Called when server returns 200 + HTML form with error markup. * * @param html - HTML response from server with validation errors * @param form - The submitted form element */ private swapFormErrors; } /** * @wlindabla/sonata_spa — BatchPageSubscriber * Handles crud:batch events — posts the batch form, shows the confirmation modal, * then re-submits with confirmation=ok and dispatches the batch lifecycle events. * @author AGBOKOUDJO Franck */ /** * Handles navigation to Sonata batch pages (crud:batch). * * Full pipeline on crud:batch: * 1. Read the submitted batch form — check that at least one item is selected * 2. POST to Sonata batch URL → Sonata returns the confirmation HTML page * 3. BatchFetcher parses the page → extracts csrfToken, encodedData, title, message * 4. Dispatch {@link SpaEvents.BATCH_CONFIRM_REQUESTED} → UI shows confirmation modal * The developer's handler calls: * event.accept() → proceeds with the confirmed batch POST * event.cancel() → dispatches {@link SpaEvents.BATCH_CONFIRM_CANCELLED} * 5. If accepted → POST batch URL with confirmation=ok + csrfToken + encodedData * 6. On success → dispatch {@link SpaEvents.BATCH_SUCCEEDED} → navigate to list * 7. On failure → dispatch {@link SpaEvents.BATCH_FAILED} * * The developer provides the confirmation UI by listening to: * {@link SpaEvents.BATCH_CONFIRM_REQUESTED} * * @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, * confirmButtonText: event.confirmData.btnDeleteText ?? 'Execute', * }); * if (result.isConfirmed) { await event.accept(); } else { event.cancel(); } * }); * ``` */ declare class BatchPageSubscriber implements SpaSubscriberInterface { private readonly dispatcher; private readonly batchFetcher; private readonly navigate; constructor(dispatcher: BrowserEventDispatcher, batchFetcher: BatchFetcherInterface, navigate: (url: string) => Promise); getSubscribedEvents(): ReturnType; /** * Handle crud:batch event. * Reads the form, checks the selection, fetches the confirmation page, * then delegates to the developer's confirmation UI handler. * * @param event - The SpaCrudEvent dispatched by SpaKernel */ onBatch(event: SpaCrudEvent): Promise; /** * Execute the confirmed batch POST request and dispatch the lifecycle events. * * Dispatches the full batch lifecycle: * - {@link SpaEvents.BATCH_PROCESSING} immediately after user confirms * - {@link SpaEvents.BATCH_SUCCEEDED} on 2xx response * - {@link SpaEvents.BATCH_FAILED} on 4xx–599 response * * On success, navigates to the list page after a short delay so consumers * of BATCH_SUCCEEDED have time to display feedback. * * @param confirmData - The data extracted from the Sonata confirmation page * @param routeMatch - The RouteMatch resolved from the original batch URL */ private performBatch; } /** * Bridges the exactOptionalPropertyTypes incompatibility between * HttpRequestSubscriber (compiled with `priority?: number | undefined`) * and EventSubscriberInterface (declared with `priority?: number`). * * The cast through `unknown` is intentional and safe here — both types * are structurally identical at runtime; the difference only exists at * the TypeScript type-checking level due to exactOptionalPropertyTypes. */ declare class SonataHttpRequestSubscriber extends HttpRequestSubscriber { constructor(); getSubscribedEvents(): ReturnType; } export { BatchPageSubscriber, DashboardSubscriber, DeletePageSubscriber, FormSubscriber, ListPageSubscriber, ShowPageSubscriber, SonataHttpRequestSubscriber };