Click here to Skip to main content
15,895,799 members
Articles / Web Development / HTML

jQuery-ko based "Component" definitions (pushing forward MVVM)

Rate me:
Please Sign up or sign in to vote.
4.83/5 (4 votes)
21 Jul 2014CPOL7 min read 14.3K   50   6  
Have a greater experience with MVVM by developing reusable and nested jQuery-ko based "Component" definitions

Introduction

A Component definition is a javascript function which when called with new word instantiates a javascript object (the viewModel) and also defines and inserts the ko-synchronized view (html) into the document (web page).

But not only this, Component definitions must be able to be used (instantiated) alone or nested in other Component definitions. In this way we will always end up with one Component definition per page, but which will make use (probably) of a lot of other nested Component definitions.

A simple web page with labels and text-boxes

Next image shows you the final page we are going to obtain.

Image 1

As the image shows in the debugger-chrome part you see that page code is basically a series of javascript script source file list (where different Component definitions are contained) and apply ko.applyBindings() function to an instance of one of the Component definitions.

Next image shows you how this web page has been obtained in relation to the Components being defined.

Image 2

As you see from the image Component definitions are nested in a tree structural way.

When instantiating CPageLabelsAndTextBoxes Component we obtain view corresponding to the whole page (blue, pink and green in the image). This is so because CPageLabelsAndTextBoxes uses (instantiates) in its inside (in its definition) another Component, CLabelsAndTextBoxes2, which it uses (instantiates) in its inside (in its definition) another Component (twice), CLabelsAndTextBoxes1.

In blue we see the part of the whole view (the page, inserted into document because of instantiation of CPageLabelsAndTextBoxes) that it has been defined or coded in CPageLabelsAndTextBoxes definition. In gpink we see the part of the view that it is defined (coded) in CLabelsAndTextBoxes2. In green we see the parts of the view that its definition is contained in CLabelsAndTextBoxes1 code.

Now we are ready to take a look at how a Component definition looks like (the code).

CLabelsAndTextBoxes1.js

JavaScript
function CLabelsAndTextBoxes1(prefix,$c,data){
    // html
    $c.html(''+
        '<p>set '+data+' <input data-bind="value:'+prefix+'a"></p>'+
        '<p>'+data+' is <span data-bind="text:'+prefix+'a"></span></p>'+
    '');
    // js
    this.a=ko.observable("");  
    // css  
}

There are three sections: html, js and css. The html section defines and inserts the view into the document (web page) in a specific point of it (as content of $c, wich happens to be a jQuery element of the page).

The js section defines the viewModel or javascript object ko-synchronized with the view and adds any code to add behaviour, process data, share data between sibling Component definitions instances, ...

The css section applies style to the view inserted into the document using as we will see jQuery with a context parameter, the one given by $c and class attributes, never id's).

CLabelsAndTextBoxes2.js

JavaScript
function CLabelsAndTextBoxes2(prefix,$c,data){
    // html
    $c.html(''+
        '<p>set '+data[0]+' <input data-bind="value:'+prefix+'a"></p>'+
        '<p>'+data[0]+' is <span data-bind="text:'+prefix+'a"></span></p>'+
        '<div class="c1"></div>'+
        '<div class="c2"></div>'+
    '');
    // js
    var c1="c1";
    var c2="c2";
    var $c1=$("."+c1,$c);
    var $c2=$("."+c2,$c);
    this[c1]=new CLabelsAndTextBoxes1(prefix+c1+".",$c1,data[1]);
    this[c2]=new CLabelsAndTextBoxes1(prefix+c2+".",$c2,data[2]);
    this[c2].a=this[c1].a;
    this.a=ko.observable("");
    // css
}

The interesting thing of this Component definition is that it uses first Component twice. To do it it defines in its view places to insert the views from first Component defined and in the js section it instantiates the first Component defined (twice) passing in each case as parameter the point of the view to where insert those other views. But not only this, after instantiation, because these instances belongs to the viewModel it defines it makes a relation between properties of the viewModels (siblings) just instantiated, meanning in this case that labels in green will always have the same value (data) in the web page.

This last point it is very interesting and we will make use of it also in the next case study. You must do things like this when you want to share data between two (or more) sibling instances of different (or the same) Component definitions.

CPageLabelsAndTextBoxes.js

JavaScript
function CPageLabelsAndTextBoxes(prefix,$c,data){
    // html
    $c.html(''+
        '<p>set '+data[0]+' <input data-bind="value:'+prefix+'a"></p>'+
        '<p>'+data[0]+' is <span data-bind="text:'+prefix+'a"></span></p>'+
        '<div class="c1"></div>'+
    '');
    // js
    var c1="c1";
    var $c1=$("."+c1,$c);
    this[c1]=new CLabelsAndTextBoxes2(prefix+c1+".",$c1,data[1]);
    this.a=ko.observable("");
    // css
}

This last Component definition is the one that we will use to instantiate and obtain the viewModel of the page (and also the view or page itself). Then we will apply ko.applyBindings() function over it.

labelsAndTextBoxes.htm

HTML
<!doctype html>
<html>
<head>
<title>CPageLabelsAndTextBoxes "Component"</title>
<script src="http://cdnjs.cloudflare.com/ajax/libs/knockout/3.1.0/knockout-min.js"></script>
<script src="http://code.jquery.com/jquery-1.11.0.min.js"></script>
<script src="CLabelsAndTextBoxes1.js"></script>
<script src="CLabelsAndTextBoxes2.js"></script>
<script src="CPageLabelsAndTextBoxes.js"></script>
<script>
$(document).ready(function(){
    ko.applyBindings(new CPageLabelsAndTextBoxes("",$("#page"),["a",["b","cd","dc"]]));
});
</script>
</head>
<body>
<div id="page"></div>
</body>
</html>

The code for the web page or html document has been comented before. Important thing to note is order in which javascript source files are listed. Obviously, if CB Component definition depends on CA Component definition, then javascript source file for CA Component definition must be listed before javascript source file for CB Component definition.

As you can see, Compoent definitions have external dependencies, which are the two javascript frameworks (jQuery and ko) and other Component definitions. All this dependencies must be included as a javascript source file listed in the header section of the page or html document.

Model or data we pass to the Component we instantiate in the page must have the format this Component expects, which will depend on its definition and on all the other Component definitions it uses nested in a tree hierarchical manner.

This means at the end we will have a unique case of MVVM, that is, Model View ViewModel, but the interesting thing it is how this unique case of MVVM for the whole page has been obtained through reusable pieces of software that can be used alone or nested in a tree hierarchical way.

Master and Detail case

Next image shows the final result (page) we are going to obtain.

Image 3

It is a master and detail case.

Next it is the data we are going to use for it.

data.js

JavaScript
var data=[
    {
        cols:[
            {value:ko.observable("seat")},
            {value:ko.observable("leon")},
            {value:ko.observable("Barcelona Motors")},
            {value:ko.observable("13500 €")},
            {value:ko.observable("200 km/h")},
            {value:ko.observable("120 hp")},
            {value:ko.observable("silver")}
        ]
    },
    {
        cols:[
            {value:ko.observable("ford")},
            {value:ko.observable("ka")},
            {value:ko.observable("Best Cards 4U")},
            {value:ko.observable("10500 €")},
            {value:ko.observable("160 km/h")},
            {value:ko.observable("70 hp")},
            {value:ko.observable("red ")}
        ]
    },
    {
        cols:[
            {value:ko.observable("bmw")},
            {value:ko.observable("320i")},
            {value:ko.observable("Import and Export")},
            {value:ko.observable("18500 €")},
            {value:ko.observable("220 km/h")},
            {value:ko.observable("180 hp")},
            {value:ko.observable("green ")}
        ]
    },
    {
        cols:[
            {value:ko.observable("volkswagen")},
            {value:ko.observable("golf")},
            {value:ko.observable("Barcelona Auto")},
            {value:ko.observable("20500 €")},
            {value:ko.observable("220 km/h")},
            {value:ko.observable("150 hp")},
            {value:ko.observable("white candy")}
        ]
    }
];

As you see, for each row of data we have to, through the Component definitions programming, make them distribute in two kinds of grids, one the master that only shows some of the data per row, and the detail grid which will show all the data per row.

First I show you the general or most basic grid Component definition, which we will use it to show the detail grid.

CGrid.js

JavaScript
function CGrid(prefix,$c,data){
    // html 
    $c.html(''+
        '<div class="grid" data-bind="foreach:'+prefix+'rows">'+
            '<div class="row" data-bind="foreach:cols">'+
                '<div class="col" data-bind="text:value"></div>'+
                '<div class="col-sep" data-bind="visible:!isLast()">&nbsp;</div>'+
            '</div>'+
            '<div class="clear"></div>'+
        '</div>'+
    '');
    // js 
    this.rows=data;
    this.rows.forEach(function(r){
        var numCols=r.cols.length; 
        r.cols.forEach(function(c,i){
            c.isLast=function(){
                return i===numCols-1;
            };
        });
    });
    // css
    $(".grid",$c).css({
        "overflow":"hidden"
    });
    $(".clear",$c).css({
        "clear":"both"
    });
    $(".row",$c).css({
        "float":"left",
        "border-radius":"16px",
        "border":"1px solid red",
        "background-color":"#cfcfcf"
    });
    $(".col",$c).css({
        "float":"left",
        "width":"90px",
        "white-space":"nowrap",
        "overflow-x":"auto",
        "margin":"1px 9px",
        "text-align":"center"
    });
    $(".col-sep",$c).css({
        "float":"left",
        "width":"2px",
        "border-radius":"4px",
        "background-color":"black"
    });
    $c.css({
        "font-family":"sans-serif"
    });
}

What this Component definition does it is to accept data in a specific format and show it in a normal grid. When instantiating the Component we have the view defined, rendered and stylized and the viewModel or javascript object also instantiated and with the corresponding ko bindings with the view ready to be activated.

Next I show you the Component for the grid with radio buttons, the once used in this case for the master.

CGridWithRadioButtons.js

JavaScript
function CGridWithRadioButtons(prefix,$c,data){
    // html 
    $c.html(''+
        '<div class="grid" data-bind="foreach:'+prefix+'rows">'+
            '<div class="clear"></div>'+
            '<div class="radio"><input type="radio" name="select" data-bind="click:checked"></div>'+
            '<div class="row" data-bind="foreach:cols">'+
                '<div class="col" data-bind="text:value"></div>'+
                '<div class="col-sep" data-bind="visible:!isLast()">&nbsp;</div>'+
            '</div>'+
        '</div>'+
    '');
    // js 
    var self=this;
    this.rows=data;
    this.doRadio=function(){};
    this.rows.forEach(function(row,i){
        row.checked=(function(j){
            return function(){
                self.i=j;
                self.doRadio();
            }
        })(i);    
        var numCols=row.cols.length;
        row.cols.forEach(function(c,i){
            c.isLast=function(){
                return i===numCols-1;
            };
        });
    });
    // css
    $(".grid",$c).css({
        "overflow":"hidden"
    });
    $(".clear",$c).css({
        "clear":"both"
    });
    $(".radio",$c).css({
        "float":"left"
    });
    $(".row",$c).css({
        "float":"left",
        "border-radius":"16px",
        "border":"1px solid red",
        "background-color":"#cfcfcf"
    });
    $(".col",$c).css({
        "float":"left",
        "width":"90px",
        "white-space":"nowrap",
        "overflow-x":"auto",
        "margin":"1px 9px",
        "text-align":"center"
    });
    $(".col-sep",$c).css({
        "float":"left",
        "width":"2px",
        "border-radius":"4px",
        "background-color":"black"
    });
    $c.css({
        "font-family":"sans-serif"
    });
}

The special thing about this Component definition it is that, apart from rendering the radio buttons per row, it also defines a function to be executed when a radio it is selected. This function must be overwritten by Component that uses this Component.

Next we see CPageMasterDetail Component definition. This is the Component that will be instantiated to obtain a unique viewModel for the whole page (or view) that will be ko-sincronized with it. That is, the whole page will be a unique ko-view-viewModel example, but the interesting thing it is how this unique ko-view-viewModel has been constructed, by modular parts or pieces of software (Component definitions) allowing for programming and personalizing behaviour in each level of the tree

CPageMasterDetail.js

JavaScript
function CPageMasterDetail(prefix,$c,data){
    // html
    $c.html(''+
        '<div class="c1"></div>'+
        '<br>'+
        '<div class="c2" data-bind="visible:'+prefix+'selected()"></div>'+
    '');
    // js
    var c1="c1";
    var c2="c2";
    var $c1=$("."+c1,$c);
    var $c2=$("."+c2,$c);
    var self=this;
    this.data=data;
    this.dataForGridWithRadios=function(){
        var data=[];
        self.data.forEach(function(row){ 
            var row2={cols:[]};
            row.cols.forEach(function(obj,i){
                if(i>2){
                    return;
                }
                var obj2={value:obj.value};
                row2.cols.push(obj2);
            });
            data.push(row2);
        });
        return data;
    };
    this.dataForDetail=function(){
        var data=[];
        self.data[0].cols.forEach(function(obj,i){
            var row={cols:[{value:ko.observable("")}]};
            data.push(row);
        });
        return data;
    };
    this[c1]=new CGridWithRadioButtons(prefix+c1+".",$c1,this.dataForGridWithRadios());
    this[c2]=new CGrid(prefix+c2+".",$c2,this.dataForDetail())
    this[c1].doRadio=function(){
        var index=self.c1.i;
        self.data[index].cols.forEach(function(c,i){
            self.c2.rows[i].cols[0].value(c.value());
        });
        if(self.selected()!=true){
            self.selected(true);
        }
    };
    this.selected=ko.observable(false);
    // css
};

This Component definition takes data in a specific format and defines two functions to, from this data, get data for the instance of the two other Component definitions used in it, that is the CGridWithRadioButtons to show the master and the CGrid to show the detail for each row selected from the master.

Apart from defining this two functions for data processing, it also overwrites functionality from CGridWithRadioButtons instance, the one relative to the event of selecting a radio, applying the code we want, in this case to ko-update data in the detail.

Finally, we review source code for the html web page.

masterDetail.htm

HTML
<!doctype html>
<html>
<head>
<style>
/*this is to change appearence of scroll bar*/
::-webkit-scrollbar{
width:4px;
height:4px;
}
::-webkit-scrollbar-track{
background:#666666;
    -webkit-box-shadow: inset 0 0 1px rgba(0,0,0,0.3); 
    border-radius: 10px;
}
::-webkit-scrollbar-thumb{
background:#ffffff;
    border-radius: 10px;
    -webkit-box-shadow: inset 0 0 1px rgba(0,0,0,0.5); 
}
</style>
<title>CPageMasterDetail "Component"</title>
<script src="http://code.jquery.com/jquery-1.11.0.min.js"></script>
<script src="http://ajax.aspnetcdn.com/ajax/knockout/knockout-3.0.0.js"></script>
<script src="CGridWithRadioButtons.js"></script>
<script src="CGrid.js"></script>
<script src="data.js"></script>
<script src="CPageMasterDetail.js"></script>
<script>
$(document).ready(function(){
    var myViewModel=new CPageMasterDetail("",$("#page"),data);
    ko.applyBindings(myViewModel);
});
</script>
</head>
<body>
<div id="page"></div>
</body>
</html>

You see it is pretty simple and all the coding it is in the Component definitions source files.

Case study one plus case study 2, case study 3

This section is to demonstrate how a Component definition can be used alone or nested in a tree way in other Component definitions. In two previous sections we used by its own (meaning alone, not nested) CPageLabelsAndTextBoxes and CPageMasterDetail Component definitions. In this section I am going nest this two Component definitions in a new Component definition to develop a web page which is just an appending of case study one and case study 2.

Image 4

CPageLabelsAndTextBoxesMasterDetail.js

JavaScript
function CPageLabelsAndTextBoxesMasterDetail(prefix, $c, data){
    // html
    $c.html(''+
        '<div class="c1"></div>'+
        '<div class="c2"></div>'+
    '');
    //js
    var c1="c1";
    var c2="c2";
    var $c1=$("."+c1,$c);
    var $c2=$("."+c2,$c);
    this[c1]=new CPageLabelsAndTextBoxes(prefix+c1+".",$c1,data[0]);
    this[c2]=new CPageMasterDetail(prefix+c2+".",$c2,data[1]);
    // css
    $c1.css({
        "float":"left",
        "padding":"10px",
        "border":"3px dashed grey",
        "margin":"10px"
    });
    $c2.css({
        "float":"left",
        "padding":"10px",
        "border":"3px dashed grey",
        "margin":"10px"
    });
}

labelsAndTextBoxesMasterDetail.htm

JavaScript
<!doctype html>
<html>
<head>
<style>
/*this is to change appearence of scroll bar*/
::-webkit-scrollbar{
width:4px;
height:4px;
}
::-webkit-scrollbar-track{
background:#666666;
    -webkit-box-shadow: inset 0 0 1px rgba(0,0,0,0.3); 
    border-radius: 10px;
}
::-webkit-scrollbar-thumb{
background:#ffffff;
    border-radius: 10px;
    -webkit-box-shadow: inset 0 0 1px rgba(0,0,0,0.5); 
}
</style>
<title>CPageLabelsAndTextBoxesMasterDetail "Component"</title>
<script src="http://code.jquery.com/jquery-1.11.0.min.js"></script>
<script src="http://ajax.aspnetcdn.com/ajax/knockout/knockout-3.0.0.js"></script>
<script src="CGridWithRadioButtons.js"></script>
<script src="CGrid.js"></script>
<script src="data.js"></script>
<script src="CPageMasterDetail.js"></script>
<script src="CLabelsAndTextBoxes1.js"></script>
<script src="CLabelsAndTextBoxes2.js"></script>
<script src="CPageLabelsAndTextBoxes.js"></script>
<script src="CPageLabelsAndTextBoxesMasterDetail.js"></script>
<script>
$(document).ready(function(){
    var myViewModel=new CPageLabelsAndTextBoxesMasterDetail("",$("#page"),[["a",["b","cd","dc"]],data]);
    ko.applyBindings(myViewModel);
});
</script>
</head>
<body>
<div id="page"></div>
</body>
</html>

Conclusions

In this article I have showed how to put MVVM pattern one step further by developing jQuery-ko based Component definitions in a tree nested way. In this manner to develop a web page means to develop just one Component definition which will make use, probably, of many other nested Component definitions, and apply over an instance of it (over the viewModel) ko.applyBindings() function to activate all ko-bindings between views and viewModels (views and viewModels nested in a tree way to conform a unique view-viewModel case per page).

A Component definition it is a piece of software which can be used either alone or reused nested in other Component definitions.

data format for the Model will depend on Component definition instantiated in the page (and also on all the other nested Component definitions).

Sharing of data between two or more siblings in a level of the tree can be established through manipulation of javascript viewModel properties deep inside the tree.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Spain Spain
bla bla bla bla bla bla bla bla bla

Comments and Discussions

 
-- There are no messages in this forum --