Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / Javascript

Two-Way Data Binding in Pure JavaScript

5.00/5 (3 votes)
27 Apr 2020MIT11 min read 24.9K  
Detailed explanation and examples of databind approach to a solution
This article introduces databind approach to a solution - you will see a detailed explanation with examples. Moreover, it would mention the basic concepts of how to create events on a pure object. You will find advice to build a clear structured solution, and how to implement a small Web JS application.

Introduction

It is relatively easy to attach and detach events to any element over JavaScript. The more challenging task is when the application becomes bigger and messy with lots of attach/detach constructions. Eventually, it becomes hard to manage everything, or figure out the purpose of some event handler. There should be a purpose, a plan for event handlers. Though, when trying to update or find an issue within the solution would become just a task to fix the case, instead of spending much time to figure out what are all those attached event handlers for.

A Few Words About Code Planning and Good Architecture

Regularly occurring code constructions can be Ajax requests, attach/detach events, show/hide views, etc. They are easy to determine since they often come in pairs. Or there could be enough repetition that the tension appears to over-code all repeated similarities into the high-level solution. Sometimes, it is a good approach. Sometimes, it could lead to too many restrictions, though, thinking that having all those parts just copy/pasted with slightly modified changes seemed to look like not a bad idea. It could be a boring task. When it comes time to investigate the code and grep (find) all values, all similar code parts can be easily located over lookup search terms or regex.

It is always good to have a plan. Even if it is stupid. Even when there is no plan, still it is a plan. A plan can occur in the middle of code writing. The good solution, the finished one is the solution with the plan/idea that was from the beginning or emerged during implementation.

The Big Structure for the Small JS Application

It can be skipped by impatient readers. The main part is after the title "Introducing data binding (more rules/more planning)". To make the subject easy to comprehend, let's think about a small application. How can it be structured and implemented? What are the participants? Where is the place that would allow to give more attention to data binding and less focus on the other stuff.

There should be a place for participants. Participants would communicate with each other by means of exchange data. We will call it an application. Think about a place for a repetitive code that can be over-coded. I'm talking about utilities. And there should be a couple of participants that would keep data binding. Probably, something more like templates to build UI.

The simple structure of the application that would help to focus on the data binding implementation.

JavaScript
class App {
    // will keep the most needed part to run an application
    constructor(selector) {
    }
    // will make properties that are needed just from the beginning
    // but not a part of the constructor
    initialize() {
    }
    // will make more properties to initialize
    // can be used several times or just to easily locate when reading code
    initialize$App(el) {
    }
    // This is a helper method.
    // It would show useful information to understand the process.
    logModelState(model) {
    }
    // there we will keep stuff related to databinding
    bind(formModel) {
    }
    // clean up everything what was produced by initialize method
    remove() {
    }
}
// application entry point
setTimeout(() => {
    // There is no working code yet
    // but there is a clue how it could be used.
    // The actual application with "body" element
    const app = new App('body');
    // some models. For this example, just one.
    const model = new TimerModel();
    // This is a part to initialize the application
    app.initialize();
    // This is a part that would give application ability to read input from UI
    app.bind(model);
});

The most important part of the application is user input. The look of the application. UI will allow us to read and process user input. Let's build a small UI with a couple of fields and basic debugging. The UI would look something like that.

The HTML part of the application:

JavaScript
// template for the main form
// will hold the main UI that is not related to the subject
// but it is important as it would hold the form with fields
const mainTemplate = data => `
<div>
    <div class="form">
    </div>
    <pre class="model-state">
    </pre>
</div>
`;
// template for the form (our main focus)
// assist user input. UI that will participate with databinding
const formTemplate = data => `
<div class="form-item"><label>Enter your name: 
<input type="text" class="name" size="40" value=""/></label></div>
<div class="form-item"><label>Base64 code name: 
<input type="text" class="output" size="40" value=""/></label></div>
<div class="form-item"><span class="current-time"> </span></div>
`;

Since it is communication, there should be at least two participants. The first that would present the form and the second that would process the input from UI and show the processed results. And some debug information.

The View and the Model:

JavaScript
// The first participant
// will read UI and pass data to a model
class FormView {
    // As usual, it would keep the most needed part to run Form UI
    // make events, attach event handlers
    constructor(selector) {
    }
    // Make properties that are needed from the beginning build UI
    initialize() {
    }
    // This part will make properties that are part of the UI
    initialize$FormView(el) {
    }
    // will bind UI to properties over events
    bind(model) {
    }
    // will remove properties form events
    unbind() {
    }
    // clean up everything what was produced by initialize method
    remove() {
    }
}
// The second participant
// This will hold some business logic along with databinding
class TimerModel {
    // This will make a part that is required to run Model
    // will build databinding logic here
    constructor() {
    }
    // Make more properties that are a part of the databinding
    initialize() {
    }
    // This will simulate business logic of the application
    processForm() {
    }
    // Detach event listeners
    // Removes more resources, e.g., timer function
    remove() {
        utils.getResult(this, () => this.processFormRemove);
        clearInterval(this.timer);
    }
}

The main structure is finished. Still, there is nothing visible on UI. It is time to think about internal implementation. First - show UI. UI will come from templates. There should be a tool that would deal with rendering HTML. If there is UI, then we need to locate DOM elements, attach/detach event handlers. It should be simple, versatile, maybe with new fresh ideas.

JavaScript
// keeps tools that usually repeated many times in the code
// and can be extracted into the separate namespace
const utils = {
    // renders HTML from template to UI
    html(el, html) {
        // one line of implementation. For production would not be enough.
        // Looks perfect for our example.
        el.innerHTML = html;
    },
    // locates element to keep on a form object
    // the method is based on the modern Web API
    // with the best practice from jQuery
    el(selector, inst = document) {
        // it is expected that there could be passed null or undefined
        if (!selector) {
            return null;
        }
        // it is expected selector can be a string or element instance
        if ('string' === typeof selector) {
            return inst.querySelector(selector);
        }
        // if selector is instance, let's just return it
        return selector;
    },
    // attach and detach event handler to/from DOM element
    // that method will return another function to remove event handler
    // I have a long thought about what would give small code and
    // ended up with this solution
    on(inst, selector, eventName, fn) {
        // makes anonymous function
        // Smells like a potential memory leak and not convenient to use
        const handler = function (evnt) {
            // There is a catch. With this condition, it would be possible to use
            // event bubble feature. Event handler can be attached to the parent
            // element. Attaching event handlers to parent element will allow to
            // re-render internal html of the view many times without re-attaching
            // event handlers to child elements
            if (evnt.target.matches(selector)) {
                fn(evnt);
            }
        }
        // definitely it can leak memory
        inst.addEventListener(eventName, handler);
        // Let's fix inconvenience. Let's return another method that would help
        // to deal with detach handler. Now the "on" method is going to be
        // used more conveniently. But with certain approach.
        // remove event handler from the event listener element
        return function () {
            inst.removeEventListener(eventName, handler);
        }
    },
    // this is tool to evaluate method
    // if method exists on the object, it will be evaluated
    // to avoid "ifs" in the code implementation
    getResult(inst, getFn) {
        const fnOrAny = getFn && getFn();
        if (typeof fnOrAny === 'function') {
            return fnOrAny.call(inst);
        }
        return fnOrAny;
    }
};

Now we can build UI. Let's think about the constructors from classes App and FormView. There is no point for those classes without UI. They are accepting one selector argument within the method signature.

Lets put this line this.el = utils.el(selector); into their constructors:

JavaScript
constructor(selector) {
    // It will create "el" property on the instance.
    // The main element that would hold more UI elements within.
    this.el = utils.el(selector);
}

The small rule. In the case it is view class that works with DOM element, we will keep DOM element in the el property. Since there is el variable on the instance, let's use it to render HTML. And since it is about rendering HTML, I'm not sure that it would be a great idea to keep it inside of the constructor. It would be better to have separate initialize method just for that purpose.

The updated initialize method of the App class:

JavaScript
initialize() {
    utils.html(this.el, mainTemplate({}));
    this.initialize$App(this.el);
    this.form.initialize();
}

The updated initialize method for the FormView class:

JavaScript
initialize() {
        utils.html(this.el, formTemplate({}));
        this.initialize$FormView(this.el);
}

I guess you have noticed two more new methods calls: initialize$App and initialize$FormView. Do you remember, I've mentioned a plan? Good or bad it doesn't matter. This is it. It is hard to judge now how is it good to build properties for DOM elements within initialize method. I have decided to keep such commands separated. In case it is a bad plan, I can rethink and extract those methods into the parent initialize method. In case it is good - such structure will be preserved.

Given below are implementations of initialize$App and initialize$FormView methods:

JavaScript
initialize$FormView(el) {
    this.name = utils.el('.name', el);
    this.output = utils.el('.output', el);
    this.currentTime = utils.el('.current-time', el);
}
...
initialize$App(el) {
    this.form = new FormView(utils.el('.form', el));
    this.modelState = utils.el('.model-state', el);
}

Both methods are responsible to make more properties on the instance. And it is the only place that would build/update such properties. Any time view will be (re)rendered, those methods can be called after and refresh properties with new DOM elements.

The application can be started and it should show a simple form with two fields/text-boxes. Nothing more right now. The most interesting part will come when data binding will be introduced .

Introducing Data Binding (More Rules/More Planning)

The first task of data binding is to monitor changes. In the example, changes will come from the view/UI to the model and changes will come from the model to view. The simple part is to adjust the solution with receiving changes from the view/UI.

Take into account that it is not about a one-time change. There is input/text-box. Text can be entered, deleted, copy/pasted many times till the final version will be accepted by the form. Text-box can have a default value. There could be future code updates, e.g., introduce fields validation, updates to UI design. The model for the view can be replaced with another model with different initial values in fields. The view should be nicely decoupled from the model as well. The point is that view should be sufficient by itself even if it is not attached to the model.

The focus is on data that comes from UI instead of the way in which that data can be enquired from the form. Next rule. The data can be read/written by getters/setters. Two benefits and one drawback. Easy to read/write values from fields, do future updates into UI. E.g., replace input text by complex dropdown with predefined values and search suggestions. It would help to keep binding rules clear. And drawback, need to write more code for getters and setters.

If it is about more code, there is another rule. Let's keep getters/setters pairs between constructor and initialize methods. With that approach, it would be easy to locate getters/setters and understand that they are getters/setters but nothing other.

The getters and setters for both inputs, "Enter your name" and "Base64 code name".

Part of the class FormView:

JavaScript
getName() {
    return this.name.value;
}
setName(val) {
    if (val !== this.name.value) {
        this.name.value = val;
    }
}
getOutput() {
    return this.output.value;
}
setOutput(val) {
    if (val !== this.output.value) {
        this.output.value = val;
    }
}
setCurrentTime(val) {
    if (val != this.currentTime.innerText) {
        this.currentTime.innerText = val;
    }
}

It is easy to read and write data to UI, there is no need to pass some variables that contain changed data. It would be enough to pass the view and refer to data over getters and setters. That opens another possibility. Now DOM event handlers can be written in a simpler way.

And if to talk about event handlers, there will be a lot of them. The good clue for the next rule. This time, it is naming. Let's name event handlers according to this pattern: on<EventName><FieldName>, e.g., onInputName. This event handler will pass the value from the view/UI to the model.

View to Model event handlers. One more rule: Let's keep all handlers after the unbind method in the class FormView.

JavaScript
onInputName() {
    this.model.prop('name', this.getName());
}
onInputOutput() {
    this.model.prop('output', this.getOutput());
}

this.model - This is something that I will regret with the example. It makes a strong coupling between view and model. Since it is about data binding, let's keep it coupled with a form for the seek of brevity. Obviously, for the real application, the view should be decoupled from the model. This is a good candidate to optimize in the end.

Time for the first data binding commands. Attach event handlers to the related elements. Again rule. Let's keep all events that we are attaching and that are a part of the data binding within bind method.

Updated bind method:

JavaScript
// here, model argument will be a clue that there is a room to optimize the solution
bind(model) {
    // update data from DOM to model
    this.onInputNameRemove = utils.on(this.el, '.name', 'input', () => this.onInputName());
    this.onInputOutputRemove = 
              utils.on(this.el, '.output', 'input', () => this.onInputOutput());
}

this.onInputNameRemove and this.onInputOutputRemove - are methods to detach event handlers from the form. They will be called in the unbind method.

Updated unbind method:

JavaScript
unbind() {
    // detach event handlers from DOM elements
    utils.getResult(this, () => this.onInputNameRemove);
    utils.getResult(this, () => this.onInputOutputRemove);
}

The way to update data from view/UI to model is ready. It was easy, because DOM elements have addEventListener/removeEventListener methods. The model is just a class. Data binding demands that the model should have a way to notify about change state.

To the convenient use it would be great if model would have on method like it is for DOM elements in utils.on. And another important method is trigger, over that method the model will notify any listening participants about changes. Although, it would be great if any class can implement notify interface. That would be useful for the future. It should be small but enough to accomplish data binding.

A couple of methods below:

JavaScript
// notify about changes
function dispatcher() {
    const handlers = [];
    return {
        add(handler) {
            if (!handler) {
                throw new Error('Can\'t attach to empty handler');
            }
            handlers.push(handler);
            return function () {
                const index = handlers.indexOf(handler);
                if (~index) {
                    return handlers.splice(index, 1);
                }
                throw new Error('Ohm! Something went wrong with 
                                 detaching unexisting event handler');
            };
        },
        notify() {
            const args = [].slice.call(arguments, 0);
            for (const handler of handlers) {
                handler.apply(null, args);
            }
        }
    }
}
// builds "on" and "trigger" methods to be used in any object
// makes events from the list of event names that are passed as arguments
// e.g. const [on, trigger] = initEvents('change:name')
function initEvents() {
    const args = [].slice.call(arguments, 0);
    const events = {};
    for (const key of args) {
        events[key] = dispatcher();
    }
    return {
        on(eventName, handler) {
            return events[eventName].add(handler);
        },
        trigger(eventName) {
            events[eventName].notify();
        }
    };
}

With such a tool, it becomes possible to create events, attach/detach event listeners, notify about changes. The first candidate is the model. And the second is the FormView.

The code below adjusts the Model to allow notify about changes:

JavaScript
// this is our full Model that will notify about changes
class TimerModel {
    constructor() {
        // lets construct events
        const{ on, trigger } = initEvents(this,
            'change:name', // notify name property changes
            'change:output', // notify output property changes
            'change:time' // notify time property changes
        );
        // now model will allow to trigger and subscribe for changes
        this.on = on;
        this.trigger = trigger;
        // this is internals state of the model
        this.state = {
            name: 'initial value',
            output: '',
            time: new Date()
        };
        // initialize custom business logic
        this.initialize();
    }
    initialize() {
        this.timer = setInterval(() => this.prop('time', new Date()), 1000);
        this.processFormRemove = this.on('change:name', () => this.processForm());
    }
    // probably it would be to boring to write getter/setter for every field on the model
    // here is a universal method that would serve as getter/setter for any property
    prop(propName, val) {
        if (arguments.length > 1 && this.state.val !== val) {
            this.state[propName] = val;
            this.trigger('change:' + propName);
        }
        return this.state[propName];
    }
    // custom business logic
    processForm() {
        setTimeout(() => {
            this.prop('output', btoa(this.prop('name')));
        });
    }
    // don't forget to have a method that would clean the model
    remove() {
        utils.getResult(this, () => this.processFormRemove);
        clearInterval(this.timer);
    }
}

Let's make the same thing with FormView as well:

JavaScript
class FormView {
    constructor(selector) {
        const { on, trigger } = initEvents(this,
            'change:model'
        );
        this.on = on;
        this.trigger = trigger;
        this.el = utils.el(selector);
    }
...

Now FormView can be adjusted with more data binding commands. This time to transfer data from the model to view. The nice thing to keep in mind is that any repetitive stuff is hard to extract into the "utility" library, at least try to keep in the one place. Try hard to not spread over the whole component solution. The bind, unbind methods and following event handlers methods would be better to keep as close to each other as possible. That would help for future maintenance, maybe refactoring.

The full version of bind, unbind methods and event handlers of the FormView class:

JavaScript
// will bind UI to properties over events
bind(model) {
    // update data from DOM to model
    this.onInputNameRemove = utils.on(this.el, '.name', 'input', () => this.onInputName());
    this.onInputOutputRemove = utils.on
             (this.el, '.output', 'input', () => this.onInputOutput());
    // update data from model to DOM
    this.syncNameRemove = model.on('change:name', () => this.syncName());
    this.syncOutputRemove = model.on('change:output', () => this.syncOutput());
    this.syncCurrentTimeRemove = model.on('change:time', () => this.syncCurrentTime());
}
// will remove properties form events
unbind() {
    utils.getResult(this, () => this.onInputNameRemove);
    utils.getResult(this, () => this.onInputOutputRemove);
    utils.getResult(this, () => this.syncNameRemove);
    utils.getResult(this, () => this.syncOutputRemove);
    utils.getResult(this, () => this.syncCurrentTimeRemove);
}
// transfer data from view/UI to model
onInputName() {
    this.model.prop('name', this.getName());
}
onInputOutput() {
    this.model.prop('output', this.getOutput());
}
//transfer data from mode to view/UI
syncName(evnt) {
    this.setName(this.model.prop('name'));
}
syncOutput(evnt) {
    this.setOutput(this.model.prop('output'));
}
syncCurrentTime() {
    this.setCurrentTime(this.model.prop('time'));
}

Just one more impossible step left. That makes all that work. The first thing that comes to my mind is to make more methods. It would be setModel and syncModel methods. The first one is to set a model and trigger change model event on the FormView. The second one is the event handler. And a small update into the constructor method with attach that event handler. Such an event handler is not a part of the data binding but will play an important role in the whole FormView class.

More changes to class FormView:

JavaScript
class FormView {
    constructor(selector) {
        ...
        this.sycnModelRemove = this.on('change:model', () => this.syncModel());
    }
...
    setModel(val) {
        if (val !== this.model) {
            this.model = val;
            this.trigger('change:model');
        }
    }
...
    syncModel() {
        this.unbind();
        this.setName(this.model.prop('name'));
        this.setOutput(this.model.prop('output'));
        this.model && this.bind(this.model);
    }
...

And the cherry on the pie is updated class "App::bind" method to run all that beast:

JavaScript
class App {
...
    // there we will keep stuff related to databinding
    bind(formModel) {
        this.form.setModel(formModel);
    }
...

If everything was right, there will be a form with two inputs. Typing in the first input would update the second input with the base64 encoded string from the first input. The first input at the beginning will have "initial value" as the initial value.

The form will show the current time. Something like this.

Now it is time to think about the made job and make some conclusions:

  1. Two-way data binding in pure JavaScript is possible.
  2. If data binding is based on getters/setters, there will be a lot of them.
  3. Every field would require at least two event listeners for two-way data binding.
  4. There should be a good implementation for the Observer design pattern.
  5. Code could become messy, it is good to set some rules into the code structure to help future developers to not mess with the implementation.
  6. Adjusting two-way data binding feels like extra work with tension to extract repeating parts into a separate library. But how hard is it to try extract that would give less effective library versus just keep binding commands within the one place in the component.
  7. Having good data binding will help to decouple the view from the model.

And if we talk about decoupling. There is my regret part. Where the model is coupled with the view. It was done just to focus on data binding commands. For the most patient one that passed all that way till the end. Here is an example of how to decouple the model from the view.

Let's make some code cleaning tasks in the class FormView. Remove that last part with event handler in the constructor, setModel and syncModel methods. Then clean up the constructor. And update setModel and bind method with the code from below:

JavaScript
class FormView {
    constructor(selector) {
        this.el = utils.el(selector);
    }
...
    setModel(model) {
        this.unbind();
        if (!model) {
            return;
        }
        this.setName(model.prop('name'));
        this.setOutput(model.prop('output'));
        model && this.bind(model);
    }
...
    bind(model) {
        // update data from DOM to model
        this.onInputNameRemove = utils.on(this.el, '.name', 'input', 
                                 () => model.prop('name', this.getName()));
        this.onInputOutputRemove = utils.on(this.el, '.output', 'input', 
                                   () => model.prop('output', this.getOutput()));
        // update data from model to DOM
        this.syncNameRemove = model.on('change:name', () => this.setName(model.prop('name')));
        this.syncOutputRemove = model.on('change:output', 
                                () => this.setOutput(model.prop('output')));
        this.syncCurrentTimeRemove = model.on('change:time', 
                                     () => this.setCurrentTime(model.prop('time')));
    }
...

Now the form is less coupled with the model. And all data binding commands are consolidated within the only bind method.

Here is the final result in jsfiddle.

History

  • 12th April, 2020: Initial version

License

This article, along with any associated source code and files, is licensed under The MIT License