Advanced JS - Refactoring Toward Objects

Javascript has several object creation syntaxes, but object configurations (in a sense, associative arrays) occupy a special place among them. This modeling approach is often referred to as object configurations because it contains more configuration information when used, similar to JSON syntax in practice. When object configurations do not store motion in themselves, they act as an associative array to it. Associative arrays are another form of an array whose index is usually a form of an array containing more meaningful phrases.

PS: You can download the source codes of the project here. Don't forget to put a star on the repo if you like it.

Author's note: One of the top 10 JS topics we used most when programming a payment terminal was object configurations. When a new provider was integrated into the terminal, information such as the added provider's identifier, min-max payment limit, which provider group it belongs to, the names of parameters that will come and go during the request-response, etc., were stored in the object configurations.

{
    "__type": "pages",
    "__objects": [{
        "pageId": "30492001",
        "title": "",
        "useOnline": "true", // Онлай авторизация false-нет, true-да.
        "__objects": [{
            "__type": "controls",
            "__objects": [{
                "header": "Xahiş edirik, 15 rəqəmli kodu daxil edin", // Приветствие
                "footer": "", // Подпись под полем ввода номера
                "mask": "", // Маска поля ввода
                "name": "account",
                "nobr": "false",
                "regexp": "^\\d{15}$",
                "strip": "True", // Удаление доп. символов в маске например кавычки. true - удалить false - нет 
                "type": "text_input"
            }, {
                "layout": "DG", // Язык клавиатуры по умолчанию DG - цифровая, Буквенная ( AL - английский язык, ALC - Всключенный Capslock, ALS - Зажат Shift, ALR - Русская раскладка, ALRC - Русская с Capslock, ALRS - Русская с Shiftom (Первая буква))
                "type": "keyboard"
            }]
        }]
    }]
}

PS: User-defined arrays just store information, not specific actions. But object configurations store both associative data and actions (methods). Therefore, it is normal that you will not come across the term associative array in many JS literature.

How are object configurations created?

Object configurations are mainly declared using the {} symbol. You will rarely come across a variant written with a new Object(). Object configurations usually store the configuration of a particular object according to its name, and methods for manipulating those configurations. This declaration of an object allows concepts to be viewed as objects.

let person = {
    name: "Hesen",
    surname: "Mammadov"
}

You can use it as an associative array, simply storing values in object configurations. When developing large applications, it may be very necessary for the object to act as an associative array. So getting some information from the backend can be expensive for your application. At this time, we store static information in the form of an associative array. When we programmed the Emanat terminal, we saved the configuration of the providers in the form of an associative array. If the provider names on the terminal's home page came from the backend, the terminal would be heavily loaded. But reading them from a JS associative array gives us maximum speed.

There are some rules to be aware of when creating object configurations.

Step 1

To protect the internal structure of the object from external interference, the variables of the object are usually declared with the sign _.

let person = {
    _name: "Hesen",
    _surname: "Mammadov",
    changeName: function (name) {
        //validate here
        this._name = name;
    }
}

Step 2

Following the OOP paradigm, the internal variables of an object should only be allowed to change via a method.

Step 3

It is possible to dynamically add an absent field to the object. Because object configurations can also act as just another form of arrays!

//this code however works fine...
person.age = 45;

Step 4

By configuring the object, it is possible to prohibit dynamically adding objects to its, iteration.

Step 5

The variable of the object, unlike normal variables, consists of two or more words and can be written separately. In this case, we can simply refer to it using the array syntax.

let person = {
    _name: "Hesen",
    _surname: "Mammadov",
    'full info':'Full info here',
    changeName: function (name) {
        //validate here
        this._name = name;
    }
}

Step 6

If the property names of the object follow the naming rules of variables, then we can write them in quotes or not.

let person = {
    '_name': "Hesen",
    '_surname': "Mammadov",
    'full info':'Full info here',
    changeName: function (name) {
        //validate here
        this._name = name;
    }
}

What we learned in practice

To practice what we've learned, we'll try to put together a small app like the one below using javascript. We will create not only the animation on the page but all the HTML elements using Js so that we can better understand the object configurations.

Advanced JS : Refactoring toward Objects

In the first version of the program, the following functional codes were written to create the container and buttons of the page

function createButton(txt, classList) {
    let btn = document.createElement('button');
    btn.innerText = txt;
    for (let f of classList) {
        btn.classList.add(f);
    }
    return btn;
}

function createImage(src, width) {
    let img = document.createElement('img');
    img.src = src;
    img.style.width = width;
    return img;
}

function createContainer() {
    let dvContainer = document.createElement('div');
    dvContainer.className = 'controls';
    return dvContainer;
}

These functions are ultimately used by the loadAllHtml() function.

function loadAllHtml() {
    let img = createImage('img/going.gif', '20%');
    document.body.insertBefore(img, document.body.firstChild);
    let dvContainer = createContainer();
    document.body.insertBefore(dvContainer, document.getElementById('scr'));
    let btn1 = createButton('Start', ['btn', 'btn-start']);
    let btn2 = createButton('Stop', ['btn', 'btn-stop']);
    let btn3 = createButton('Reset', ['btn', 'btn-reset']);
    dvContainer.appendChild(btn1);
    dvContainer.appendChild(btn2);
    dvContainer.appendChild(btn3);
}

Our function called activeAllElements() is designed to connect to events. this function allows us to bind events to buttons on the page.

function activeAllElements() {
    let interval = null;
    let isRunning = false;
    document.getElementsByClassName('btn-start')[0].addEventListener('click', function() {
        if (!isRunning) {
            interval = setInterval(() => {
                let img = document.querySelector('img');
                let iml = parseInt(getComputedStyle(img).marginLeft);
                iml += 2;
                isRunning = true;
                img.style.marginLeft = `${iml}px`;
            }, 20);
        }
    });
    document.getElementsByClassName('btn-stop')[0].addEventListener('click', function() {
        if (interval != null) {
            clearInterval(interval);
            isRunning = false;
        }
    });
    document.getElementsByClassName('btn-reset')[0].addEventListener('click', function() {
        if (interval != null) {
            clearInterval(interval);
            isRunning = false;
            document.querySelector('img').style.marginLeft = 0;
        }
    });
}

Although the codes we wrote work, they have very serious gaps. Let's list some of these gaps,

  1. Functions like createButton() and createImage() are very poorly written functions. They do not allow solving all problems from a class in this example. Since we are talking about the HTML element in the end, these functions can be combined to create a final function, which we have done in the OOP version.
  2. The registration process for events is not perfect. Because the codes we write are strongly dependent on the context, it is almost impossible to use those functions in another similar project.
  3. The activeAllElements() function does not completely reflect reality in terms of both naming and operation. Because there are codes here just for the sake of work.

Refactoring towards object

Refactoring is the process of improving the code of a program without changing its functionality. That is, the program works correctly as it is, but the code is refined.

To do proper refactoring in the code we write, we first need to do a proper separation of responsibility.

According to the division of responsibilities, it is necessary to ensure the creation of objects on the page and the preparation of animation. We will create a separate object for each of these tasks. First, let's look at our DOM object. This object will allow you to easily create DOM elements.

let dom = {
    createElement: function(tagName, className, cssProps) {
        //create dom element
        let element = document.createElement(tagName);
        //check if this element is valid html element
        if (element != null) {
            //if so, then validate classname existance
            if (this._isValidValue(className)) {
                element.className = className;
            }
            //enumerate all css properties given as  object
            for (let f in cssProps) {
                //check if object key is valid attribute
                if (this._isValidAttr(f)) {
                    //then use it as attribute
                    element[f] = cssProps[f];
                }
                //otherwise,it is just style.Use it as style..
                else {
                    element.style[f] = cssProps[f];
                }
            }
        }
        return element;
    },
    _isValidValue: function(val) {
        return (typeof(val) !== "undefined" && val)
    },
    _isValidAttr: function(attr) {
        return (attr == 'src' || attr == 'href' || attr == 'innerText' || attr == 'id');
    }
}

Now that we can create DOM elements, we can move on to the animation part. Object-oriented mechanism saves us from the concept of a global variable.

let animation = {
    _isRunning: false,
    _interval: null,
    _animatableElementInstance: null,
    animate: function(elementId, sec) {
        this._animatableElementInstance = document.getElementById(elementId);
        if (!this._isRunning) {
            this._interval = setInterval(function() {
                let element = document.getElementById(elementId);
                let imgML = parseInt(getComputedStyle(element).marginLeft);
                imgML += 1;
                element.style.marginLeft = `${imgML}px`;
            }, sec);
            this._isRunning = true;
        }
    },
    stopAnimate: function() {
        if (this._interval != null) {
            this._isRunning = false;
            clearInterval(this._interval);
        }
    },
    resetAnimate: function(elementId) {
        this.stopAnimate();
        let element = document.getElementById(elementId);
        element.style.marginLeft = 0;
    }
}

In the end, we just rely on the functional aspect to create the elements on the page.

function loadAllHtmlContent() {
    //create img
    let img = dom.createElement('img', 'img-basic', {
        'src': "going.gif",
        'width': "20%",
        'id': 'animetableImage'
    })
    //add it before script
    document.body.insertBefore(img, document.getElementById('scr'));
    //create container
    let divContainer = dom.createElement('div', 'controls');
    //with buttons
    let btn_start = dom.createElement('button', 'btn btn-start', {
        'innerText': 'start'
    });
    let btn_stop = dom.createElement('button', 'btn btn-stop', {
        'innerText': 'stop'
    });
    let btn_reset = dom.createElement('button', 'btn btn-reset', {
        'innerText': 'reset'
    });
    //add them to container
    divContainer.appendChild(btn_start);
    divContainer.appendChild(btn_stop);
    divContainer.appendChild(btn_reset);
    //append container after img
    document.body.after(img, divContainer);
}

After writing these codes, it is enough for us to add this code by simply opening the script tag inside the body tag on our required page.

document.addEventListener('DOMContentLoaded', function() {
    //create all html elements
    loadAllHtmlContent();
    document.querySelector('.btn-start').addEventListener('click', function() {
        animation.animate('animetableImage', 10);
    });
    document.querySelector('.btn-stop').addEventListener('click', function() {
        animation.stopAnimate();
    });
    document.querySelector('.btn-reset').addEventListener('click', function() {
        animation.resetAnimate('animetableImage');
    });
});

As you can see, each object performs only its operations.