Post form with pure JavaScript (XMLHttpRequest)

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.

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

Leave a Reply

Your email address will not be published. Required fields are marked *