How I Developed JG.js, my Powerful JS Library
JG.js is a small utility JavaScript library I wrote and maintain to speed up my frontend development workflow and to learn more about the inner workings of JavaScript and the DOM. It is designed for websites that use only vanilla JavaScript, and it automates many of the most common frontend features I have repeatedly developed during my web development experience. These include modals, loaders, ajax-submitted forms, stripe inputs, and more. Let’s dive in!
Modals
One feature I use all the time in my projects is modals. A modal is a dialog box that pops up over the top of the main content on a webpage to display new information and inputs when a certain action is performed. Traditionally, modals have been a bit tricky to implement, involving using absolute positioning and special CSS classes to position and reveal properly.
In my module, I use a more modern approach to modal creation that involves the dialog tag and its associated showModal function. By making use of a custom html attribute, I am able to easily dictate that a button should open a modal upon being clicked:
<button jg_open="modal_1">Show Modal</button>
<dialog class="jg_modal" id="modal_1" style="padding: 1rem;">
<h1>Example Modal</h1>
</dialog>
All I have to do is create an html button, and set its jg_open attribute equal to the id of the dialog with class jg_modal I want to open, and the button will be linked to the modal, opening it whenever the button is clicked.
So, we can open the modal, but how do we close it? My module takes care of that too. It automatically adds a close button in the top right-hand corner of the modal. The content of that button defaults to an X, but it can be modified by simply defining a template with its id set to jg_close_btn_content on the document.
<template id="jg_close_btn_content">
<i class="fa-solid fa-sm fa-x"></i>
</template>
Loaders
Next up is the loader script I made. I often like to append a loader element to the body of a container while an asynchronous task is running. Here is an example of how the it works:
<button onclick="async_task(event)">Show Modal</button>
<div id="loader_container"></div>
<script>
async function async_task(event){
let container = document.getElementById('loader_container')
let loader = jg_get_loader()
container.innerHTML = ""
container.appendChild(loader)
//execute some async task here...
container.innerHTML = ""
//do something with the result
}
</script>
Simply call the function jg_get_loader to get the element, and insert it into another element, and you have a spinning loader. By default, a simple css loader is used, but much like the dialog close button, the developer can customize the loader content by defining a template on the document.
<template id="jg_loader_content">
<i class="fa-solid fa-xl fa-gear fa-spin"></i>
</template>
This template will use a spinning Font Awesome icon as the loader in place of the css loader.
Honeypots
Honeypots are a type of input that is used to help catch spam submissions of forms made by bots and web crawlers. To implement honeypots, one must create text inputs with persuasive name attributes, like “email” and “name”, and put them in the form, but style them in such a way that a user would never fill them out. This can be achieved using css styling. This way, users, who look at the rendered DOM, will not be able to see the inputs, while bots, which simply parse the raw html, will spot the inputs. Then, when the form is submitted, the server can look to see if the honeypot inputs were filled out, and if they were, it can react under the assumption that a bot submitted the form, like refraining from sending an email from a submitted contact form.
In my implementation, any form tagged with the class jg_honeypot_form will have honeypot inputs automatically generated with it. The script looks into all of these forms, and pulls out any text or email type inputs. Then, it masks each of them by changing their name attribute, creates a copy of each of those inputs, tags them with jg_honeypot class, and appends them to the form body. Take a look at how I apply honeypots to a form below.
<form action="/" method="POST" class="jg_honeypot_form">
<label for="name_input">Name</label>
<input type="text" id="name_input" name="name">
<label for="email_input">Email</label>
<input type="email" id="email_input" name="email">
<label for="message_input">Message</label>
<input type="text" id="message_input" name="message">
<input type="submit" value="submit">
</form>
As you can see, all I have to do is apply the jg_honeypot_form class to the desired form. Behind the scenes, when the form is submitted, my script checks to see if any of the honeypot inputs have values set. If any were filled out, it sets a value on the request, jg_honeypot_suspects_bot, to true. The server can then use this attribute to determine if it should process the form.
Ajax-Submitted Forms
In modern web apps, it is almost always preferable to submit forms without reloading the page. If you have ever implemented anything using ajax requests, you know that it takes many lines of boilerplate code to submit an ajax request. Additionally, it can take quite a bit of work to migrate a traditionally-submitted form to ajax. That is where my ajax forms module comes in:
<form action="/" jg_ajax_method="GET">
<label for="name_input">Name</label>
<input type="text" id="name_input" name="name">
<label for="email_input">Email</label>
<input type="email" id="email_input" name="email">
<label for="message_input">Message</label>
<input type="text" id="message_input" name="message">
<input type="submit" value="submit">
</form>
A form can be submitted using an ajax GET request by setting the jg_ajax_method attribute to GET. The method for using an ajax POST request is identical:
<form action="/" jg_ajax_method="POST">
<!-- form fields here -->
<input type="submit" value="submit">
</form>
Oftentimes, the request will return some data that you want to use somewhere on the webpage. My module provides an interface for that, too. Set the jg_ajax_response_handler attribute of the form equal to the name of a function you have defined in a script tag. This function will be called after the server responds with data from the server. Your handler function should take two parameters: event, the submit event from when the form was submitted, and response, the data returned by the server as a response to the submission.
<form action="/" method="jg_ajax_get" jg_ajax_response_handler="handler">
<!-- form fields here -->
<input type="submit" value="submit">
</form>
<script>
function handler(event, response){
console.log(response)
//perform some task here
}
</script>
Stripe Inputs
Stripe is one of the largest online payment processers, used by thousands of merchants for managing online purchases and subscriptions. To facilitate this, developers need to integrate the secure Stripe input on their billing form. The only problem? It takes a ton of lines of code to get this input to appear on your page.
That is why I decided to automate the creation of these inputs in my module. To begin, there are two prerequisites:
<script>
window['jg_stripe_public_key'] = "pk_STRIPE_PUBLIC_KEY_HERE..."
</script>
<script src="https://js.stripe.com/v3/"></script>
Firstly, your Stripe public key needs to be defined on the window, on the jg_stripe_public_key key. This will allow your app to manage billing with you Stripe account. Secondly, Stripe’s official script for creating billing inputs needs to be included on the page. Then, we can move on to actually creating the billing form:
<form action="/" method="POST" id="jg_checkout_form">
<!-- form fields here -->
<input type="text" id="jg_card_holder_name_input">
<div id="jg_stripe_card_container"></div>
<input type="submit" value="submit">
</form>
My script finds forms that should contain a Stripe Charge input using the jg_checkout_form id. Then, it searches within them for an element with the id jg_stripe_card_container. A text input with id set to jg_card_holder_name_input should also be defined on the form. Note that this means that only one payment form per page is supported. With that, the input will automatically be rendered when the page loads by Stripe.
Problems Emerge
As I developed the form-related features of JG.js, I began to notice some rather strange issues going on under the hood. Honeypots, ajax forms, and Stripe inputs were not compatible with each other. After some digging, I discovered that all these issues sprouted from the way my event listeners worked.
form.addEventListener('submit', (event) => {
console.log('Form submitted')
})
Above is an example of an event listener that is called when a form is submitted. “form” is a reference to an element in the DOM, in this case a <form></form> tag, gotten using the document.getElementById function. Then, two arguments are passed to the addEventListener function. First, we pass a handle to the event we would like to connect to, in this case, the “submit” event of the form, fired whenever the form is submitted. Secondly, the callback, or code to be run when the event occurs, is defined. It is this callback that was causing the trouble.
It turns out, the order the event listeners are registered in determines the order in which their callbacks will be fired. So, if the event listener I registered to submit the form in my honeypot module was called before the one in the ajax form module, because the callback of the honeypot event listener submits the form manually at the end, the ajax form would never get the opportunity to run, because the page would refresh. Alternatively, if the ajax form event listener was registered before the Stripe input event listener, there was the potential for the user to be double-charged, once for the ajax request callback, and once for the regular submission at the end of the Stripe callback.
Add to that the issue that the honeypot form needed to be reset back to its original state after the form was submitted by my ajax forms module (since the page does not refresh), and there was a really sticky problem where different users of the module would have different strange, obscure bugs pop up on their site based on the order they included scripts from my module in, and which features they applied to their forms. To solve this problem, I needed a way to guarantee the orders that the event listeners would be applied in, while also allowing them to communicate with each other to coordinate which event handler would submit the form. That is where my initialization script, JG.js, comes in.
JG.js Initialization Script
In order to ensure event listeners are registered in the proper order, I wrote the JG.js script. It must be included on the page, along with other feature scripts from my module. Then, it will detect the other scripts I developed that were included using the keys each of them set on the window object upon initialization, and call their initialization function. Crucially, I made sure to call the initialization function for honeypots first, then stripe inputs, then ajax forms, as you can see below.
/*
This script handles the initialization of all other scripts, since some scripts need to be loaded before others
for event propagation purposes.
*/
window.jg_js = {} //initialize jg_js object
window.JG_MODAL_KEY = 'jg_modal'
window.JG_LOADER_KEY = 'jg_loader'
window.JG_HONEYPOT_KEY = 'jg_honeypot'
window.JG_AJAX_FORM_KEY = 'jg_ajax_form'
window.JG_STRIPE_CHARGE_INPUT_KEY = 'jg_stripe_charge_input'
window.JG_ALERTS_KEY = 'jg_alerts'
document.addEventListener('DOMContentLoaded', (e)=>{
/*
Note: in order to make jg_honeypot.js, jg_stripe_charge_input.js, and jg_ajax_form.js work, they are loaded in the order specified below.
This is because the event listeners in each script need to be added with jg_honeypot.js first, then jg_stripe_charge_input.js, then jg_ajax_form.js
since they all involve form submission and the event listeners are added in the order the scripts are loaded.
*/
//init jg_honeypot
if (window.jg_js[JG_HONEYPOT_KEY]){
try{
__init_jg_honeypot(e)
} catch (error) {
console.error("Error initializing JG Honeypots:", error);
}
}
//init jg_stripe_charge_input
if (window.jg_js[JG_STRIPE_CHARGE_INPUT_KEY]){
try{
__init_jg_stripe_charge_input(e)
} catch (error) {
console.error("Error initializing JG Stripe Charge Inputs:", error);
}
}
//init jg_ajax_form
if (window.jg_js[JG_AJAX_FORM_KEY]){
try{
__init_jg_ajax_form(e)
} catch (error) {
console.error("Error initializing JG Ajax Forms:", error);
}
}
//init jg_modal
if (window.jg_js[JG_MODAL_KEY]){
try {
__init_jg_modal(e);
} catch (error) {
console.error("Error initializing JG Modals:", error);
}
}
//init jg_laoder
if (window.jg_js[JG_LOADER_KEY]){
try{
__init_jg_loader(e)
} catch (error) {
console.error("Error initializing JG Loaders:", error);
}
}
//init jg_alerts
if (window.jg_js[JG_ALERTS_KEY]){
try{
__init_jg_alerts(e)
} catch (error) {
console.error("Error initializing JG Alerts:", error);
}
}
})
This results in honeypot event listeners firing first, then stripe inputs, then ajax forms. The second portion of this problem I needed to solve was how to communicate between the modules about which module should submit the form.
To accomplish this, I leveraged the capture and bubble phase of the events. The capture phase runs first, then the bubbling phase follows. So, each of my modules uses the capture phase to decide which module will submit the form:
form.addEventListener('submit', (event)=>{
event.jg_form_submit = JG_HONEYPOT_SUBMIT_KEY
}, true)
The final true argument makes the event listener fire in the capture phase. In that event, I tag the event with a new attribute, jg_form_submit, set equal to a key that identifies the module that will submit the form. That key is checked at the end of the other event listener that is set to run in the bubble phase, and if it matches the key in the current module, that module submits the form.
//submit the form
if (!event.jg_submitted && event.jg_form_submit == JG_HONEYPOT_SUBMIT_KEY){
console.log("honeypot form submit")
event.jg_submitted = true
event.target.submit()
}
Because the event listeners are registered in order, if only honeypots exist, then the honeypots script will submit the form, but if Stripe inputs are also included, then only Stripe inputs will submit the form, and if ajax forms are enabled, then it will override the first two and only the ajax forms script will submit the form. When a form is submitted, jg_submitted is also set on the event to ensure a later script does not submit the form.
Lessons Learned
JG.js has been a great project that has given me the opportunity to take a deep dive into the DOM and how JavaScript can be used to interact with it. Particularly useful has been the new information about the capture and bubble phases in event listening, and that the order listeners are registered in makes an important difference. Additionally, JG.js is a useful module that I will be using to great extents to speed up my web development workflow in upcoming projects. Keep an eye on the project, monitor its development, and use it in your projects by cloning the github repo!
Leave a Reply