Ad/iklan :

3gp Mp4 HD
Play/Download
Live 3D App
Search.Pencarian Menu

Add text send email to rh3252705.adda@blogger.com or Click this (Text porn Will delete) | Tambah teks kirim email ke rh3252705.adda@blogger.com atau Klik ini (Teks porno akan dihapus)
Total post.pos : 17047+

[Go Make Things] Progressively enhancing forms with an HTML Web Component (part 2)

Yesterday, we looked at how I build JavaScript-free interactive web apps using HTML forms and PHP.

Today, I want to share how I progressively enhance those into asynchronous ajaxy interactions using HTML Web Components and a little JavaScript.

Let's dig in!

The HTML in HTML Web Component

HTML Web Components are an approach to building Web Components where you progressively enhance the HTML that's already there instead of rendering it entirely from JavaScript.

I start by wrapping my <form> elements in an <ajax-form> custom element…

<ajax-form>  	<form action="/path/to/add-item.php" method="POST">  		<label for="item">The New Item</label>  		<input type="text" name="item" id="item" required>  		<button>Add Item</button>  		<div role="status"></div>  	</form>  </ajax-form>

Now, we're ready to write some JavaScript!

Instantiating our Web Component

Like any Web Component, this one starts by defining the custom element, passing in a class, and running super() in the constructor() method.

(If you're new to Web Components, I've got a short tutorial on writing your first one here.)

customElements.define('ajax-form', class extends HTMLElement {    	/**  	 * The class constructor object  	 */  	constructor () {    		// Always call super first in constructor  		super();    	}    });  

Next, I get the [role="status"] element used for displaying form announcements and save it to the this.announce property.

If the element doesn't exist yet, I create it. Then, I get this.form from inside my custom element, and append the this.announce element into it.

This could be refactored a bit, but how it works is unique to how my apps get built using Hugo, my preferred Static Site Generator (SSG).

/**   * The class constructor object   */  constructor () {    	// Always call super first in constructor  	super();    	// Add a form status element  	this.announce = this.querySelector('[role="status"]') || document.createElement('div');  	this.announce.setAttribute('role', 'status');    	// Set base properties  	this.form = this.querySelector('form');  	this.form.append(this.announce);    }  

Options & Settings

This Web Component supports a bunch of options.

I'll talk through what they all do in a bit, but the main one is this.msgSubmitting. This is the text that gets displayed when the form is submitting, but before I've gotten a response back from the server.

/**   * The class constructor object   */  constructor () {    	// ...  	this.form.append(this.announce);    	// Define options  	this.msgSubmitting = this.getAttribute('msg-submitting') ?? 'Submitting...';  	this.msgDisappear = this.hasAttribute('msg-disappear');  	this.keepFields = this.hasAttribute('keep-fields');  	this.updateURL = this.getAttribute('update-url');  	this.removeElem = this.getAttribute('remove');  	this.delay = this.hasAttribute('delay') ? 6000 : 0;    }  

With Web Components, all of your options and settings are configured using attributes on the custom element. This one uses the [msg-submitting] attribute.

For example, on a login form, I'd use Logging in... as the status message, like this…

<ajax-form msg-submitting="Logging in...">  	<form action="/path/to/login.php" method="POST">  		<!-- ... -->  	</form>  </ajax-form>
  • msg-submitting - The message to show when the form is submitting.
  • msg-disappear - If this attribute exists, automatically remove success status messages after 6 seconds.
  • keep-fields - If this attribute exists, do not reset the form fields on success.
  • update-url - A URL to update the current window's browser history to (without reloading the page) on success.
  • remove - The selector for a parent element to remove from the DOM entirely on success (will remove the Web Component, too).
  • delay - The number of milliseconds to wait before re-enabling the form on success (prevents accidental multiple submits).

Listening for submit events

Now, we can actually listen for submit events on our form.

I take advantage of the handleEvent() method, which lets you easily preserve the context of this when working with events in a class pattern.

/**   * The class constructor object   */  constructor () {    	// ...  	this.delay = this.hasAttribute('delay') ? 6000 : 0;    	// Listen for events  	this.form.addEventListener('submit', this);    }    /**   * Handle events on the component   * @param  {Event} event The event object   */  handleEvent(event) {  	this[`on${event.type}`](event);  }  

I setup an onsubmit() method to handle my submit events. I also use the async keyword to make it an asynchronous function, since we'll be calling an API.

/**   * Handle submit events   * @param  {Event} event The event object   */  async onsubmit (event) {  	// ...  }  

Some helper methods

I use a few helper methods for working with my form.

The disable() method adds a [form-submitting] attribute, and the enable() method removes it. The isDisable() method checks if that attribute exists.

/**   * Disable a form so I can't be submitted while waiting for the API   */  disable () {  	this.setAttribute('form-submitting', '');  }    /**   * Enable a form after the API returns   */  enable () {  	this.removeAttribute('form-submitting');  }    /**   * Check if a form is submitting to the API   * @return {Boolean} If true, the form is submitting   */  isDisabled () {  	return this.hasAttribute('form-submitting');  }  

I could instead set this.isSubmitting as an instance property, but using an HTML attribute let's you hook into it for styling purposes.

This is preferred over the [disabled] attribute, which can create accessibility issues.

[form-submitting] button {  	/* Make the button appear disabled  */  }

I also have a method to showStatus() messages in my form.

It accepts the msg to display, a boolean indicating if the message is a success or not, and a boolean indicating if the entire form lifecycle is complete.

It displays the text in the this.announce element, and adds a success or error class.

If the element was a success and complete, and the element should be removed, it displays a toast message instead of an inline form message, since the form element is about to get removed from the UI.

And if the message was set to disappear, it gets automatically removed after 6000 milliseconds (or 6 seconds).

/**   * Update the form status in a field   * @param {String}  msg      The message to display   * @param {Boolean} success  If true, add success class   * @param {Boolean} complete If true, form submit is complete   */  showStatus (msg, success, complete) {    	// Show the message  	this.announce.innerHTML = msg;  	this.announce.className = success ? 'success-message' : 'error-message';    	// If content should be removed, switch to toast notification  	if (success && complete && this.removeElem) {  		this.toast();  	}    	// If success and message should disappear  	if (success && this.msgDisappear) {  		setTimeout(() => {  			this.announce.innerHTML = '';  			this.announce.className = '';  		}, 6000);  	}    }  

Why two attributes for what seems like one thing (success and complete)? Submitting is not an error, but the form isn't complete yet either. So that's success = true but complete = false.

Handling events

The first thing I do in the onsubmit() method is run the event.preventDefault() method to stop the form from reloading the page.

Then, I check if the this.isDisabled() already. If so, the form is already submitting, so I return to end the function early. If not, I run this.disable() to disable it.

Then, I show this.msgSubmitting using the showStatus() method, and set success to true.

/**   * Handle submit events   * @param  {Event} event The event object   */  async onsubmit (event) {    	// Stop form from reloading the page  	event.preventDefault();    	// If the form is already submitting, do nothing  	// Otherwise, disable future submissions  	if (this.isDisabled()) return;  	this.disable();    	// Show status message  	this.showStatus(this.msgSubmitting, true);    }  

Now, we're ready to call the API!

Calling the API

I wrap the entire API call in a try-catch-finally block.

This will handle any errors, and do some cleanup after the response is returned. If there's an error, I console.warn() it, and use the showStatus() method to display the error.message in the UI.

In the finally block, I enable() the form again. I run this in a setTimeout() method, using the this.delay property for the number of milliseconds. If undefined by an attribute, it has a default value of 0 and will run immediately.

/**   * Handle submit events   * @param  {Event} event The event object   */  async onsubmit (event) {    	// ...  	this.showStatus(this.msgSubmitting, true);    	try {  		// ...  	} catch (error) {  		console.warn(error);  		this.showStatus(error.message);  	} finally {  		setTimeout(() => {  			this.enable();  		}, this.delay);  	}    }  

To call the API itself, I use object destructuring to get the action and method properties from this.form.

Then, I make a fetch() request to the API, and await the response.

I pass in the method, use a serialize() method to get the form data and assign it to the body, and define some headers. The X-Requested-With header is how my PHP backend knows it's an ajax request and not a full page reload.

try {    	// Call the API  	let {action, method} = this.form;  	let response = await fetch(action, {  		method,  		body: this.serialize(),  		headers: {  			'Content-type': 'application/x-www-form-urlencoded',  			'X-Requested-With': 'XMLHttpRequest'  		}  	});    }  

The serialize() method uses the FormData API to get the form field values, and the URLSearchParams API to create a URL encoded string (or query string) from those values.

/**   * Serialize all form data into an encoded query string   * @return {String} The serialized form data   */  serialize () {  	let data = new FormData(this.form);  	let params = new URLSearchParams();  	for (let [key, val] of data) {  		params.append(key, val);  	}  	return params.toString();  }  

Handling the API response

Once we get a response back, we can use the Response.json() method to convert it into a JSON object (this is asynchronous, so I need the await operator).

If the response.ok property is false, there was an error, so I throw data to trigger the catch() method to run. If not, I use the showStatus() method to display the data.message.

This time, I pass in true for both success and complete.

try {    	// ...    	// Get the response data  	let data = await response.json();    	// If there's an error, throw  	if (!response.ok) throw data;    	// If message, display it  	if (data.message) {  		this.showStatus(data.message, true, true);  	}    }  

The extras

Let's look at some of the extra stuff that might happen after a successful response.

If there's an updateURL property defined on the instance, I use the history.replaceState() method to update the current URL without reloading the page.

try {    	// ...    	// If updateURL, update it  	if (this.updateURL) {  		history.replaceState(history.state, null, this.updateURL);  	}    }  

The the data response has a url property, that means on success the user should be redirected to that URL.

I use the window.location.href property to do that.

try {    	// ...    	// If URL, redirect  	if (data.url) {  		window.location.href = data.url;  	}    }  

If the keepFields instance property is not true, I use the HTMLFormElement.reset() method to clear all of the fields.

I have a helper method setup for this, but I probably don't need one.

try {    	// ...    	// Clear the form  	if (!this.keepFields) {  		this.reset();  	}    }  

I emit() a custom event that other scripts and Web Components can hook into. This emits an ajax-form event on the Web Component, with the response data as an event.detail.

We'll look at how I use that tomorrow.

try {    	// ...    	// Emit custom event  	this.emit(data);    }  

And finally, if the this.removeElem property is defined, I find the matching element, and use the Element.remove() method to remove it from the DOM.

try {    	// ...    	// Optionally remove all HTML  	if (this.removeElem) {  		let elemToRemove = this.closest(this.removeElem) || this;  		elemToRemove.remove();  	}    }  

How toasts work

One thing I didn't cover is the tost() method.

This relocates to the this.announce element to a fixed element at the bottom of the page, and adds a unique class for styling the notifications differently.

It's used specifically when the element the this.announce element was in will no longer exist, but you still need to notify the user of a successful action.

In it, I look for a .toast-wrapper element. If one doesn't exist, I create it and append it to the end of the document.body.

/**   * Convert status message into a toast   */  toast () {    	// Get the toast wrapper  	let wrapper = document.querySelector('.toast-wrapper');    	// If there is no wrapper, make one  	if (!wrapper) {  		wrapper = document.createElement('div');  		wrapper.className = 'toast-wrapper';  		document.body.append(wrapper);  	}    }  

Next, I reset the this.announce.className to .toast, and use the Element.prepend() method to add it as the first element in my wrapper.

Then, I use setTimeout() to remove it from the UI after 6 seconds, since toast components are typically short lived and automatically disappear.

/**   * Convert status message into a toast   */  toast () {    	// ...    	// Add the toast class to the notification  	this.announce.className = 'toast';  	wrapper.prepend(this.announce);    	// Remove after 6 seconds  	setTimeout(() => {  		this.announce.remove();  	}, 6000);    }  

Putting it all together…

You can download the entire <ajax-form> element on GitHub. I'll also be adding it to my members toolkit.

Tomorrow, I'll show you how I use custom events and another HTML Web Component to dynamically update certain parts of the UI after a form is successfully submitted.

Like this? A Go Make Things membership is the best way to support my work and help me create more free content.

Cheers,
Chris

Want to share this with others or read it later? View it in a browser.

Share :

Facebook Twitter Google+
0 Komentar untuk "[Go Make Things] Progressively enhancing forms with an HTML Web Component (part 2)"

Back To Top