Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Vanilla JavaScript Data Binding

Observables

You don't really need a framework or fancy cutting-edge JavaScript features to do two-way data binding. Let's start basic - first and foremost, you need a way to tell when data changes. Traditionally, this is done via an Observer pattern, but a full-blown implementation of that is a little clunky for nice, lightweight JavaScript. So, if native getters/setters are out, the only mechanism we have are accessors:

var n = 5;
function getN() { return n; }
function setN(newN) { n = newN; }

console.log(getN()); // 5
setN(10);
console.log(getN()); // 10

That's a little boring though, let's rearrange that to just a single function:

var _n = 5;
function n(n) {
  if (arguments.length) _n = n;
  return _n;
}

console.log(n()); // 5
n(10);
console.log(n()); // 10

Now, suppose we want to notify stuff when the value changes...

var _n = 5, _nListeners = [];
function n(n) {
  if (arguments.length && n !== _n) {
    _n = n;
    _nListeners.forEach(function(listener) { listener(n); });
  }
  return _n;
}
n.subscribe = function(listener) { _nListeners.push(listener); }

console.log(n()); // 5
n.subscribe(function(newN) { console.log(newN); });
n(10); // logs 10
n(10); // no output, value didn't change.

Note that we don't notify subscribers that the value changed, if the value didn't actually change!

That's a real pain in the ass if we want to be doing that a lot, so let's wrap it up in a neat little generator function:

function observable(value) {
  var listeners = [];

  function notify(newValue) {
    listeners.forEach(function(listener){ listener(newValue); });
  }

  function accessor(newValue) {
    if (arguments.length && newValue !== value) {
      value = newValue;
      notify(newValue);
    }
    return value;
  }

  accessor.subscribe = function(listener) { listeners.push(listener); };

  return accessor;
}

var n = observable(5);
n.subscribe(function(newN) { console.log(newN); });
n(10); // logs 10

Cool! Now we're getting somewhere! Using observable(), we can now have as many little pre-packaged observable values as we want!

The next step is learning how to combine them. Suppose we want to do some basic math and add two observables together. We can't just do c(a() + b()), because c won't know when a or b change - it will get set once, but that's it. What we need to do is subscribe to changes on a or b, so that when either of them change, we update c:

var a = observable(3), b = observable(2);

var c = observable(a() + b());

a.subscribe(function(){ c(a() + b()); });
b.subscribe(function(){ c(a() + b()); });

console.log(c()); // 5
a(10);
console.log(c()); // 12
b(7);
console.log(c()); // 17

Now, if you're feeling clever, you'll notice that there's a lot of repetition going on there. We can fix that by pulling out functions:

var a = observable(3), b = observable(2);

function calculation() { return a() + b(); }

var c = observable(calculation());

function listener() { c(calculation()); }
a.subscribe(listener);
b.subscribe(listener);

As it happens, updating dependent values this way turns out to be very common. In fact, it's really the same thing as normal JavaScript operations, except in an observable way. Wouldn't it be cool if we could automatically set up those subscriptions?

Let's start by wrapping up the boilerplate above. We need a way to calculate the value of the observable, and we need to know the observables that participate in that calculation. We'll call this variation on an observable a computed value:

function computed(calculation, dependencies) {
  // start with the initial value
  var value = observable(calculation());

  // register a listener for each dependency, that updates the value
  function listener() { value(calculation()); }
  dependencies.forEach(function(dependency) {
    dependency.subscribe(listener);
  });

  // now, wrap the value so that users of computed() can't manually update the value
  function getter() { return value(); }
  getter.subscribe = value.subscribe;

  return getter;
}

Note that we wrap the observable that we're calculating the value of in a read-only version. Because what would it mean for code to manually set the value of a calculation? If you literally say that, for example, c is the sum of a + b, it really doesn't make much sense to come along later and set c to five. What happens if a or b update? They'd overwrite that value anyways. Therefore, we avoid any confusion by returning a read-only accessor.

Let's put this to use:

var a = observable(3), b = observable(2);
var c = computed(function(){ return a() + b(); }, [a, b]);

console.log(c()); // 5
a(10);
console.log(c()); // 12
b(7);
console.log(c()); // 17

Woo hoo! Now we're chugging!

Binding

The second big hurdle we need to clear is data binding. Frameworks like Angular and React tackle this in big, complex, scary ways. I don't know about you, but I don't like scary.

Let's continue with the adding example, but now let's represent it with text boxes:

<input type="text" id="a-text">
+
<input type="text" id="b-text">
=
<input type="text" id="c-text" readonly>

The first challenge is how to get our observables into the text boxes. Turns out, that's pretty easy (assuming a, b, c from above):

var aText = document.getElementById('a-text');
aText.value = a();
a.subscribe(function(_a){ aText.value = _a; });

var bText = document.getElementById('b-text');
bText.value = b();
b.subscribe(function(_b){ bText.value = _b; });

var cText = document.getElementById('c-text');
cText.value = c();
c.subscribe(function(_c){ cText.value = _c; });

Sigh again with the repetition. Let's clean that up:

function bindValue(input, observable) {
  input.value = observable();
  observable.subscribe(function(){ input.value = observable(); });
}

bindValue(aText, a);
bindValue(bText, b);
bindValue(cText, c);

Much better! The second half of the problem is updating our values when the text boxes change. That's pretty easy too, actually. All we need to do is listen to events on the input, and update the observable accordingly. Let's just update our bindValue function:

function bindValue(input, observable) {
  input.value = observable();
  observable.subscribe(function(){ input.value = observable(); });

  input.addEventListener('input', function() {
    observable(input.value);
  });
}

Now, whenever the textbox value changes, we'll update the observable, and when the observable changes, we update the textbox. We have lift-off!

Well, actually, only in theory. We need to make a slight adjustment to our bindValue function for this particular example - we've been doing math with integers, but text box values are strings. Adding two strings concatenates them, it doesn't add their numeric values!

Option one is to just force the value to an integer, but of course, that limits our function to ONLY working with integers, and that's not very much fun. Instead, let's do a little sniffing to figure out what we want to do...

function bindValue(input, observable) {
  var initial = observable();
  input.value = initial;
  observable.subscribe(function(){ input.value = observable(); });

  var converter = function(v) { return v; };
  if (typeof initial == 'number') {
    converter = function(n){ return isNaN(n = parseFloat(n)) ? 0 : n; };
  }

  input.addEventListener('input', function() {
    observable(converter(input.value));
  });
}

Now, if the initial value of the observable is a number, we try to interpret further values of the input as a number as well.

You can see a working example of the observables and bindings, as well as a complete listing of the above code on JSBin

Conclusion

As you can see, we were able to implement a proof-of-concept two-way data-binding example using nothing but vanilla JavaScript, that's compatible all the way back to IE 6!

Not only is our solution lightweight, with the whole "framework" being less than 50 lines total, but using it is very nearly as easy and expressive as its non-observable counterparts.

In fact, it turns out you can take this simple implementation very, very far. I'll leave it as an exercise to the reader to implement, but using this code as a base, you can:

  • Automatically detect referenced observables in computeds
  • Manage entire observable arrays and objects containing other observables
  • Have properly managed cyclic dependencies between computeds
  • Implement many many many differnet bindings from observables to DOM, not just input values
  • Declare bindings as data attributes directly in the HTML, and apply them wholesale
  • and much, much more.

How do I know you can do this? Because this is exactly the same approach that Steve Sanderson's (amazing, excellent, stupdendous) Knockout library takes. What I listed here - observables, computeds, and DOM bindings, are the core foundations of Knockout. What I love most about it though, is that I can summarize how Knockout works in 50 lines of code, by iteration on a very simple idea.

For some reference, here's what this whole example looks like in Knockout:

<input type="text" data-bind="numericValue: a">
+
<input type="text" data-bind="numericValue: b">
=
<input type="text" data-bind="numericValue: a() + b()" readonly>

<script>
ko.bindingHandlers.numericValue = {
  init: function(element, valueAccessor) {
    var observable = valueAccessor();
    element.addEventListener('input', function() {
      var v = element.value;
      observable(isNaN(v = parseFloat(v)) ? 0 : v);
    });
  },
  update: function(element, valueAccessor) {
    element.value = ko.unwrap(valueAccessor());
  }
};

ko.applyBindings({
  a: ko.observable(3),
  b: ko.observable(2)
});
</script>

Note that while Knockout does supply a 'value' binding out of the box, it doesn't attempt to parse numbers like we need it to for this example. So, as is the Knockout way, we make our own binding to handle that case.

@pjdevries

This comment has been minimized.

Copy link

pjdevries commented Dec 11, 2015

Thanx for this nicely written, understandable data binding tutorial. It took me a couple of reads to understand all that's going on, but I think I do now. Not only cool to see how simple data binding can be, but also a fine insight in the strength and flexibility of JavaScript in general.

@brycepj

This comment has been minimized.

Copy link

brycepj commented Jan 11, 2016

Thanks for writing this. Very clear, straightforward, and well-written!

@psypersky

This comment has been minimized.

Copy link

psypersky commented Feb 24, 2016

Awesome! you deserve a medal

@maraisr

This comment has been minimized.

Copy link

maraisr commented Mar 2, 2016

Love it mate - nice and simple!

@katopz

This comment has been minimized.

Copy link

katopz commented Apr 11, 2016

This longest gist I ever read. Pretty cool!

@abacaj

This comment has been minimized.

Copy link

abacaj commented Apr 12, 2016

This is good, make it longer and a blog post - you got a hit.

@steveshore

This comment has been minimized.

Copy link

steveshore commented Feb 8, 2017

How do you handle event listeners? Would you use the data-bind attributes like they do?

@mdublin

This comment has been minimized.

Copy link

mdublin commented Mar 9, 2017

Very cool

@SaleRise

This comment has been minimized.

Copy link

SaleRise commented Sep 10, 2017

I really like this approach which is unlike others where people use time polling and the likes and then update the object.

I am wondering how will it work in observables are on the property in the data object like:

data_obj = {
  a: 4,
  b: 5
}

var e = observable(data_obj.a), f = observable(data_obj.b);
var c = computed(function(){ return e() + f(); }, [e, f]);
var uptodate = observable(data_obj);
setInterval(function() { 
  console.log(e() +" "+ f());
  console.log(uptodate());
                       }, 2000);

How can we get up-to-date value of the data object, ofcourse this data object can be complex with nested object?

Thanks for the really helpful share!

@easymagic

This comment has been minimized.

Copy link

easymagic commented Dec 26, 2017

Great article.

@nkev

This comment has been minimized.

Copy link

nkev commented Jan 10, 2018

Beautifully worded article. 🥇 More please!

@sreid70

This comment has been minimized.

Copy link

sreid70 commented Mar 23, 2018

Thanks. I've been using frameworks like Angular and Vue and I do like them. I think they've really helped me structure my code a little better. Unfortunately, I start to use them as a crutch and stop learning how to do things in plain old JS. JS has come a long way and I know I should stop reaching for a framework every time I start a project. Thanks for the great introduction to observables and binding.

@planetarydev

This comment has been minimized.

Copy link

planetarydev commented Jun 17, 2018

Thanks for this nice and simple understanding article.

@timmd909

This comment has been minimized.

Copy link

timmd909 commented Sep 10, 2018

Hope you're doing well, Austin! I'm glad my memory bookmarked this page. Shooting this link to explain KO saves a lot of time.

@easymagic

This comment has been minimized.

Copy link

easymagic commented Sep 21, 2018

Great Article, Thumbs Up.

@kostasx

This comment has been minimized.

Copy link

kostasx commented Apr 18, 2020

Excellent read! You just laid out the foundations of almost every frontend framework out there. Congratulations and thank you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.