CustomEvents in JavaScript
Taking notes on CustomEvents in JavaScript. Creating a list of CustomEvents that I use in my own design system so that I can more easily keep track of them
References
Custom Events
The CustomEvent
interface represents events initialized by an application for any purpose.
CustomEvent.detail
- the read-only property of the CustomEvent
interface returns any data when initializing an event.
CustomEvent.bubbles
- set to true
if you want the event to bubble up through the DOM tree, else set to false
if you don't want the event to bubble.
Events created and dispatched by the client-side JavaScript code, rather than by the browser, are often called synthetic events.
Events can be created with the new Event()
and new CustomEvent()
constructors, and they can be dispatched with HTMLElement.dispatchEvent(Event|CustomEvent)
.
Compatibility
Why Use Custom Events
- If you are creating an npm package and you want to notify users of this package that some event that they might want to listen to has occurred or you want to send some manipulated / created data back to the user.
- The user can't access variables in another module unless those variables are exported or passed to the user through custom events.
- Example: I use the @pquina/pintura package for image editing, and I listen for the
process
,loaderror
, andprocesserror
Custom Events that are dispatched on the editor so that that I can handle them appropriately. - I use many HTMX events as well.
- Similar to why you might want to dispatch Custom Events in a npm package, you might want to dispatch events when you use lazy loading of JavaScript code.
- It is sometimes easier to access data across modularized code through events rather than by importing the module and accessing the data through a getter-like function.
Adding Custom Events in Typescript
Probably the best way to add Custom Events in TypeScript would be to follow the answer to this stack overflow question and modify the addEventListener
/ dispatchEvent
definitions in the global namespace as seen in the Typescript documentation. Below, you can see an example of modifying the global namespace to add the custom events "customnumberevent"
and "anothercustomevent"
, which have a detail
property (see above) of type number
and CustomParams
.
interface CustomEventMap {
"customnumberevent": CustomEvent<number>;
"anothercustomevent": CustomEvent<CustomParams>;
}
declare global {
interface Document { //adds definition to Document, but you can do the same with HTMLElement
addEventListener<K extends keyof CustomEventMap>(type: K,
listener: (this: Document, ev: CustomEventMap[K]) => void): void;
dispatchEvent<K extends keyof CustomEventMap>(ev: CustomEventMap[K]): void;
}
}
export { }; //keep that for TS compiler.
While the above may be the best way to implement Custom Events in TypeScript, I use the below method because it is easier and since I have a weird way of transpiling TypeScript - enough so that modifying global type definition wouldn't help much.
type RandomType = CustomEvent<{ a: "Hello World" }>
function handleA(a: Event) {
const event = a as RandomType;
const detail = event.detail.a;
}
document.addEventListener('CHANGE',handleA);
Custom Events in My Design System
NEW_CONTENT_LOADED
type NEW_CONTENT_LOADED_Event = CustomEvent<{ el: HTMLElement|undefined }>
- The
NEW_CONTENT_LOADED
event should be dispatched whenever new content is loaded in the DOM. We check all of the newly loaded elements to see if we should add listeners to them or register Lexical or Code editors. - If the
el
attribute indetail
is missing, then we usedocument.querySelector
to search for elements to add listeners to, else we useel.querySelector
to search for elements to add listeners to.
- The
tabChange
type tabChange_Event = CustomEvent<{ from: number, to: number: wrapper: HTMLDivElement }>
- Listen for tab changes. I created this event originally so that we can use one google maps instance, but load different geographies based on whether or not the tab has changed.
new_element_added
type VALID_CURRENT_MODES = "circle" | "ellipse" | "line" | "path" | "polygon" | "polyline" | "text" | "manipulate" | "rectangle"
type new_element_added_Event = CustomEvent<{ type: VALID_CURRENT_NODES, shape: PIXI.Graphics|PIXI.Text }>
- This event is current used in the create svg implementation to notify other elements that an element has been added to the
<canvas>
.
- This event is current used in the create svg implementation to notify other elements that an element has been added to the
lexical_state_registered
type lexical_state_registered_Event = CustomEvent<undefined>
- Event is dispatched on the form that wraps around the lexical editor to notify the form element that a new lexical state has been registered.
- This is used so that the form can keep track of the state of the lexical editors in the form. It is currently used so that we can warn users when they are about to navigate away from a page with unsaved changes.
media-upload-complete
type objs_type = { url: string, height?:number, width?: number}[];
type media_upload_complete_Event = CustomEvent<{ upload: objs_type }>
- This event is dispatched on the input element that accepts image, audio, or video files.
- The reason this event is dispatched / needs to be dispatched is because the default event that a change in the
<input>
element triggers is prevented withe.preventDefault()
and also because we typically don't care about the object that was uploaded by the user, but we do care about the object that is stored in the S3 database. - In other words, we care about the media object after it has been uploaded to the database.
- The
url
attribute is equal to the URL of the object stored in S3 (or its CloudFront equivalent) and the width/height attributes are equal to the width / height of the image / video that was uploaded (or they areundefined
in the case of audio files). - This event is triggered when images, audio files, or video files are uploaded to the application.
nav-media
type nav_media_Event = CustomEvent<{ detail: "next"|"back" }>
- For multiple image inputs, we only want to use one pintura image editor to edit each of the images, but we want to be able to switch between images to edit. We need to use this
nav-media
event to listen for when users want to navigate between images to edit, so that we can load the new image appropriately into the image editor.
- For multiple image inputs, we only want to use one pintura image editor to edit each of the images, but we want to be able to switch between images to edit. We need to use this
gif-upoad
// urls is a string with multiple urls conatentated by joining with | as the delimeter - might want to change that and use a string array
type gif_upload_Event = CustomEvent<{ urls: string }>
- GIFs can't be edited using the pintura image editor, so we need our own custom upload process for them. As a result, we need the
gif-upload
event, which is like themedia-upload-complete
event, but for gifs.
- GIFs can't be edited using the pintura image editor, so we need our own custom upload process for them. As a result, we need the
delete-image
type delete_image_Event = CustomEvent<{ inputName: string }>
- Images are kept track of using a client side object that holds references to the pintura image editor for that image input. When you delete an image input, you also want to delete the reference to the input on the client side object. This is where the
delete-image
event comes into play. Thedelete-image
event deletes from the client side object references to the image input with the nameinputName
. - All image inputs and the client side object that keeps track of images are deleted on page change.
- Images are kept track of using a client side object that holds references to the pintura image editor for that image input. When you delete an image input, you also want to delete the reference to the input on the client side object. This is where the
HIDE_OPEN_POPOVER
type HIDE_OPEN_POPOVER_event = CustomEvent<{ str: string }>
- I use floating-ui for (custom) tooltips, menus, floating action buttons, and select components. The library is great for these elements - elements that need to be positioned absolutely but need to appear in the same place relative to another element.
- The custom popovers are dispatched on a click or mouseover event, and the element they show (the popover) is identified by the
data-pelem
attribute on the element that dispatched the popover. Thedata-pelem
attribute should look like a CSS query selector. - Open popovers and their update functions (functions that run to make sure the elements are positioned correctly after a scroll event or some manipulation of the screen) are stored in a client side object.
- Custom popovers are normally closed ad removed from the client side object on a
click
event outside of the element or ablur
event, but sometimes you need to force the element to close - which is why the HIDE_OPEN_POPOVER custom event was created. This event allows you to force the open popover to be closed and removed from the client side object. - The
str
attribute should be a string that is equal to thedata-pelem
attribute of the element that dispatched the popover.
delete-lexical-instance
type delete_lexical_instance_Event = CustomEvent<undefined>
- The
delete-lexical-instance
event should be dispatched on an element that is a wrapper for lexical instance -<div data-rich-text-editor
, and when this event is dispatched on a rich text editor wrapper, the reference to that Lexical Editor in the client side object is deleted.
- The
OPEN_DIALOG
type OPEN_DIALOG_Event = CustomEvent<{ dialog: HTMLDialogElement }>
- The
OPEN_DIALOG
event should be dispatched when you want to open a dialog properly and you don't have access to theopenDialog()
function in the main script. This should be utilized in lazy-loaded scripts.
- The
CLOSE_DIALOG
type CLOSE_DIALOG_Event = CustomEvent<{ dialog: HTMLDialogElement }>
- The
CLOSE_DIALOG
event should be dispatched when you want to close a dialog properly and you don't have access to thecloseDialog()
function in the main script. This should be utilized in lazy-loaded scripts.
- The
REGISTER_LEXICAL_CODE_EDITOR
type REGISTER_LEXICAL_CODE_EDITOR_Event = CustomEvent<{ detail: HTMLElement }>
- This event is registered on the
document
. - This
CustomEvent
is used for theHtmlNode
in the Lexical implementation, and it probably should not be used elsewhere.
- This event is registered on the
GET_CODEMIRROR_TEXT
type GET_CODEMIRROR_TEXT_Event = CustomEvent<{ id: string }>
- This event is registered on the
document
. - The id provided in this event, which should be the id of the
div[data-editor-wrapper]
, is used to get the input to the code mirror editor that is wrapped with the wrapper. - This event then dispatches the
GO_CODEMIRROR_TEXT
event on thatdiv[data-editor-wrapper]
.
- This event is registered on the
GET_LEXICAL_EDITORS_CODE
type GET_LEXICAL_EDITORS_CODE_Event = CustomEvent<>
- This event is registered on the
form#custom-html-form
and on any element that was provided in the detail of theREGISTER_LEXICAL_CODE_EDITOR
custom event. It was created so that the user can edit custom HTML inside a lexical instance inside aHtmlNode
. - The event looks for a
div[id^="html_wrapper"]
inside or on the element that it was dispatched. If this element exists, then we get the code inside that editor, and dispatch aGOT_LEXICAL_EDITORS_CODE
event on that editor.
- This event is registered on the
INSERT_LEXICAL_CODE_EDITORS
type INSERT_LEXICAL_CODE_EDITORS_Event = CustomEvent<{ detail: HTMLElement }>
- This event looks for all
div[data-codemirror]
elements inside the element provided in the detail, and it registers those editors.
- This event looks for all
REMOVE_LEXICAL_CODE_EDITORS
type REMOVE_LEXICAL_CODE_EDITORS_Event = CustomEvent<{ }>
- Unregisters the editor with
id^="html_wrapper_"
that is inside of element that dispatched the event. - The event should be dispatched on an element with a code mirror editor inside of it.
- Unregisters the editor with
GOT_LEXICAL_EDITORS_CODE
type GOT_LEXICAL_EDITORS_CODE_Event = CustomEvent<{ html: string }>
- This event is dispatch on any element that listens for the
GET_LEXICAL_EDITORS_CODE
event. - It returns the html code inside the code mirror editor that has a wrapper with an id
^="html_wrapper"
.
- This event is dispatch on any element that listens for the
GOT_CODEMIRROR_TEXT
type GOT_CODEMIRROR_TEXT_Event = CustomEvent<{ code: string }>
- This event is dispatched on and should be listened to on the
div[data-editor-wrapper]
element. The event was created so that you can get the text of a code mirror editor without having to dynamically load the library. - It returns the code inside of the element with the id that was provided in the
GET_CODEMIRROR_TEXT
event. - It is called after the
GET_CODEMIRROR_TEXT
event.
- This event is dispatched on and should be listened to on the
OPEN_SNACKBAR
type GOT_CODEMIRROR_TEXT_Event = CustomEvent<{ selem: string }>
- This event is dispatched on the
document
- You should pass in the snackbar selector string to the detail of the event.
- The purpose of this custom event is to open a snackbar that matches the selector string.
- This event is dispatched on the
Comments
There are currently no comments to show for this article.