Form Event Handling Pattern - javascript

I am using a backend where it is ideal that I send an ajax post request rather than using the default action on forms.
With this in mind, I need to extract the final fields that are selected in my form.
I have various text fields, radio buttons, checkboxes, etc.
I've always struggled gaining a good understanding of event delegation and event propagation. I'm not entirely sure if this is the topic I should be worried about with what I am trying to achieve.
I know I can write code that grabs all of the information in my form by placing an ID on each field and a have a function extract each value on the ID such as:
function example(){
var field0 = $('#field0').val();
var field1 = $('#field1').parent().hasClass('active')
// ... and more
}
I've used this pattern for a while and I don't feel like it is efficient.
I have two pattern idea, but I am still not sure if this is a "common practice"
Since I am not concerned about the data in each field until the form is submitted, I could run a loop on all of my input based fields on my form and extract the contents, instead of assigning an ID to each individual input field.
I can listen to changes on the form (I am not exactly sure how to do this, this is where event delegation/propagation will come into play). Instead of waiting for the submit button to gather all the info in the form, I will have some type of listener that detects a change on the form (not sure if that is possible).
I've been using my current pattern for several months and would like to improve myself, If anyone has any suggestions, links, or criticism about my thoughts on a new approach I'd appreciate it.

So, you basically propose 3 ways to get all form fields with a value on submit (or a similar event):
hard-code IDs and retrieve their values, e.g.
var field_a = document.getElementById('a')
, field_b = document.getElementById('b')
, form = document.getElementById('my_form');
form.addEventListener('submit', function() {
fetch('//your/api/endpoint', {
method: 'POST',
body: JSON.stringify({a: field_a.value, b: field_b.value})
});
});
loop all and retrieve their values, e.g.
var form = document.getElementById('my_form');
form.addEventListener('submit', function() {
var values = [].reduce.call(
form.querySelectorAll('input, textarea, select'),
function(values, element) {
values[element.name] = element.value;
return values;
},
{}
);
fetch('//your/api/endpoint', {
method: 'POST',
body: JSON.stringify(values)
});
});
watch for changes inside the form, accumulate them
var form = document.getElementById('my_form')
, state = {};
form.addEventListener('change', function(e) {
state[e.srcElement.name] = e.value;
});
form.addEventListener('submit', function() {
fetch('//your/api/endpoint', {
method: 'POST',
body: JSON.stringify(state)
});
});
From a performance perspective, option 1. will be the fastest, followed by 2 followed by 3 (with the last 2 I'm not 100% certain, querySelectorAll can be expensive, but listening for tons of change events might be as well -- depends on how often change events are triggered I'd say).
From development perspective (how long does it take to set up a form), 2 and 3 should not be that different as they are both generic (and you can use my code sample as a start).
"Real" data-binding (like Angular) or "pure state" (like React) pretty much come down to options 2/3 as well (just that the framework will perform the heavy lifting for you).
Regarding option 3 (listening for a change on the whole form): https://stackoverflow.com/a/4616720/1168892 explains quite well how event bubbling in JavaScript happens. To use that you have to make sure that no element inside the form cancels the change event (otherwise it would not bubble to the form itself). To not cancel events is the default behavior, so you would have to explicitly make this wrong (and with that you can just have an eye on it in your implementation).
I didn't use jQuery in my examples as that can all be done by browsers directly now. What I used are Element.querySelectorAll, Array.reduce and window.fetch.

Pattern #1 (use serializeArray)
$('#formId').on('submit', function(e){
var allData;
e.preventDefault();
allData = $(this).serializeArray();
// use the allData variable when sending the ajax request
});
Pattern #2 (use the delegated form of $container.on('event', 'selector', ..) and the change event)
$('#formId').on('change', 'input,textarea,select', function(){
var element = $(this), // element that changed
value = element.val(); // its new value
// do what you want ..
});

Without jquery I once wrote a function that return in an object all input value tie with its name.
I think it's better than plain id link, because you don't have to worry about what's inside your form, as long as your giving a name attribute to your inputs.
function getFormData(form) {
var data = {};
for (var i = 0; i < form.elements.length; i++) {
var input = form.elements[i];
if (input.value && input.type !== 'submit' && input.type !== 'button') {
data[input.name] = input.value;
}
}
return data;
}
All you need to do is passing your form like this:
var form = document.querySelector('.monFormulaire');
// your form data
var data = getFormData(form);

Related

Save and restore check status of multiple checkboxes in a Chrome extension

I can save and restore a user input in a text field, based on the ID of the field - the code works as i expect, like:
function save_options() {
var api = document.getElementById('text_field').value;
chrome.storage.sync.set({
savedApi: api,
});
}
document.getElementById("save").addEventListener("click", save_options);
function restore_options() {
chrome.storage.sync.get({"savedApi": ''}, function(items) {
document.getElementById('text_field').value = items.savedApi;
});
}
document.addEventListener('DOMContentLoaded', restore_options);
Now i want to save the check status of multiple checkboxes. Should i really repeat this construction for every checkbox? Could somebody point me to a smarter way to save status of multiple checkboxes?
IF this inputs are in side a form you can use this method to get all form data
First way (It is 2019 and there's a better way to do this):
const form = document.querySelector('form'):
const data = new URLSearchParams(new FormData(form).entries());
Second way (or if you want a plain Object instead):
const form = document.querySelector('form');
const data = Object.fromEntries(new FormData(form).entries());

How to show message to user if they are trying to leave the page or close the window only if form data has been changed?

I have requirement in one of my projects where users would like to see a message before they leave the page or close the browser/window if the form data has been changed. I found this function that will catch the scenario if user is leaving the window or closing the browser.
window.addEventListener('beforeunload', function (e) {
e.preventDefault();
e.returnValue = '';
});
I'm wondering if there is a way to show that message only if form data has been changed. SO user in that case would know that data has changed. I did not find a lot about this topic or any constructive solutions so far. If anyone have any examples or suggestions please let me know.
Thank you.
If you're not using any modern framework you can build your custom Form State helper.
When form is ready on DOM you can iterate over each input element and assign a data property say (data-init-val) and set its value equal to the input element value.
let inputs = document.querySelectorAll('form#userform input');
for(let inputEl of inputs){
inputEl.setAttribute('oldVal', inputEl.value);
}
This way you're caching the existing form values. When user is closing the window you can run a function to compare current input values with the initial cached values via:
function checkFormState(){
let inputs = document.querySelectorAll('form#userform input');
let stateChanged = false;
for(let inputEl of inputs){
if(inputEl.getAttribute('oldVal') !== inputEl.value){
stateChanged = true;
break;
}
}
return stateChanged;
}
Your eventListener can make an informed decision based on the stateChanged value.
PS: You should modify the input element value caching/compare logic based on the input elements in your form.
Headover to https://jsfiddle.net/v9rztay6/ and see it in action.

Getting all values being sent to the server at once

I have a form, bunch of inputs and submit button. Some of the inputs are added and deleted dynamically. And some of them may be (or may not) disabled but I still need to get their values of the server.
So I have to add a handler to the button and collect the values of the disabled inputs manually and add them up to other, enabled. inputs. But I'd like not to go that way as it seems too complex (since some of the inputs are added / removed dynamically).
I can also add hidden inputs for each inputs which can be disabled but still it's a bit complex.
What I want is similar to 1. But instead of manually enumerating the enabled inputs by jquery selectors, I want to just get all values are being sent to the server at once by some jquery function maybe or by any other way. And then manually (by selectors) add up the values from the disabled inputs to them. Is this possible?
You could use the jQuery .serialize() for that.
The general structure of the function would look something like this
var mydata = $("#myform").serialize();
$.ajax({
type: "POST",
url: "ajaxurl",
data: mydata,
dataType: "json",
success: function(data) {
...
}
});
So I just figured you had a problem of how to get the disabled fields as well. You could use the function suggested in this thread:
Disabled fields not picked up by serializeArray
Just to make the answer complete, I will copy the plugin from there.
The usage is like this:
var data = $('form').serializeAllArray();
And here's the plugin itself:
(function ($) {
$.fn.serializeAllArray = function () {
var obj = {};
$('input',this).each(function () {
obj[this.name] = $(this).val();
});
return $.param(obj);
}
})(jQuery);
You could use a strategy like the following in your submit handler:
Select all disabled elements .. (don't lose the reference to these)
Enable all disabled elements
Serialize the form .. to get all the data
Disable the elements back
Submit your form.

Submitting form/getting HTML with JavaScript without iframe?

Context:
I work a student job transcribing paper reports in a webapp. It's old and we unfortunately can't change the source nor directly run a DB query.
It only checks if the unique ID exists once you submit the entire form, and you can't submit it unless it's entirely filled. Needless to say, it's a huge waste of time as you often transcribe the whole thing only to realise it's a duplicate.
Objective:
I made the userscript below that launches a search the search on the onblur of the unique ID's input(noReferenceDeclarant), checks if there are any matches (rows) and returns accordingly. Runs with Greasemonkey. The search form is in another page on the same domain. The search form does not take any URL arguments.
Can this be done without using an iframe (AJAX perhaps?)
This is a tool for my own productivity & to learn JS at the same time. As I'm still very much a beginner, any tips to make that code cleaner are welcome.
//Adding function to input's blur event
$(document).on ("blur", "#noReferenceDeclarant", isRefNumberExists);
//Vars
var noReferenceDeclarant = '';
var loadCode = 0;
var $searchForm;
//Fonctions
function isRefNumberExists ()
{
noReferenceDeclarant = $('#noReferenceDeclarant').val();
loadCode = 0;
//Make sure there's data in the input before proceeding
if (noReferenceDeclarant)
{
//Build search iframe
$searchForm = $('<iframe />', {
name: 'searchWindow',
src: 'rechercherGriIntranet.do?methode=presenterRechercher',
id: 'searchWindow',
width: 0,
height: 0
}).appendTo('body');
$searchForm.load(searchRefNumber);
}
}
function searchRefNumber()
{
var isExists = false;
//Check which "load" it is to avoid submit loops
if (loadCode === 0)
{
loadCode = 1;
//Filling search form with search term
$(this.contentDocument).find('#noReference').val(noReferenceDeclarant);
//Set search form preferences
$(this.contentDocument).find('#typeRapportAss').prop('checked', false);
$(this.contentDocument).find('#typeRapportAS').prop('checked', false);
$(this.contentDocument).find('#typeRapportSI').prop('checked', true);
//Submit the form
$(this.contentDocument).find('form:first').submit();
}
else if (loadCode === 1)
{
loadCode = 2;
//See if there are any tr in the result table. If there are no results, there a thead but no tr.
var foundReports = $(this.contentDocument).find('.resultatRecherche tr').length;
if (foundReports > 0)
{
if (confirm('A report matching this ID already exists. Do you want to display it?'))
{
//Modal window loading the report in an iframe. Not done yet but that's fairly straightforward.
}
else
{
//Close and return to the form.
}
}
}
//Reset variables/clean ressources
delete $searchForm;
$('#dateRedactionRapport').focus();
}
On the whole I've seen far, far worse code.
Ajax could do it, but then you'd just have to put the AJAX response into the DOM (as an iframe, most likely).
In this instance, I'd keep the approach you have. I think it is the sanest.j
Without the full context, there may be a way to clean up the loadCode -- but what you have is pretty same and works. A lot of folks would call it a semaphore, but that is just an issue of terminology.
The only thing I"d really clean up is recommend not calling the jQuery object so often..
// Many folks recommend that jQuery variables be named $<something>
var $doc = $(this.contentDocument);
doc.find('#typeRapportAss').prop('checked', false);
$doc.find('#typeRapportAS').prop('checked', false);
$doc.find('#typeRapportSI').prop('checked', true);
If you wanted to play with jQuery data structures, you could make a 'config' object that looks like this:
var formValues = {
typeRapportAs: false,
typeRapportAS: false,
typeRapportSI: true
};
then iterate over that to (using for ... in with .hasOwnProperty).
Not NEEDED for this project, what you are doing is fine, but it might make a learning exercise.

Trying to clean out form inputs with jQuery so I can add it back into the form

I have a pretty simple HTML form where users can enter in information about a person. Below that form is a button which allows them to 'add more'. When clicked, the 'person' form is copied and appended to the page.
The way I used to do this was to take my HTML file, copy out the relevant section (the part that gets 'added more') and then save it into a variable in the Javascript. This became rather annoying when I had to make changes to the form as I would then have to make the same changes to the Javascript variable.
My new method is to create the variable dynamically in Javascript. When the page loads, I use jQuery to grab out the 'add more' part of the code and cache the HTML into a variable. Then when the 'add more' button is clicked, I append that cached HTML to the page.
The problem is with form inputs. The server-side code autofills the form with the user's data from the database. I want to cache that HTML data with no form inputs...
My current function looks like this:
function getHTML($obj, clean)
{
if (clean)
{
var $html = $obj.clone();
$html.find('input').each(function() { $(this)[0].value = ''; });
}
else
{
var $html = $obj;
}
var html = $html.wrap('<div></div>').parent()[0].innerHTML;
$html.unwrap();
return html;
}
It doesn't work. I'm also unsure if this is the best approach to solving the problem.
Any ideas?
I don't know why this wouldn't work. I can't see how the function is being called, or what is being passed to it.
I guess one thing I'd do differently would be to create a .clone() whether or not you're "cleaning" the inputs. Then you're not wrapping and unwrapping an element that is in the DOM. Just use the if() statement to decide whether or not to clean it.
Something like this:
function getHTML($obj, clean) {
var $clone = $obj.clone();
if (clean) {
$clone.find('input').each(function() { this.value = ''; });
}
return $clone.wrap('<div></div>').parent()[0].innerHTML;
}
Or a little more jQuery and less code:
function getHTML($obj) {
return $obj.clone().find('input').val('').end().wrap('<div/>').parent().html();
}
A little less efficient, but if it only runs once at the page load, then perhaps not a concern.
Or if it is going to be made into a jQuery object eventually anyway, why not just return that?
function getHTML($obj) {
return $obj.clone().find('input').val('').end();
}
Now you've returned a cleaned clone of the original that is ready to be inserted whenever you want.
EDIT:
Can't figure out right now why we can't get a new string.
Here's a function that will return the DOM elements. Beyond that, I'm stumped!
function getHTML($obj, clean) {
var $clone = $obj.clone();
if (clean) {
$clone.find('input').each(function() {
this.value = '';
});
}
return $clone.get(); // Return Array of DOM Elements
}
EDIT: Works now.
I ditched most of the jQuery, and used .setAttribute("value","") instead of this.value.
Give it a try:
function getHTML($obj, clean) {
var clone = $obj[0].cloneNode(true);
var inputs = clone.getElementsByTagName('input');
console.log(inputs);
for(var i = 0, len = inputs.length; i < len; i++) {
inputs[i].setAttribute('value','');
}
return $('<div></div>').append(clone)[0].innerHTML;
}
I would wrap the part of the form that needs to be cloned in a <fieldset>:
<form id="my_form">
<fieldset id="clone_1">
<input name="field_1_1">
<input name="field_2_1">
<input name="field_3_1">
</fieldset>
</form>
Add one more
Then for the jQuery script:
$("#fieldset_clone").click(function(event) {
// Get the number of current clones and set the new count ...
var cloneCount = parseInt($("fieldset[id^=clone_]").size());
var newCloneCount = cloneCount++;
// ... then create new clone based on the first fieldset ...
var newClone = $("#clone_1").clone();
// .. and do the cleanup, make sure it has
// unique IDs and name for server-side parsing
newClone.attr('id', 'clone_' + newCloneCount);
newClone.find("input[id^=clone_]").each(function() {
$(this).val('').attr('name', ($(this).attr('name').substr(0,7)) + newCloneCount);
});
// .. and finally insert it after the last fieldset
newClone.insertAfter("#clone_" + cloneCount);
event.preventDefault();
});
This would not only clone and clean the set of input fields, but it would also set new ID's and names so once the form is posted, their values would not be overwritten by the last set.
Also, in case you want to add the option of removing sets as well (one might add too many by mistake, or whatever other reason), having them wrapped in a <fieldset> that has an unique ID will help in accessing it and doing a .remove() on it.
Hope this helps.

Categories