This post describes how you can post a form by using pure JavaScript (Vanilla JS). JavaScript is used to interact asynchronous with servers, you can display progress bars and loading animations while requests are sent to a server.
I have been using JQuery heavily to post ajax requests and to get ajax responses from servers. I want to reduce my dependency on JQuery and are rewriting front-end code to rely on pure JavaScript only.
XMLHttpRequest (XHR) objects is used in JavaScript to interact with servers. XMLHttpRequest can be used for all type of data, it is not just XML as the name implies.
HTTP defines several methods that describes the action that will be performed on the server. GET, HEAD, POST, PUT, DELETE, TRACE, OPTIONS, CONNECT and PATCH are methods that might be present in server methods. Many server methods can have the same uri, one that handles GET and one that handles POST for example. A POST request to a GET method will not be allowed. HTTP methods forms a contract between the called method and the caller.
This code has been tested and is working with Google Chrome (75.0.3770.100), Mozilla Firefox (67.0.4) and Microsoft Edge (42.17134.1.0), without any polyfill. It works in Internet Explorer (11.829.17134.0) with a polyfill for XMLHttpRequest. If you want to support older browsers, check out our post on transpilation and polyfilling of JavaScript.
Form
This form includes different types of input controls and a progress bar. A form with a file upload control must be sent as multipart/form-data. A button type control is used instead of a submit type to not send the form to the action url directly.
@inject ICommonServices tools
@{
// Get form values
WebDomain current_domain = ViewBag.CurrentDomain;
UserDocument user = ViewBag.User;
KeyStringList tt = ViewBag.TranslatedTexts;
// Get translated texts
string register_account_tt = tt.Get("register-account");
string edit_tt = tt.Get("edit");
string user_details_tt = tt.Get("user-details");
string email_tt = tt.Get("email");
string password_tt = tt.Get("password");
string confirm_password_tt = tt.Get("confirm-password");
string public_name_tt = tt.Get("public-name");
string image_tt = tt.Get("image");
string upload_main_image_tt = tt.Get("upload-main-image");
string save_tt = tt.Get("save");
// Set the title for the page
if (user.user_email == "" && user.facebook_user_id == "")
{
ViewBag.Title = register_account_tt;
}
else
{
ViewBag.Title = edit_tt + " " + user_details_tt.ToLower();
}
// Set meta data
ViewBag.MetaTitle = ViewBag.Title;
ViewBag.MetaDescription = ViewBag.Title;
ViewBag.MetaKeywords = ViewBag.Title;
ViewBag.MetaCanonical = current_domain.web_address + "/user/edit";
ViewBag.MetaRobots = "noindex, follow";
// Set the layout for the page
Layout = "/Views/shared_front/_standard_layout.cshtml";
}
@*Title*@
<h1>@ViewBag.Title</h1>
<div class="annytab-basic-space"></div>
@*Menu*@
@await Html.PartialAsync("/Views/user/_user_menu.cshtml")
@*Edit form*@
<form id="inputForm" action="/user/edit" method="post" enctype="multipart/form-data">
@*Hidden data*@
@Html.AntiForgeryToken()
@*General information*@
<div class="annytab-top-form-container">
<input name="txtId" type="hidden" tabindex="-1" value="@user.id" />
<div class="annytab-form-label">@email_tt</div>
<input id="txtEmail" name="txtEmail" type="text" class="annytab-form-control" value="@user.user_email" placeholder="@email_tt" data-val="true"
data-val-required="@String.Format(tt.Get("error-field-required"), email_tt)" data-val-regex="@String.Format(tt.Get("error-field-invalid"), email_tt)"
data-val-regex-pattern="^.+[@@].+[.].+$" data-val-remote="@String.Format(tt.Get("error-field-invalid"), email_tt)"
data-val-remote-additionalfields="*.txtId,*.txtEmail" data-val-remote-url="/user/verify_email" />
<div class="field-validation-valid" data-valmsg-for="txtEmail" data-valmsg-replace="true"></div>
<div class="annytab-form-label">@password_tt</div>
<input name="txtPassword" type="password" class="annytab-form-control" value="" placeholder="@password_tt" />
<input name="txtConfirmPassword" type="password" class="annytab-form-control" placeholder="@confirm_password_tt" data-val="true"
data-val-equalto="@String.Format(tt.Get("error-field-confirm"), password_tt)" data-val-equalto-other="txtPassword" />
<div class="field-validation-valid" data-valmsg-for="txtConfirmPassword" data-valmsg-replace="true"></div>
<div class="annytab-form-label">@public_name_tt</div>
<input name="txtPublicName" type="text" class="annytab-form-control" value="@user.public_name" placeholder="@public_name_tt" />
</div>
<div class="annytab-basic-space"></div>
@*User image*@
<div class="annytab-top-form-container">
<div class="annytab-form-label">@(String.Format(upload_main_image_tt, "256 kb", "[jpg|jpeg|png|gif]"))</div>
<input name="uploadMediaFile" type="file" class="annytab-form-control annytab-form-upload" data-container-selector="#img0" data-val="true" data-val-file="@String.Format(tt.Get("error-upload-file"), "jpg|jpeg|png|gif", "262144")"
data-val-file-maxsize="262144" data-val-file-extensions="jpg|jpeg|png|gif" />
<div class="field-validation-valid" data-valmsg-for="uploadMediaFile" data-valmsg-replace="true"></div>
<div class="annytab-basic-space"></div>
<div id="img0">
@if (string.IsNullOrEmpty(user.image_url) == true)
{
<i class="fas fas fa-user-secret fa-6x fa-fw annytab-green-color"></i>
}
else
{
<img src="@(Tools.GetBlobStorageUrl() + "users/" + user.image_url)" alt="@image_tt" />
}
</div>
</div>
<div class="annytab-basic-space"></div>
@*Progress bar*@
<div style="display: block;position:relative;background-color: #6d6c6c;width: 100%;height: 40px;text-align: center;font-size: 16px;line-height: 40px;color: #ffffff;padding: 0; margin: 0;">
<div id="progress-bar" style="position:absolute;background-color: #4CAF50;width: 0;height: 40px;">
<span id="loading-text" style="position:center;">0%</span>
</div>
</div>
<div data-valmsg-summary="true">
<ul></ul>
</div>
@*Button panel*@
<div class="annytab-basic-button-container">
<input type="button" class="annytab-basic-button" value="@save_tt" onclick="sendForm()" />
<input type="button" class="annytab-basic-button" value="GetCustomInformation" onclick="getCustomInformation()" />
</div>
<div class="annytab-basic-space"></div>
</form>
Controller Method
This is the server method that should handle the post request, it takes a collection of form key-value-pairs as input.
// Update user details
// POST: /user/edit
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> edit(IFormCollection collection)
{
// Get the signed in user
Claim claim = ControllerContext.HttpContext.User.FindFirst("user");
UserDocument user = claim != null ? JsonConvert.DeserializeObject<UserDocument>(claim.Value) : null;
// Get the current domain
WebDomain current_domain = await this.web_domain_repository.GetCurrentDomain(ControllerContext.HttpContext);
// Get translated texts
KeyStringList tt = await this.static_text_repository.GetFromCache(current_domain.front_end_language_code);
// Create a new post if the user is null
if(user == null)
{
return Json(data: new ResponseData(false, "", String.Format(tt.Get("error-update-post"), tt.Get("user"))));
}
// Update values
user.user_email = collection["txtEmail"].ToString().StripHtml();
user.user_password = collection["txtPassword"].ToString() != "" ? PasswordHash.CreateHash(collection["txtPassword"].ToString()) : user.user_password;
user.public_name = collection["txtPublicName"].ToString().StripHtml();
// Make sure that the email is unique
ModelItem<UserDocument> user_on_email_model = await this.user_repository.GetByEmail(user.user_email);
if (user_on_email_model.item != null && user.id != user_on_email_model.item.id)
{
return Json(data: String.Format(tt.Get("error-field-unique"), tt.Get("email")));
}
// Get uploaded files
IFormFileCollection files = collection.Files;
// Loop all the images and save them
for (int i = 0; i < files.Count; i++)
{
// Just continue if the file is empty
if (files[i].Length == 0)
continue;
// Set the filename
string filename = user.id + Path.GetExtension(files[i].FileName);
// Delete the old image
if (user.image_url != "")
{
await this.blob_storage_repository.Delete("users", user.image_url);
}
// Add the blob
await blob_storage_repository.UploadFromStream("users", filename, files[i].OpenReadStream());
// Set the source
user.image_url = filename;
}
// Add or update the post
await this.user_repository.Upsert(user);
// Return a success response
return Json(data: new ResponseData(true, "", String.Format(tt.Get("success-post-updated"), tt.Get("user-details"))));
} // End of the edit method
Post with JavaScript
This is the JavaScript method used to post a html form to the server method. The entire form is added to the FormData object.
function sendForm()
{
// Get variables
var form = document.getElementById('inputForm');
var progress_bar = document.getElementById('progress-bar');
var loading_text = document.getElementById('loading-text');
// Get form data
var form_data = new FormData(form);
// Post form data
var xhr = new XMLHttpRequest();
xhr.open('POST', form.getAttribute('action') , true);
xhr.onload = function ()
{
if (xhr.status === 200)
{
// Get the response
var data = JSON.parse(xhr.response);
// Check the success status
if (data.success === true)
{
// Output a success message
toastr['success'](data.message);
}
else
{
// Output error information
toastr['error'](data.message);
}
}
else
{
// Output error information
toastr['error'](xhr.status + " - " + xhr.statusText);
}
};
xhr.upload.addEventListener("progress", function (evt)
{
if (evt.lengthComputable)
{
var width = Math.round((evt.loaded / evt.total) * 100);
progress_bar.style.width = width + '%';
loading_text.innerHTML = width * 1 + '%';
}
}, false);
xhr.onerror = function ()
{
// Output error information
toastr['error'](xhr.status + " - " + xhr.statusText);
};
xhr.send(form_data);
} // End of the sendForm method
The response from the server is a response data object that looks like this.
public class ResponseData
{
#region variables
public bool success { get; set; }
public string id { get; set; }
public string message { get; set; }
public string url { get; set; }
#endregion
#region Constructors
public ResponseData(bool success, string id, string message, string url)
{
// Set values for instance variables
this.success = success;
this.id = id;
this.message = message;
this.url = url;
} // End of the constructor
#endregion
} // End of the class
Example, append form data.
var fd = new FormData();
fd.append('__RequestVerificationToken', document.getElementsByName('__RequestVerificationToken')[0].value);
fd.append('id', '123');
fd.append('filename', 'text.json');
fd.append('files[]', document.getElementById('uploadFileControl').files[0]);
Example, get data.
function getCustomInformation() {
var id = 'bbfe3671-e3f1-4c36-9a2b-1dae99bbfcae';
var lang = 'sv';
var xhr = new XMLHttpRequest();
xhr.open('GET', '/home/get_custom_information/' + id + "?lang=" + lang, true);
xhr.onload = function ()
{
if (xhr.status === 200)
{
// Get the response
var data = JSON.parse(xhr.response);
// Output a success message
toastr['success']("<b>" + data.title + "</b><br>" + data.description);
}
else
{
// Output error information
toastr['error'](xhr.status + " - " + xhr.statusText);
}
};
xhr.onerror = function ()
{
// Output error information
toastr['error'](xhr.status + " - " + xhr.statusText);
};
xhr.send();
} // End of the getCustomInformation method