diff --git a/client/src/controllers/InitController.test.js b/client/src/controllers/InitController.test.js
index 088abb8d72..e48a8feeea 100644
--- a/client/src/controllers/InitController.test.js
+++ b/client/src/controllers/InitController.test.js
@@ -51,6 +51,7 @@ describe('InitController', () => {
expect(handleEvent).toHaveBeenCalledTimes(1);
expect(testDiv.getAttribute('data-controller')).toBeNull();
+ expect({ ...testDiv.dataset }).toEqual({});
});
});
@@ -147,5 +148,77 @@ describe('InitController', () => {
'other-custom:event',
]);
});
+
+ it('should support the ability to block additional events and classes removal', async () => {
+ jest.clearAllMocks();
+
+ expect(handleEvent).not.toHaveBeenCalled();
+
+ document.addEventListener(
+ 'w-init:ready',
+ (event) => {
+ event.preventDefault();
+ },
+ { once: true },
+ );
+
+ const article = document.createElement('article');
+ article.id = 'article';
+ article.innerHTML = `
CONTENT
`;
+
+ document.body.append(article);
+
+ await jest.runAllTimersAsync();
+
+ // only once - the w-init, no other events should fire
+ expect(handleEvent).toHaveBeenCalledTimes(1);
+ expect(handleEvent).toHaveBeenLastCalledWith(
+ expect.objectContaining({ type: 'w-init:ready' }),
+ );
+ });
+ });
+
+ describe('when using detail for the dispatched events', () => {
+ const handleEvent = jest.fn();
+
+ document.addEventListener('w-init:ready', handleEvent);
+ document.addEventListener('my-custom:event', handleEvent);
+
+ const detail = { someMessage: 'some value' };
+
+ beforeAll(() => {
+ jest.clearAllMocks();
+
+ application?.stop();
+
+ document.body.innerHTML = `
+
+ Test body
+
+ `;
+
+ application = Application.start();
+ });
+
+ it('should dispatch event with a detail', async () => {
+ application.register('w-init', InitController);
+ await Promise.resolve(); // no delay, just wait for the next tick
+
+ expect(handleEvent).toHaveBeenCalledTimes(2);
+
+ const [[firstEvent], [secondEvent]] = handleEvent.mock.calls;
+
+ expect(firstEvent.type).toEqual('w-init:ready');
+ expect(firstEvent.detail).toEqual(detail);
+
+ expect(secondEvent.type).toEqual('my-custom:event');
+ expect(secondEvent.detail).toEqual(detail);
+ });
});
});
diff --git a/client/src/controllers/InitController.ts b/client/src/controllers/InitController.ts
index 336de58750..0eee5d295f 100644
--- a/client/src/controllers/InitController.ts
+++ b/client/src/controllers/InitController.ts
@@ -2,8 +2,8 @@ import { Controller } from '@hotwired/stimulus';
import { debounce } from '../utils/debounce';
/**
- * Adds the ability for a controlled element to add or remove classes
- * when ready to be interacted with.
+ * Adds the ability for a controlled element to dispatch an event and also
+ * add or remove classes when ready to be interacted with.
*
* @example - Dynamic classes when ready
*
@@ -14,20 +14,32 @@ import { debounce } from '../utils/debounce';
*
* When the DOM is ready, two additional custom events will be dispatched; `custom:event` and `other-custom:event`.
*
+ *
+ * @example - Detail dispatching
+ *
+ * When the DOM is ready, the detail with value of a JSON object above will be dispatched.
+ *
*/
export class InitController extends Controller
{
static classes = ['ready', 'remove'];
static values = {
delay: { default: -1, type: Number },
+ detail: { default: {}, type: Object },
event: { default: '', type: String },
};
- declare readonly readyClasses: string[];
- declare readonly removeClasses: string[];
-
- declare eventValue: string;
+ /** The delay before applying ready classes and dispatching events. */
declare delayValue: number;
+ /** The detail value to be dispatched with events when the element is ready. */
+ declare detailValue: Record;
+ /** The custom events to be dispatched when the element is ready. */
+ declare eventValue: string;
+
+ /** The classes to be added when the element is ready. */
+ declare readonly readyClasses: string[];
+ /** The classes to be removed when the element is ready. */
+ declare readonly removeClasses: string[];
connect() {
this.ready();
@@ -41,25 +53,56 @@ export class InitController extends Controller {
* Support the ability to also dispatch custom event names.
*/
ready() {
- const events = this.eventValue.split(' ').filter(Boolean);
const delayValue = this.delayValue;
+ const detail = { ...this.detailValue };
debounce(() => true, delayValue < 0 ? null : delayValue)().then(() => {
this.element.classList.add(...this.readyClasses);
this.element.classList.remove(...this.removeClasses);
- this.dispatch('ready', { bubbles: true, cancelable: false });
- events.forEach((name) => {
- this.dispatch(name, { bubbles: true, cancelable: false, prefix: '' });
- });
+
+ if (
+ this.dispatch('ready', {
+ bubbles: true,
+ cancelable: true,
+ detail,
+ }).defaultPrevented
+ ) {
+ return;
+ }
+
+ this.eventValue
+ .split(' ')
+ .filter(Boolean)
+ .forEach((name) => {
+ this.dispatch(name, {
+ bubbles: true,
+ cancelable: false,
+ detail,
+ prefix: '',
+ });
+ });
+
this.remove();
});
}
/**
* Allow the controller to remove itself as it's no longer needed when the init has completed.
+ * Removing the controller reference and all other specific value/classes data attributes.
*/
remove() {
const element = this.element;
+
+ (this.constructor as typeof InitController).classes.forEach((key) => {
+ element.removeAttribute(`data-${this.identifier}-${key}-class`);
+ });
+
+ Object.keys((this.constructor as typeof InitController).values).forEach(
+ (key) => {
+ element.removeAttribute(`data-${this.identifier}-${key}-value`);
+ },
+ );
+
const controllerAttribute = this.application.schema.controllerAttribute;
const controllers =
element.getAttribute(controllerAttribute)?.split(' ') ?? [];