8
\$\begingroup\$

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/

\$\endgroup\$
2
\$\begingroup\$

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);
})();
\$\endgroup\$
  • \$\begingroup\$ Hi, just wanted to warn you that, as is, I can't accept your answer as it's not a review of my code but a minor improvement at best. \$\endgroup\$ – Gary Olsson Feb 16 '17 at 10:17

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.