RFC: JS Event Listeners

RFC: JS Event Listeners

Andrew Lyons -
回帖数:4
Core developers的头像 Moodle HQ的头像 Particularly helpful Moodlers的头像 Peer reviewers的头像 Plugin developers的头像 Testers的头像

Hi folks,

It's been a big few years in Moodle for Javascript. We've migrated to support of the newer ES6-style Javascript modules (MDL-62497 since Moodle 3.8), dropped support for Internet Explorer (MDLSITE-6109 since Moodle 3.10), added support for sub-directories in Javascript (MDL-66192 since Moodle 3.8), and we are constantly trying to improve the landscape for our developers through a range of other micro and macro changes.

I know that many of you are enjoying the newer style of JS module, and finding them nicer and easier to write.

One of the changes that I have been looking at recently is to simplify the generation and consumption of Javascript events.

At present we have a number of custom event systems in use in core, these include:

  • YUI Custom Events (e.g. `Y.publish('someCustomEvent');`);
  • jQuery Custom Events (e.g. `$(document).trigger('someCustomEvent');` and `$(document).on('someCustomEvent', * someHandler);`);
  • the `core/custom_interaction_events` module, which is an extension of the jQuery Custom Events;
  • the `core/pubsub` module (e.g. `PubSub.publish('someCustomEvent', {some: 'custom', data});` and `PubSub.subscribe('someCustomEvent', someHandler);`); and
  • use of native custom events (e.g. `document.addEventListener('someCustomEvent', someHandler);` and `document.dispatchEvent(new CustomEvent('someCustomEvent', {detail: {some: 'custom', data}}));`).

Sadly none of these are very compatible with one another and the presence of some of the older YUI events in particular cause problems. Additionally it's not clear which should be used for new code.

To try and simplify things and make it clearer for new code, I have been looking at a way to migrate away from these various different approaches and to recommend a single approach for all custom events.

The suggestion I am proposing is to migrate the use of all of the above to only use the native custom events. You can read about how to use, trigger, and consume native custom events on the MDN.

This migration will include:

  • deprecation of all custom YUI events (primarily the `moodle-core-events` YUI module) via MDL-70990;
  • deprecation of all custom jQuery events located in `core/event` via MDL-70990;
  • migration of the `core/custom_interaction_events` to use native events (MDL to be confirmed); and
  • allow publishers of `core/pubsub` to plan migration to use native events (MDL to be confirmed).

Deprecation of each of these older systems will each include a migration path to allow the publisher to migrate to the native events, including publishing deprecation notices to the developer console in some cases.

The first issue in this chain is MDL-70990 and it includes a helper module: `core/event_dispatcher` which exports a helper function `dispatchEvent` which is a wrapper around EventTarget.dispatchEvent function and fills in some of the defaults, and sets the default target to `document`.

It also proposes a structure for events putting all event details for a component into a single location: `[component]/events` which exports a list of `eventTypes` and, optionally, some notification functions. For example:

// lib/form/amd/src/events.js

import {dispatchEvent} from 'core/event_dispatcher';

/**
 * Event types for the `core_form` subsystem.
 *
 * @const
 * @property {string} formFieldValidationFailed An event triggered upon form field validation failure.
 */
export const eventTypes = {
    formFieldValidationFailed: 'core_form/formFieldValidationFailed',
};

export const notifyFormFieldValidationFailed = (field, message) => dispatchEvent(eventTypes.formFieldValidationFailed, {
        message,
    }, field, {
        cancelable: true,
    }
);

A module wishing to publish notify or publish the event can do so easily:

import {notifyFormFieldValidationFailed} from 'core_form/events';

// ...

const someFieldInTheForm = document.querySelector('#someFormId input');
notifyFormFieldValidationFailed(someFieldInTheForm, 'The form validation failed for reason x');

Any code which wishes to listen to the above event can do something like the following:

import {eventTypes as formEvents} from 'core_form/events';

// ...

const myForm = document.querySelector('#someFormId');
myForm.addEventListener(formEvents.formFieldValidationFailed, e => {
    window.console.log(e.detail); // "The form validation failed for reason x"
});

I appreciate any feedback that you may have,

Andrew

平均分:Useful (3)
回复Andrew Lyons

Re: RFC: JS Event Listeners

Tim Hunt -
Core developers的头像 Documentation writers的头像 Particularly helpful Moodlers的头像 Peer reviewers的头像 Plugin developers的头像

I agree with trying to move everything to native custom events. That avoids the xkcd 927 problem:

However, then you start talking about a new 'core/event_dispatcher' AMD module, which does seem like a new standard. Why is that extra layer needed? What benefit does it bring?

回复Tim Hunt

Re: RFC: JS Event Listeners

Andrew Lyons -
Core developers的头像 Moodle HQ的头像 Particularly helpful Moodlers的头像 Peer reviewers的头像 Plugin developers的头像 Testers的头像

Hi Tim,

Thanks for the reply - yes the aim is to reduce the number of competing standards. The core/event_dispatcher module is intended as a convenience function to wrap the creation of a native CustomEvent class instance and [element].dispatchEvent function. It also serves to provide some sensible defaults which make sense for Moodle events.

It is entirely optional and the following can be used instead:

export const notifyFormFieldValidationFailed = (field, message) => {
    const customEvent = new CustomEvent(eventTypes.formFieldValidationFailed, {
        bubbles: true,
        cancelable: true,
        detail: message,
    });

    field.dispatchEvent(customEvent);

    return customEvent;
};

The core/event_dispatcher::dispatch_event makes it significanlty easier, sets a new default value for bubbles (normally false but we want it true), and it also takes an optional element which defaults to document when not specified which also simplifies things.

With the convenience function the above becomes:

export const notifyFormFieldValidationFailed = (field, message) => dispatchEvent(
    eventTypes.formFieldValidationFailed,
    message,
    field,
    {cancelable: true}
);

As I say, it is just a convenience function and the it doesn't change the way that Events are listened to, and doesn't preclude you from using the native way. It can pass all of the same arguments as the native CustomEvent constructor so I wouldn't say it's a different or competing standard.

Hope that helps

Andrew

回复Andrew Lyons

Re: RFC: JS Event Listeners

Andrew Lyons -
Core developers的头像 Moodle HQ的头像 Particularly helpful Moodlers的头像 Peer reviewers的头像 Plugin developers的头像 Testers的头像


Just to give another couple of examples to indicate why I've included the event dispatcher:

 
// Without:
event const notifyContentUpdated = element => {
const customEvent = new CustomEvent(eventTypes.contentUpdated, {bubbles: true});
element.dispatchEvent(customEvent);
return customEvent;
};

// With the event dispatcher:
event const notifyContentUpdated = element => dispatchEvent(eventTypes.contentUpdated, {}, element);

// Without:
event const notifyBlockRemoved = blockInstanceId => {
const customEvent = new CustomEvent(eventTypes.blockRemoved, {
bubbles: true,
detail: {blockInstanceId},
});
document.dispatchEvent(customEvent);
return customEvent;
};

// With the event dispatcher:
event const notifyBlockRemoved = blockInstanceId => dispatchEvent(eventTypes.blockRemoved, {blockInstanceId});

Andrew