JS and HTML without Frameworks Odyssey (bindings)

JS and HTML without Frameworks Odyssey (bindings)

Introduction

In our previous article, we delved into JavaScript and HTML without relying on any frameworks. We constructed a basic MVC architecture to facilitate page navigation, aiming to create a Single Page Application (SPA) adhering to MVC principles. While such scenarios are rare in real-world applications, developers typically utilize existing frameworks rather than creating new ones from scratch. Nevertheless, our journey was driven by the pursuit of learning.

Upon reviewing the source code of my previous blog post, I noticed a significant omission: bindings. In this follow-up post, let's work on implementing simple binding mechanisms to enhance coding efficiency in future projects. So, without further delay, let's dive in.

What are bindings

Bindings are the backbone of web development, linking data in an application's model to corresponding elements in its view. This linkage ensures seamless updates between data and the user interface, simplifying development and fostering code organization. By decoupling presentation logic from business logic, bindings facilitate easier maintenance and scalability of applications. Essentially, bindings act as the glue that harmonizes various components of a web application, facilitating smooth communication between the data layer and the user interface.

Exploring Binding Types

Data bindings can be one-way, where data flows only from the model or data source to the view on a page. Changes in the model automatically update the view, but changes in the view do not affect the model. Alternatively, data bindings can be two-way, allowing data to flow both from the model to the view and from the view to the model. In one-way data binding, the flow of data is simpler and more predictable, making it suitable for scenarios where unidirectional data flow is sufficient. On the other hand, two-way data binding provides real-time synchronization between the model and the view, which is beneficial for interactive user interfaces where users need to input data and see immediate feedback.

Practical Implementation

Let's put theory into practice by creating a component named Profile, consisting of profile.js and profile.html files. Our objective is to implement two-way bindings, ensuring that changes in data automatically reflect in the UI and vice versa.

My approach is to create a base class named Component. All subsequent components will inherit from the Component class. Initially, I'll incorporate the binding logic within the Component class, although in the future, I intend to develop a separate binding handler for this purpose. However, for now, we can include the logic within the base class and observe its behavior. Additionally, I plan to introduce a 'data-bind' attribute. This attribute will signify that upon every change of this element, we need to locate the corresponding data and update it, ensuring that once the UI is updated, the data is also updated accordingly.

So the logic is as follows: we need to find elements with a specific attribute, subscribe to their 'input' events, and update the data accordingly. Let's proceed by implementing a function updateInstance responsible for locating the data based on the attribute value and passing it accordingly.

    updateInstance(event) {
        if (event.target && event.target.dataset.bind) {
            let t = this;
            const instance = event.target.dataset.bind.split('.');
            instance.forEach((e, i) => {
                if (i == instance.length - 1) {
                    t[e] = event.target.value
                } else {
                    t = !t[e] ? t[e] = Object.create(null) : t[e];
                }
            });
        }
    }

This function first checks if the event has a valid target and if that target has a bind. If the conditions are met, the function extracts the property path from the data-bind attribute using split('.'). This splits the attribute value into an array of strings, where each string represents a key or property within the instance.

Then, it iterates over each key in the property path using forEach. For each key:

  • If it's the last key in the path (i.e., the leaf node), it assigns the value of the DOM element to that property of the instance.

  • If it's not the last key, it checks if the property exists in the current instance. If it doesn't exist, it creates an empty object using and assigns it to the property. It then updates the reference to point to the newly created object for further property traversal.

In summary, this function dynamically updates the state or properties of an instance based on changes in the DOM elements, allowing for flexible and reactive behavior in the application.

The logic for updating the UI based on data changes can be implemented in various ways. One effective approach that I found most suitable for myself is to utilize Proxy. By wrapping my data with Proxy and updating the set function, I can achieve seamless and efficient synchronization between the data and the UI.

    updateUI(keys = []) {        
        let object = this;
        keys.forEach(key => {
            object[key] = new Proxy(object[key] || Object.create(null), {
                set(target, prop, value) {
                    if(target[prop] !== value) {
                        target[prop] = value;
                        document
                        .querySelectorAll(`[data-bind='${[...keys, prop].join('.')}']`)
                        .forEach(l => { l.value = value; });
                        return true;
                    }
                }
            });
            object = object[key];
        });
    }

This code defines a function named updateUI responsible for updating the UI whenever the data model changes. It accepts an optional parameter keys, which is an array representing the path to the property that needs updating in the data model. Here's a simplified explanation:

The function iterates over each key in the keys array and creates a Proxy object for the corresponding property in the data model. It overrides the set trap of the Proxy to intercept property assignments. When a property is set, it checks if the new value is different from the existing value. If the new value is different, it updates the property in the data model and updates all DOM elements with a data-bind attribute that match the updated property's path. Finally, it returns true to indicate that the property assignment was successful.

This function dynamically ensures that UI elements are updated whenever specific properties in the data model change, maintaining synchronization between the data and the UI.

Now we have to put everything together during the initialization of the Component.

    binding() {        
        const dataBindingCollection = Array.from(document.querySelectorAll('[data-bind]'));
        if (dataBindingCollection.length > 0) {            
            dataBindingCollection.forEach(e => this.htmlBinding(e, this.updateInstance.bind(this)));
            const array = dataBindingCollection.map(e => e.dataset.bind.split('.').slice(0, -1).join('.'));
            const set = new Set(array).forEach(e => this.updateUI(e.split('.')));
            this.update(dataBindingCollection);
        }
    }

This function selects all elements with a data-bind attribute and subscribes to input events to update the data model through the updateInstance function. It also wraps the data with a Proxy using the same data-bind collection.

Conclusion

In conclusion, the implementation of binding mechanisms is pivotal in modern web development. By establishing seamless connections between data and user interface elements, bindings enhance user experience and ensure the consistency and accuracy of displayed information. Through organizing code, facilitating efficient data flow, and offering flexibility in approach, bindings contribute significantly to code maintainability and adaptability. As we continue to explore and experiment with binding techniques, we not only enhance our development skills but also foster continual learning and improvement within the web development community.

Happy coding!