Dataview.js

Dataview.js is a JavaScript library connecting data objects to selected views. It helps you write more consistent applications where nothing happens by accident, because you simply decide where and what can modify your data. Dataview.js is distributed under MIT license.

Author: Damian 'ferrante' Wielgosik (@varjs)

Why?

When developing in JavaScript we usually deal with smaller or larger JSON data objects like the following one:

var Article = {
    title: "Something very interesting happend in America"
    author: {
        firstname: "John",
        lastname: "Doe"
    },
    activated: true,
    published: "12/10/2010 10:00",
    categories: [1, 10, 211]
};

What happens very often is we change the data properties using different events or even whole views in a very unordered way:

$("#firstname").change(function() {
    Article.author.firstname = $(this).val();
});

$("#name").picker(function(firstname, lastname) {
    $("#firstname").val(firstname);
    $("#lasttname").val(lastname);

    Article.author.firstname = firstname;
    Article.author.lastname = lastname;
});

There are also situations when a special event is needed to keep app modules synchronized (mainly various views when you have to update UI elements):

$("#name").picker(function(firstname, lastname) {
    $("#firstname").val(firstname);
    $("#lasttname").val(lastname);

    Article.author.firstname = firstname;
    Article.author.lastname = lastname;

    System.trigger("firstnameChanged", firstname);
    System.trigger("lastnameChanged", lastname);
});

This is just one of many examples showing that the same data can be modified by different functions wherever a developer wishes to. This approach can confuse people from your team, produce a lot of unnecessary code and slow down debugging rounds (where have you set this property?!). Dataview.js tries to standarize the process of data manipulation inside JavaScript applications saving time you usually have to spend on writing event-based solutions or similar.

Quickstart

Consider the following data:

{
    name: "John Doe",
    arr: [1, 2, 3],
    obj: { 
        foo: "bar" 
    }
}

Create a new Dataview.js object using above data as a parameter:

var data = dataview({
    name: "John Doe",
    arr: [1, 2, 3],
    obj: { 
        foo: "bar" 
    }
});

Create a view:

var view = {
    updateName: function () {
        // [...]
        this.update("Leonardo da Vinci");
    },
    removeFromArr: function () {
        // [...]
        this.remove(1);
    },
    addToObj: function () {
        // [...]
        this.add("key", "value");
    }
};

Connect the view to data on selected properties..

data.connect(view).on({
    name: "updateName", // view.updateName can now modify 'name'
    arr: "removeFromArr", // view.removeFromArr can now modify 'arr'
    obj: "addToObj" // view.addToObj can now modify 'obj'
});

.. or this way:

data.connect(view).on("name", "updateName").on("arr", "removeFromArr").on("obj", "addToObj");

As you can see the grammar sounds more or less like that:

dataviewObject.connect(viewObject).on({
    foo : "methodName"
});
// Connect dataviewObject to viewObject on foo property so viewObject[methodName] can modify dataviewObject[foo]

Now you can use view methods wherever you want:

view.updateName(); // sets name to 'Leonardo da Vinci'
view.removeFromArr(); // removes second element from the arr
view.addToObj();// adds 'key => value' to obj

Notice the changes in your data:

{
    name: "Leonardo da Vinci",
    arr: [1, 3],
    obj: {
        foo: "bar",
        key: "value"
    }
}

Follow them using callbacks:

data.onRemove({
    arr: function (index) {
        console.log("Removed from array. Removed at index: ", index)
    }
});
view.removeFromArr(); 
// Removed from array. Removed at index: 1

Want to add/update/remove a property without using the view methods? No problem:

data.update("arr", 1, "new value"); // arr: [1, "new value", 3]
data.remove("arr", 1); // arr: [1, 3]
data.add("arr", "new value"); // arr: [1, 3, "new value"]

You can lock the properties from the connection so they can be modified only by methods you defined.

data.connect(view).on({
    name: "updateName",
    arr: "removeFromArr",
    obj: "addToObj"
}).lock();

Now, no one can connect new methods to the properties you locked:

data.connect(view).on({
    name: "somethingElse"
});
// "Couldn't connect new methods. Locked property: name"

The following won't work either since name/arr/obj properties are locked:

data.update("arr", 1, "new value");

You can also freeze the object in two modes: normal and strict.

data.freeze(); // normal
data.freeze(true); // strict

First one blocks any attempts to connect new views:

data.connect({foo: function () {}}).on({
    "name": "foo"
});
// ERROR: Couldn't connect a new view. Data object is freezed!

Latter also disables add/update/remove methods used directly from the Dataview object:

data.update("name", "Very nice name");
// ERROR: Object is freezed!

It's a really useful option — your data can be modified only by the views you have connected! No more mess inside JS applications!

By the way - you can work with nested properties too! Let's have data like that:

var data = dataview({
    obj: {
        foo: {
            bar: 100
        }
    }
});

And a simple view:

var view = {
    changeBar: function (value) { this.update(value); }
};

Now it's time to connect them on the property obj.foo.bar:

data.connect(view).on("obj.foo.bar", "changeBar");

Let's see if changeBar() really modifies the right property:

view.changeBar(1337);
data.get("obj.foo.bar"); // 1337

Yes, the data object looks like following right now:

{
    obj: {
        foo: {
            bar: 1337
        }
    }
}

As you probably noticed, you can useget() to have property's value:

data.get("obj.foo.bar"); // 1337

API

get

Returns the property's value.

var data = dataview({ foo : "bar" });
data.get(); // { foo : "bar" }
var data = dataview({ foo : "bar" });
data.get("foo"); // "bar"
var data = dataview({ 
    foo : {
        bar : 1
    }
});
data.get("foo.bar"); // 1

connect

Connects any view to the Dataview object. The system requires a view to be an object, otherwise throws an error.

var data = dataview({ foo : "bar" });
var view = { 
    changeFoo : function () {}
};
data.connect(view);

connect(view).on

Binds properties to the view methods. Therefore, view methods are able to modify these properties by themselves.

data.connect(view).on("foo", "changeFoo");
data.connect(view).on(["foo", "bar"], "changeFooAndBar");
data.connect(view).on("foo", ["changeFoo", "changeFooAgain"]);
data.connect(view).on(["foo", "bar"], ["changeFooAndBar", "changeBarAndFoo"]);
data.connect(view).on({
    foo : "changeFoo",
    bar : "changeBar"
});
data.connect(view).on({
    foo : ["changeFoo", "changeFooAgain"],
    bar : "changeBar"
});

When the view connected, you can use add, update, remove functions inside view methods used with on. Their context objects this will contain the proper values. For example:

changeFooAgain : function () {
    this.add("new value");
    this.update("updated value");
    this.remove();
}

Thus, you can let view methods modify your data properties without direct references.

connect().on().lock

"Locks" the properties so they can be modified only by view methods defined in a connection.

var data = dataview({ foo : "bar" });
var view = { 
    changeFoo : function () {}
};
data.connect(view).on("foo", "changeFoo").lock();

Having above foo property cannot be modified by other views or methods e.g.:

data.connect(anotherView).on("foo", "changeFoo");
// "Couldn't connect new methods. Locked property: foo"

You either can't change foo property directly from the data object:

data.update("foo", "new value");
data.get("foo"); // bar

freeze

Freezes a data object in two modes: normal and strict. Normal mode blocks any attempts to connect new views. Strict mode does the same and what's more it disables data methods: add/update/remove.

data.freeze();
data.connect(view).on("foo", "changeFoo");
// Couldn't connect a new view. Data object is freezed!

Trying strict mode:

data.freeze(true);
data.connect(view).on("foo", "changeFoo");
// Couldn't connect a new view. Data object is freezed!
data.freeze(true);
data.add(1);
// Couldn't modify the property. Data object is freezed!

dataview().add

Adds new values to the properties if they are not locked.

var data = dataview({ foo : [] });
    
data.add("foo", "new value");
data.get("foo").pop(); // "new value"

Example above will add a "new value" string at the end of the array named foo.

It's easy to put a value at given index or key:

var data = dataview({ foo : [1, 2, 3, 4] });
    
data.add("foo", 1, "new value");
data.get("foo"); // [1,"new value",2,3,4]

add function works with objects too:

var data = dataview({ foo : { } });
    
data.add("foo", "newkey", "new value");
data.get("foo.newkey"); // "new value"

You can also add something to a few properties at once:

var data = dataview({ foo : {}, bar : {} });
    
data.add(["foo", "bar"], "newkey", "new value");
data.get("foo.newkey"); // "new value"
data.get("bar.newkey"); // "new value"

view.add

Adds new values to the properties.

To use add from a view method, you have to connect the view to dataview object on some property:

var data = dataview({ foo : [] });
var view = { 
    add1337ToFoo : function () {
        this.add(1337)
    }
};
data.connect(view).on("foo", "add1337ToFoo");

Now you have view.add1337ToFoo() which can add 1337 number to the foo array (as you binded the function to that property)

view.add1337ToFoo();
data.get("foo").pop(); // 1337

This function works exactly the same as dataview.add, except you don't have to pass the property name since it is already binded. To be sure it really works, let's add a value to the array at some index:

var data = dataview({ foo : [] });
var view = { 
    addToFoo : function () {
        this.add(10, "new value");
    }
};
data.connect(view).on("foo", "addToFoo");
view.addToFoo();
data.get("foo")[10]; // "new value"

dataview().update

Updates the properties (if they are not locked or the object is not freezed in a strict mode) to given values.

var data = dataview({ foo : [] });

data.update("foo", "updated value");
data.get("foo"); // "updated value"

Example above will replace the array named foo with a new value "updated value".

Want to update a property at given key or index?

var data = dataview({ foo : [1, 2, 3, 4] });

data.update("foo", 1, "updated value");
data.get("foo")[1]; // "updated value"

update function works with objects too:

var data = dataview({ 
    foo : { 
        bar : 1
    } 
});

data.update("foo", "bar", "updated value");
data.get("foo.bar"); // "updated value"

You can also update a few properties at once:

var data = dataview({ foo : 1, bar : 2 });

data.update(["foo", "bar"], "updated value");
data.get("foo"); // "updated value"
data.get("bar"); // "updated value"

Note that to update an object, the key must exists:

var data = dataview({ foo : 1, bar : 2 });

data.update("someCoolKey", "updated value");
data.get("someCoolKey"); // undefined

It also works with nested properties:

var data = dataview({ 
    foo : { 
        bar : {
            baz: 1
        }
    } 
});

data.update("foo.bar.baz", "updated value");
data.get("foo.bar.baz"); // "updated value"

view.update

Updates the properties directly from the view method.

To use it you have to connect the view to dataview object "on" some property:

var data = dataview({ foo : [] });
var view = { 
    updateFoo : function () {
        this.update(1337);
    }
};
data.connect(view).on("foo", "updateFoo");

Now you have view.updateFoo() which can update foo to 1337.

view.updateFoo();
data.get("foo"); // 1337

This function works exactly the same as dataview.update, except you don't have to pass the property name as it is already binded. To be sure it really works, let's update a property at some index:

var data = dataview({ foo : ["old value"] });
var view = { 
    updateFoo : function () {
        this.update(1, "new value");
    }
};
data.connect(view).on("foo", "updateFoo");
view.updateFoo();
data.get("foo")[1]; // "new value"

dataview().remove

Removes the properties if they are not locked or the object is not freezed in a strict mode.

var data = dataview({ foo : [] });

data.remove("foo");
data.get("foo"); // undefined

Example above will completely remove the array named foo.

remove method can also delete the elements by passing the keys or indexes:

var data = dataview({ foo : [1, 2, 3, 4] });

data.remove("foo", 1); // removes the element from foo array at index 1
data.get("foo").length; // 3

As you can see, we removed the element at index 1, so there are 3 array elements left.

remove function works with objects too:

var data = dataview({ 
    foo : { 
        bar : 1
    } 
});

data.remove("foo", "bar");
data.get("foo.bar"); // undefined

You can remove a few properties at once:

var data = dataview({ foo : 1, bar : 2 });

data.remove(["foo", "bar"]);
data.get("foo"); // undefined
data.get("bar"); // undefined

It also works with nested properties:

var data = dataview({ 
    foo : { 
        bar : {
            baz: 1
        }
    } 
});

data.remove("foo.bar.baz");
data.get("foo.bar.baz"); // undefined

view.remove

Removes the properties directly from the view method.

To use it you have to connect the view with dataview object on some property:

var data = dataview({ foo : [] });
var view = { 
    removeFoo : function () {
        this.remove();
    }
};
data.connect(view).on("foo", "removeFoo");

Now you have view.removeFoo() which can remove foo property.

view.removeFoo();
data.get("foo"); // undefined

This function works exactly the same as dataview.remove, except you don't have to pass the property name as it is already binded. To be sure it really works, let's remove a property at some index:

var data = dataview({ foo : ["old value"] });
var view = { 
    removeFoo : function () {
        this.remove(1);
    }
};
data.connect(view).on("foo", "removeFoo");
view.removeFoo();
data.get("foo")[1]; // undefined
data.get("foo").length; // 0

onAdd

Adds the callbacks that are fired when something is added to the properties defined as keys.

data.onAdd({
    foo : function (key, value) {
        console.log("added", value, "at", key);
    }
});
data.add("foo", 2011, "Falsy Values Conference");
// "added Falsy Values Conference at 2011"

It works with view methods:

var data = dataview({ foo : [] });
var view = { 
    addToFoo : function () {
        this.add("new value");
    }
};
data.connect(view).on("foo", "addToFoo");
data.onAdd({
    foo : function (key, value) {
        console.log("added", value, "at index", key);
    }
});
view.addToFoo();
// "added new value at index 0"

onUpdate

Adds the callbacks that are fired when properties defined as keys are updated.

data.onUpdate({
    foo : function (key, value) {
        console.log("updated with", value, "at", key);
    }
});
data.update("foo", 2011, "Front-Trends Conference");
// "updated with Front-Trends Conference at 2011"

It works with view methods:

var data = dataview({ foo : [] });
var view = { 
    updateFoo : function () {
        this.update("new value");
    }
};
data.connect(view).on("foo", "updateFoo");
data.onUpdate({
    foo : function (idx, value) {
        console.log("updated with", value, "at", idx);
    }
});
view.updateFoo();
// "updated with new value at undefined"

onRemove

Adds the callbacks that are fired when properties defined as keys are removed or their elements are removed.

data.onRemove({
    Flash : function () {
        console.log("removed whole property");
    }
});
data.remove("Flash");
// "removed whole property"

It works with view methods:

var data = dataview({ foo : [1, 2, 3] });
var view = { 
    removeFromFoo : function () {
        this.remove(1);
    }
};

data.connect(view).on("foo", "removeFromFoo");
data.onRemove({
    foo : function (idx) {
        console.log("removed at index", idx);
    }
});

view.removeFromFoo();
// "removed at index 1"