mirror of
https://github.com/wagtail/wagtail.git
synced 2024-11-29 01:22:07 +01:00
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
This commit is contained in:
parent
0e9bdca5b1
commit
514a0aab9f
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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(' ') ?? [];
|
||||
|
Loading…
Reference in New Issue
Block a user