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.
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.
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
function CLabelsAndTextBoxes1(prefix,$c,data){
$c.html(''+
'<p>set '+data+' <input data-bind="value:'+prefix+'a"></p>'+
'<p>'+data+' is <span data-bind="text:'+prefix+'a"></span></p>'+
'');
this.a=ko.observable("");
}
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
function CLabelsAndTextBoxes2(prefix,$c,data){
$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>'+
'');
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("");
}
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
function CPageLabelsAndTextBoxes(prefix,$c,data){
$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>'+
'');
var c1="c1";
var $c1=$("."+c1,$c);
this[c1]=new CLabelsAndTextBoxes2(prefix+c1+".",$c1,data[1]);
this.a=ko.observable("");
}
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
<!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.
It is a master and detail case.
Next it is the data we are going to use for it.
data.js
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
function CGrid(prefix,$c,data){
$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()"> </div>'+
'</div>'+
'<div class="clear"></div>'+
'</div>'+
'');
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;
};
});
});
$(".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
function CGridWithRadioButtons(prefix,$c,data){
$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()"> </div>'+
'</div>'+
'</div>'+
'');
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;
};
});
});
$(".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
function CPageMasterDetail(prefix,$c,data){
$c.html(''+
'<div class="c1"></div>'+
'<br>'+
'<div class="c2" data-bind="visible:'+prefix+'selected()"></div>'+
'');
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);
};
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
<!doctype html>
<html>
<head>
<style>
::-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.
CPageLabelsAndTextBoxesMasterDetail.js
function CPageLabelsAndTextBoxesMasterDetail(prefix, $c, data){
$c.html(''+
'<div class="c1"></div>'+
'<div class="c2"></div>'+
'');
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]);
$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
<!doctype html>
<html>
<head>
<style>
::-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.