Writting a JS library to create a HTML form

Posted on

Problem

I’m currently writting a JS Library in order to create a form in HTML.
This library need to be :

  • Cross-browser, with compatibility with at least IE10 (earlier version can be considered as bonus)
  • Multi language : you should be able to change the form language
  • Able to load external javascript dependencies*
  • Communicate with my API
  • Fast
  • As simple to use as possible

So, here is what I’ve done for now :

window.MyBasicLibrary = (function() 
{
'use strict';

    // Utilitarian function not to loose the scope 
    function bind(context, name){
        return function(){
            return context[name].apply(context, arguments);
        };
    }

    // Utilitarian function to get DOM by class
    function getElementsByClassName(node, classname) 
    {
        var a = [];
        var re = new RegExp('(^| )'+ classname +'( |$)');
        var els = node.getElementsByTagName("*");

        for (var i = 0, j = els.length; i < j; i++)
        {
            if (re.test(els[i].className))
                a.push(els[i]);
        }
        return (a);
    }

    /*
    ** Utilitarian function to serialize a form
    ** See : https://code.google.com/archive/p/form-serialize/
    */
    function serialize(form) 
    {
        if (!form || form.nodeName !== "FORM") {
            return;
        }
        var i, j, q = [];
        for (i = form.elements.length - 1; i >= 0; i = i - 1) {
            if (form.elements[i].name === "") {
                continue;
            }
            switch (form.elements[i].nodeName) {
            case 'INPUT':
                switch (form.elements[i].type) {
                case 'text':
                case 'hidden':
                case 'password':
                case 'button':
                case 'reset':
                case 'submit':
                    q.push(form.elements[i].name + "=" + encodeURIComponent(form.elements[i].value));
                    break;
                case 'checkbox':
                case 'radio':
                    if (form.elements[i].checked) {
                        q.push(form.elements[i].name + "=" + encodeURIComponent(form.elements[i].value));
                    }                       
                    break;
                case 'file':
                    break;
                }
                break;           
            case 'TEXTAREA':
                q.push(form.elements[i].name + "=" + encodeURIComponent(form.elements[i].value));
                break;
            case 'SELECT':
                switch (form.elements[i].type) {
                case 'select-one':
                    q.push(form.elements[i].name + "=" + encodeURIComponent(form.elements[i].value));
                    break;
                case 'select-multiple':
                    for (j = form.elements[i].options.length - 1; j >= 0; j = j - 1) {
                        if (form.elements[i].options[j].selected) {
                            q.push(form.elements[i].name + "=" + encodeURIComponent(form.elements[i].options[j].value));
                        }
                    }
                    break;
                }
                break;
            case 'BUTTON':
                switch (form.elements[i].type) {
                case 'reset':
                case 'submit':
                case 'button':
                    q.push(form.elements[i].name + "=" + encodeURIComponent(form.elements[i].value));
                    break;
                }
                break;
            }
        }
        return q.join("&");
    }

    // Element should be the result of getElementByID.
    var mb = function(element)
    {
        this.mainContainer = element;
    };


    mb.prototype.regional = {
        mcta_1: "Form A <This One Works>",
        mcta_2: "Form B",
        mcta_3: "Form C",
        submitFormA: "Submit"
    };

    mb.prototype.mainContainer = undefined;
    mb.prototype.currentForm = undefined;
    mb.prototype.APIEndpoint = 'https://www.example.com/api/';

    mb.prototype.initialize =  function()
    {
        this.createMainPageHTML();
    };

    mb.prototype.hideContainers = function()
    {
        var list = getElementsByClassName(this.mainContainer, "mbform_container");
        for (var i = 0; i < list.length; i++)
        {
            list[i].style["display"] = "none";
        }
    }

    mb.prototype.createMainPageHTML = function()
    {
        var element = document.getElementById('mbform_mainpage');

        this.hideContainers();

        if (typeof(element) != 'undefined' && element != null)
        {
            element.style["display"] = "block";
        }
        else
        {
            var mcta1 = '<div id="mbform_mcta1" class="button">'+ this.regional.mcta_1 +'</div>';
            var mcta2 = '<div id="mbform_mcta2" class="button">'+ this.regional.mcta_2 +'</div>';
            var mcta3 = '<div id="mbform_mcta3" class="button">'+ this.regional.mcta_3 +'</div>';
            var mpHTML = '<div id="mbform_mainpage" class="mbform_container">'+mcta1 + mcta2 + mcta3 +'</div>';
            // TODO Load CSS

            // Add HTML to DOM
            this.mainContainer.innerHTML = mpHTML; // takes an HTML string
            // TODO Load JS Dependencies

            // Add Event listener
            document.getElementById('mbform_mcta1').addEventListener("click", bind(this, "createFormA"), false);
            document.getElementById('mbform_mcta2').addEventListener("click", bind(this, "createFormB"), false);
            document.getElementById('mbform_mcta3').addEventListener("click", bind(this, "createFormC"), false);
        }


    };

    mb.prototype.createFormA = function()
    {
        var element = document.getElementById('mbform_FormAContainer');

        this.hideContainers();
        this.currentForm = "mbform_FormA";

        if (typeof(element) != 'undefined' && element != null)
        {
            element.style["display"] = "block";
        }
        else
        {
            var iemail = '<input type="text" value="" name="email" id="mbform_iemail"/><br/>';
            var ifirstname = '<input type="text" value="" name="firstname" id="mbform_ifirstname"/><br/>';
            var ilastname = '<input type="text" value="" name="lastname" id="mbform_ilastname"/><br/>';
            var formHTML = '<form id="mbform_FormA">'+iemail + ifirstname + ilastname + '</form>'
            // TODO : Disable form default bahaviour (Submit when Enter Keypress + submit button), button is placed outside form for testing purpose.
            var submitButton = '<button id="mbform_FormASubmit">'+ this.regional.submitFormA +'</button>';

            var formContainerHTML = '<div id="mbform_FormAContainer" class="mbform_container">'+ formHTML + submitButton +'</div>';

            this.mainContainer.innerHTML += formContainerHTML; // Append HTML to existing

            document.getElementById('mbform_FormASubmit').addEventListener("click", bind(this, "saveForm"), false);
        }
    };

    mb.prototype.createFormB = function()
    {
        console.log(this.mainContainer); // Is scope ok ?
    };

    mb.prototype.createFormC = function()
    {
        console.log(this.mainContainer); // Is scope ok ?
    };

    mb.prototype.getPostFromCurrentForm = function()
    {
        var data = serialize(document.getElementById(this.currentForm));
        console.log(data);
        return (data);
    }

    mb.prototype.saveForm = function()
    {
        var xhr = new XMLHttpRequest();
        var params = this.getPostFromCurrentForm();

        xhr.open('POST', this.APIEndpoint, true);
        xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
        xhr.onload = function () {
            console.log("Loading...");
        };
        // Current APIEndpoint is example.com, do not send the form.
        //xhr.send(params);
    }

    return (mb);
})();

And here is how you initialize the library

(function() {
            var dom = document.getElementById('myForm');
            var mb = new MyBasicLibrary(dom);
            mb.initialize();
})();

I did my best to respect javascript best practice but I’m not sure if I’ve done it well. What do you think about it ?
Also, having raw HTML inside variable seems to be a bad practice, however I need to minimize the number of request made to the server for performance purpose. It means that I can’t store my HTML server side and send it through AJAX. Is it the best solution ?

I’ve created a JSFiddle in case you want to tweak it.
https://jsfiddle.net/xd4ojka2/

Solution

Library violates Single Responsibility Principle, Open/Closed Principal, and Don’t Repeat Yourself.

Responsibilies:

  • Posting Data
  • Serializing Data
  • Creating Markup
  • Maintaining State
  • UI effects
  • Logging

Open/Closed:

element-nodeName-type combination would currently be difficult for consumer of the library to extend to allow them to handle more.

Forms – Currently can only handle 3, could be made to handle zero to how many the hardware can handle.

Don’t Repeat Yourself:

Which can be seen if the question’s code is compared with this answers. Added some local variables and functions to reduce redundancy, also used native document method.

window.MyBasicLibrary = (function() 
{
'use strict';

    // Utilitarian function not to loose the scope 
    function bind(context, name){
        return function(){
            return context[name].apply(context, arguments);
        };
    }

    // Utilitarian function to get DOM by class
    function getElementsByClassName(node, classname) 
    {
        return node.getElementsByClassName(classname);// works IE9+
    }

    /*
    ** Utilitarian function to serialize a form
    ** See : https://code.google.com/archive/p/form-serialize/
    */
    function serialize(form) 
    {
        if (!form || form.nodeName !== "FORM") {
            return;
        }
        var i, j, name, value, q = [];
        var elements = form.elements;
        var push = function()
        {
            q.push(name + "=" + encodeURIComponent(value));
        };
        for (i = elements.length - 1; i >= 0; i = i - 1) {
            var element = elements[i];
            name = element.name;
            if (name === "") {
                continue;
            }
            value = element.value;
            switch (element.nodeName) {
            case 'INPUT':
                switch (element.type) {
                case 'text':
                case 'hidden':
                case 'password':
                case 'button':
                case 'reset':
                case 'submit':
                    push();
                    break;
                case 'checkbox':
                case 'radio':
                    if (element.checked) {
                        push();
                    }                       
                    break;
                case 'file':
                    break;
                }
                break;           
            case 'TEXTAREA':
                push();
                break;
            case 'SELECT':
                switch (element.type) {
                case 'select-one':
                    push();
                    break;
                case 'select-multiple':
                    var options = element.options;
                    for (j = options.length - 1; j >= 0; j = j - 1) {
                        var option = options[j];
                        if (option.selected) {
                            value = option.value;
                            push();
                        }
                    }
                    break;
                }
                break;
            case 'BUTTON':
                switch (element.type) {
                case 'reset':
                case 'submit':
                case 'button':
                    push();
                    break;
                }
                break;
            }
        }
        return q.join("&");
    }

    // Element should be the result of getElementByID.
    var mb = function(element)
    {
        this.mainContainer = element;
    };


    mb.prototype.regional = {
        mcta_1: "Form A <This One Works>",
        mcta_2: "Form B",
        mcta_3: "Form C",
        submitFormA: "Submit"
    };

    mb.prototype.mainContainer = undefined;
    mb.prototype.currentForm = undefined;
    mb.prototype.APIEndpoint = 'https://www.example.com/api/';

    mb.prototype.initialize =  function()
    {
        this.createMainPageHTML();
    };

    mb.prototype.hideContainers = function()
    {
        var list = getElementsByClassName(this.mainContainer, "mbform_container");
        for (var i = 0; i < list.length; i++)
        {
            list[i].style["display"] = "none";
        }
    }

    mb.prototype.createMainPageHTML = function()
    {
        var element = document.getElementById('mbform_mainpage');

        this.hideContainers();

        if (typeof(element) != 'undefined' && element != null)
        {
            element.style["display"] = "block";
        }
        else
        {
            var button = function (id, text)
            {
                return '<div id="mbform_mcta' + id + '" class="button">'+ text +'</div>';
            };
            var mcta1 = button(1, this.regional.mcta_1);
            var mcta2 = button(2, this.regional.mcta_2);
            var mcta3 = button(3, this.regional.mcta_3);
            var mpHTML = '<div id="mbform_mainpage" class="mbform_container">'+mcta1 + mcta2 + mcta3 +'</div>';
            // TODO Load CSS

            // Add HTML to DOM
            this.mainContainer.innerHTML = mpHTML; // takes an HTML string
            // TODO Load JS Dependencies

            // Add Event listener
            var listener = function(id, letter)
            {
                document.getElementById('mbform_mcta' + id).addEventListener("click", bind(this, "createForm" + letter), false);
            };
            listener(1, "A");
            listener(2, "B");
            listener(3, "C");
        }


    };

    mb.prototype.createFormA = function()
    {
        var element = document.getElementById('mbform_FormAContainer');

        this.hideContainers();
        this.currentForm = "mbform_FormA";

        if (typeof(element) != 'undefined' && element != null)
        {
            element.style["display"] = "block";
        }
        else
        {
            var text = function(name)
            {
                return '<input type="text" value="" name="' + name + '" id="mbform_i' + name + '"/><br/>';
            };
            var iemail = text("email");
            var ifirstname = text("firstname");
            var ilastname = text("lastname");
            var formHTML = '<form id="mbform_FormA">'+iemail + ifirstname + ilastname + '</form>'
            // TODO : Disable form default bahaviour (Submit when Enter Keypress + submit button), button is placed outside form for testing purpose.
            var submitButton = '<button id="mbform_FormASubmit">'+ this.regional.submitFormA +'</button>';

            var formContainerHTML = '<div id="mbform_FormAContainer" class="mbform_container">'+ formHTML + submitButton +'</div>';

            this.mainContainer.innerHTML += formContainerHTML; // Append HTML to existing

            document.getElementById('mbform_FormASubmit').addEventListener("click", bind(this, "saveForm"), false);
        }
    };

    mb.prototype.createFormB = function()
    {
        console.log(this.mainContainer); // Is scope ok ?
    };

    mb.prototype.createFormC = function()
    {
        console.log(this.mainContainer); // Is scope ok ?
    };

    mb.prototype.getPostFromCurrentForm = function()
    {
        var data = serialize(document.getElementById(this.currentForm));
        console.log(data);
        return (data);
    };

    mb.prototype.saveForm = function()
    {
        var xhr = new XMLHttpRequest();
        var params = this.getPostFromCurrentForm();

        xhr.open('POST', this.APIEndpoint, true);
        xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
        xhr.onload = function () {
            console.log("Loading...");
        };
        // Current APIEndpoint is example.com, do not send the form.
        //xhr.send(params);
    };

    return (mb);
})();

Leave a Reply

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