On a recent YouTube video of mine, I was asked…
How do you pass callback functions as props to a web component?
Specifically, I want to create a form web component that accepts three callback functions as input:
-
beforeSubmit
: a function to execute before the form is submitted -
afterSubmit
: a function to execute after the form is submitted -
onSubmit
: a function to execute while the form is being submitted
What are the best practices for passing these callback functions as props to the web component?
This is a great question! Let's dig in!
Don't use callbacks
With a typical JavaScript library, you pass callbacks in as part of the instantiate process.
new AjaxForm('#form-element', { beforeSubmit: function () {}, afterSubmit: function () {}, onSubmit: function () {} });
Because Web Components self-instantiate, though, there's no easy way to do that.
<ajax-form> <form> <!-- ... --> </form> </ajax-form>
You could theoretically use global named functions and pass them in as attributes…
<ajax-form before-submit="doTheThing"> <!-- ... --> </ajax-form>
Or attach methods to the element after-the-fact…
let form = document.querySelector('ajax-form'); form.beforeSubmit = function () {};
But this is, in my opinion, the wrong tool for the job.
Don't use callback functions at all. Instead, use custom events.
Custom Events
JavaScript provides developers with a way to emit custom events that developers can listen for with the Element.addEventListener()
method.
We can use custom events to let developers hook into the code that we write and run more code in response to when things happen. They provide a really flexible way to extend the functionality of a library or code base.
And, they're the perfect fit for something like a Web Component!
How to create a custom event
You can create a custom event with the new CustomEvent()
constructor.
Pass in the name of the event as an argument. You can optionally pass in an object of options
: whether or not the event bubbles
, whether or not it's cancelable
, and any detail
you want shared with the event object.
The options.detail
property can be any type of data, including an array or object.
// Create the event let event = new CustomEvent('my-custom-event', { bubbles: true, cancelable: true, detail: 'This is awesome. I could also be an object or array.' });
After creating your event
, you pass it into the Element.dispatchEvent()
method to emit it.
You can emit your event on any element. For Web Components, the custom element is often the right choice.
// Emit the event this.dispatchEvent(event);
Using a custom event in a Web Component
Using the question from my video as an example, let's imagine we have an <ajax-form>
Web Component that runs some code when a form is submitted.
customElements.define('ajax-form', class extends HTMLElement { /** * Instantiate the component */ constructor () { // Gives element access to the parent class properties super(); // Get the form this.form = this.querySelector('form'); // Listen for submit events this.addEventListener('submit', this); } /** * Handle submit events * @param {Event} event The event object */ handleEvent (event) { // Do stuff with the form... } }
Since we'll be emitting multiple custom events, let's create an emit()
method to do the heavy lifting for us…
customElements.define('ajax-form', class extends HTMLElement { /** * Instantiate the component */ constructor () { // ... } // ... /** * Emit a custom event * @param {String} type The event name suffix * @param {Object} detail Details to include with the event */ emit (type, detail = {}) { // Create a new event let event = new CustomEvent(`ajax-form:${type}`, { bubbles: true, cancelable: true, detail: detail }); // Dispatch the event return this.dispatchEvent(event); } }
Now, inside our handleEvent()
method, we can emit our custom events…
/** * Handle submit events * @param {Event} event The event object */ handleEvent (event) { // Emit an event before submitting the form this.emit('before-submit'); // Emit an event while submitting the form this.emit('submit'); // Do stuff with the form... // Emit an event after submitting the form this.emit('after-submit'); }
Then, we can listen for events like this…
let form = document.querySelector('ajax-form'); form.addEventListener('ajax-form:before-submit', function (event) { // Do stuff... });
Or, you can use event delegation to listen for events on all <ajax-form>
elements…
document.addEventListener('ajax-form:before-submit', function (event) { // Do stuff... });
Some details…
We're probably submitting our form asynchronously, so we probably need to add the async
operator to handleEvent()
, and await
when we fetch()
form data
/** * Handle submit events * @param {Event} event The event object */ async handleEvent (event) { // Emit an event before submitting the form this.emit('before-submit'); // Emit an event while submitting the form this.emit('submit'); // Do stuff with the form... let request = await fetch(this.form.action, {}); let response = await request.json(); // Emit an event after submitting the form this.emit('after-submit'); }
We probably also need to actually get the form field data. We might do that first, and pass it along as the detail
for the events.
We might also pass along the response
to the after-submit
event.
/** * Handle submit events * @param {Event} event The event object */ async handleEvent (event) { // Get the form data let data = Object.fromEntries(new FormData(this.form)); // Emit an event before submitting the form this.emit('before-submit', data); // Emit an event while submitting the form this.emit('submit', data); // Do stuff with the form... let request = await fetch(this.form.action, { method: this.form.method, headers: { 'Content-type': 'application/json' }, body: JSON.stringify(data) }); let response = await request.json(); // Emit an event after submitting the form this.emit('after-submit', { fields: data, response }); }
Cancelling the event
One fun thing you can do with custom events is cancel stuff from running using the event.preventDefault()
method.
For example, let's imagine that we wanted to check for the value of the [name="answer"]
field in our form, and only submit the form if it's equal to 42
.
let form = document.querySelector('ajax-form'); form.addEventListener('ajax-form:before-submit', function (event) { if (event.detail.answer !== '42') { event.preventDefault(); } });
If your custom event has the cancelable
option set to true
(ours does), you can use the Event.preventDefault()
method to cancel it.
The Element.dispatchEvent()
method returns false
if the event was canceled, and true
if it was not.
Inside out handleEvent()
method, we can check if our before-submit
event returns false
. If it does, we'll use the return
operator to bail early.
/** * Handle submit events * @param {Event} event The event object */ async handleEvent (event) { // Get the form data let data = Object.fromEntries(new FormData(this.form)); // Emit an event before submitting the form let canceled = !this.emit('before-submit', data); if (canceled) return; // Emit an event while submitting the form this.emit('submit', data); // Do stuff with the form... let request = await fetch(this.form.action, { method: this.form.method, headers: { 'Content-type': 'application/json' }, body: JSON.stringify(data) }); let response = await request.json(); // Emit an event after submitting the form this.emit('after-submit', { fields: data, response }); }
Wrapping up
If you need or want to extend your Web Component, custom events provide a modern, flexible way to do that works better than callback methods.
Cheers,
Chris
Want to share this with others or read it later? View it in a browser.
0 Komentar untuk "[Go Make Things] Callbacks on Web Components?"