Update InitController to support dispatching detail

- Update JSDoc throughout controller for better internal documentation
- Ensure we 'clean up' the other controller attributes when the init has completed
- Allow for the ready event to have the preventDefault called and stop other events from dispatching
- Add unit tests to support the above changes
pull/11703/head
Chiemezuo 2024-02-20 13:11:22 +01:00 zatwierdzone przez LB (Ben Johnston)
rodzic 0e9bdca5b1
commit 514a0aab9f
2 zmienionych plików z 127 dodań i 11 usunięć

Wyświetl plik

@ -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 = `<p data-controller="w-init" data-w-init-event-value="custom:event">CONTENT</p>`;
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 = `
<article
id="test"
class="test-detail"
data-controller="w-init"
data-w-init-detail-value='${JSON.stringify(detail)}'
data-w-init-event-value='my-custom:event'
>
Test body
</article>
`;
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);
});
});
});

Wyświetl plik

@ -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
* <div class="keep-me hide-me" data-controller="w-init" data-w-init-remove-class="hide-me" data-w-init-ready-class="loaded">
@ -14,20 +14,32 @@ import { debounce } from '../utils/debounce';
* <div class="keep-me hide-me" data-controller="w-init" data-w-init-event-value="custom:event other-custom:event">
* When the DOM is ready, two additional custom events will be dispatched; `custom:event` and `other-custom:event`.
* </div>
*
* @example - Detail dispatching
* <article data-controller="w-init" data-w-init-detail-value='{"status": "success", "message": "Article has entered the room"}'>
* When the DOM is ready, the detail with value of a JSON object above will be dispatched.
* </article>
*/
export class InitController extends Controller<HTMLElement> {
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<string, unknown>;
/** 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<HTMLElement> {
* 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(' ') ?? [];