This is a local datasource example.

All in one working example : dynamic form + grid and common pager

Original Github repo

Have you ever faced a situation where different form controls create different data, and reading and writing data to from these form controls need to be controlled using messy scripts?

It is common, group of radio buttons creating array data, checkbox creates true/false on/off Y/N different values, and single select creates single string data, while a multi-select creates array data.

Wouldn't it be nice if all form controls accepted and produced same format of data. If you agree please read on.

The basis of this Datasource form controls is same format of data structure is produced and consumed by every form control. This makes reading and writing data process common across controls. Controls can be easily added and removed from forms. Even there can be fair amount of backend code which can be common.

Second comes datasources. The datasource is form control agnostic. Means it will create data in a format which can be linked to a form without having to worry what form control is being used to visualize the data. The form with all the controls can be serialized into a data format which is same as the datasource format.

The same datasource can be plugged into a datagrid view which may contain pagination or a simple table. This data grid can be easily modified to create updatatable rows of data

Note: This is not a one-way or two-way data binding library like angularjs or knockout or backbone models. Data needs to be read manually and written manually from/to the datasource.

Data binding is not a problem to implement, in fact one-way data binding should be very trivial to implement since we already have included observable models. If you want to implement data binding you will need to make entire datasource observable, and then in the event handler call `setValue` to all the form controls. However nice effect can be achieved by attaching a datasource with multiple forms. Updating all forms setValue() will result in same data appear in multiple places. We have not implemented data updating automatically, but infrastructure is there to implement one-way data binding. To avoid too many events on a page this was chosen design.

The library is basically a wrapper around any ui library of your choice. Here in most places we have used jQuery-ui, but you can mix different libraries and create a wrapper which uses the compliant our interchange format. Details of structure of widget can be seen in Form Controls Widget

As you might have guessed that there will be huge amount of DOM manipulation when we do form view updates. This would have been a major concern if every model update would create a cascade of form updates. But in general there are very few user triggered updates. Some might argue this is prehistoric framework as compared to angularjs or reactjs. But in reality this is very small and very lightweight for browsers.

Try out

Default options
{
	rootList: "data",
	meta: "",
	datatype: "json",
	items: [],
	resultFields: null//[{name: "name",  mapping: "name" /*can be xpath,json path
					  // or javascript function*/}, {name: "email"}]
	,
	csvFieldSep : ",",
	csvRecordSep: "\r\n",
	csvFieldDelimiter: '"'
};
Options (Paste transform options from below examples):
For JSON

{
resultFields: [
{name:'name', mapping:'full_name'}, 
{name:'email', mapping:'email_id'}
],
datatype: "json"
}


For CSV 
{
resultFields: [
{name:'name', mapping:0}, 
{name:'year', mapping:2}
],
csvFieldSep : "#",
csvRecordSep: "!",
csvFieldDelimiter: '"',
datatype:"csv"
}

Data (Paste CSV or JSON):

Result


Generated Code for the above: 
reader = new SchemaReader([options]);
		
var res1 = reader.read([data]);
$("#transformer1").text(JSON.stringify(reader.options.resultFields));
$("#result1").text(JSON.stringify(res1));

SchemaReader

This section shows filtering Data
var data1= [{
  name: "samarjit",
  email: "samarjit@email",
  extra: "extra value"
 },
 {
 name: "tutu",
 email: "tutu@email",
 extra: "extra value2"
 }
];
			
Transform 1
reader.options.resultFields:
			

Result1


Data Mapping shortcut
Transform 2
reader.options.resultFields:
			

Result2

:


Data Mapping
var data3= [{
  full_name: "samarjit",
  email_id: "samarjit@email",
  extra: "extra value"
 },
 {
 full_name: "tutu",
 email_id: "tutu@email",
 extra: "extra value2"
 }
];
Transform 3 Options
reader.options.resultFields:
			

Result3


For data which is not an array returned from server
The rootList option is used to extract this array. Internally it uses jsonPath to extact the data Data:
		
Transform Options 
reader.options.resultFields:

The associated javascript will be
reader.options.resultFields = [{ name: 'name', mapping: 'full_name'}, {name:'email'}];
reader.options.rootList = "myarr";
reader.options.datatype="json";
reader.read(data7);

Result


CSV Reading with filtering
Data: CSV reader is quite powerful one. Record delimiter (,) and Record seperator (CR LF or LF or CR).
		

Transform options 
resultFields :

Configuring CSV datatype: 
reader.options.datatype="csv";

Result


Custom Text or CSV Reading specified field separator (#) and record sep (!)
Data: CSV reader with arbritrary record delimiter and arbritary record seperator. Even records can be optionally enclosed by some arbritrary character (for csv default is ")
		

Transform options : 
reader.options.datatype="csv";
reader.options.resultFields = [{name:"name",mapping: 0}, {name: "year",mapping:2}];
reader.options.csvFieldSep = "#";
reader.options.csvRecordSep = "!";

Result


DataSource

Local datasource Data6

Model6 


Code for DS:
ds = $.uix.datasource({reader: new SchemaReader(), model: model6 ,rawdata: data6, paging:{limit: 1} });
$(ds).on("dataviewresponse",function(){
	$("#dynresult").text( JSON.stringify( ds.result) );
});
ds.refresh(); //triggers dataviewresponse
Dyn Form: Current Obj

Form Control Widgets

Form controls are a wrapper on top of HTML controls/set of HTML controls/jQuery widgets or any other smart control from any library. This enables all widget controls to have setter and getter in a similar way. Although jquery widgets aim for a similar goal of having unified way to set and get options but it is difficult to do certain things like loop through all the html elements and get all the values required to be submitted to server.

Advantages

  • widgetHelper provides the common code for render. Each specific widget is called from widgetHelper
  • Does not submit hidden field's values that are used to maintain the internal states of widgets
  • Makes it easy to deal with checkboxes
  • Makes radio groups as single entity to which value can be set, value can be read back, and any user action on any of the radio button element will be handled within that scope of that element
  • Display label and control in one box container. From a programmatic perspective its easier to find and highlight label in case of errors or to mark mandtory
  • Display error messages in proper place even if using jQuery validation plugin which usually appends and it appends in the wrong place usually if its a datepicker.
  • Provides a provision to display tooltip for any form control
  • Almost any widget can be wrapped up into a Form control even jQuery transfer widget which a text field and two selectboxes and a few buttons will be present but only one value gets submitted to server just like any other widget
  • serializing and deserializing of values can be done within each form control
  • Wraps up multi-selects to produce a comma separated value of selected fields
  • Setting json data received from server to any form and getting json data from a form is clean so its easier to loop though all the controls and do some validation and then submit that data to server
  • Preselected items values in a selectbox/combobox even if the rest of the list is loaded via ajax. Initially the actual value is set int he textbox and later when the option values are loaded through ajax, then delayed preselection is run to give proper visual apearance of pre-selected items. This also works seemlessly accross selectbox/multiselect/transfer widgets.
Lets see the internal structure of the from controls. This will also explain how and why are some things done the way it is done. This is like a factory method to create all jquery Sliders. But this slider accompanies another text field to show the current value.
var spineSlider =  {
 		fromElm: null,
 		name: "slider",
 		render: function ($form, field,  mode  ){
 			var html =  $.templates("#tmpl_slider_"+mode).render(field); // can.view("#tmpl_datepicker_"+mode, field);
			$form.append(html);	
			var fieldWidgets = $("[data-spine-prop='disp_"+field.name+"']", $form);
			var opts = (field.widgetData!=null)?$.parseJSON(field.widgetData)||{}:{};
			
			fieldWidgets.slider(opts);
			var fieldWidgetsElm = $("[data-spine-prop='"+field.name+"']", $form);
			var that = this;
			fieldWidgets.on("slidechange",function(e,v){
				fieldWidgetsElm.val( $(this).slider("value"));
			});
			
			fieldWidgetsElm.on("change", function(e,v){
				$(this).parent().find("[data-spine-prop='disp_"+$(this).data("spine-prop")+"']").data('ui-slider').value(this.value);
			});
			
			fieldWidgets.trigger("slidechange");
 		},
 		getValue: function($el){
 			return $el.val();//$el.data('ui-slider').value();;
 		},
 		setValue: function($el, val){
 			if($.type(val) != "number"){
 				try{
 					val = parseInt(val);
 				}catch(e){
 					val = -1;
 				}
 			}
 				
 			$el.val(val);
 			$el.trigger("change");
 		},
 		showError: function($el){
 			
 		},
 		showTooltip: function($el){
 			
 		}
 	};
			
Template
<script type="text/template" id="tmpl_datepicker_edit">
<div class="fieldbox">	
   <label for="{{:name}}" class="field-label">{{:label}}

	{{if mandatoryDecorate == 'Y'}}
	<span style='color:red' ><strong>*</strong></span>
	{{/if}}
	</label>
   <div type="text" name="disp_{{:name}}" data-spine-prop="disp_{{:name}}" data-spine-type="slider" style="width:150px;display: inline-block"  value="Field Details" ></div>
   
   <input type="text"name="{{:name}}" data-spine-prop="{{:name}}" data-spine-type="slider" size="2" maxlength="10" />

   <div class="error-container"></div>
</div>
</script>

</script>
			
This is a simple widget wrapper of a normal jquery Slider. A few interesting things can be seen here.

* Slider + its label
Slider + its label forms the control. So div.fieldbox can be put anywhere and it will display correctly along with its label. This helps a lot in creating dynamic forms. Web frameworks like struts also advocates it <sj:datepicker label="Birth Date" name="dob" />.
* Error container
is predefined so error will always display in the right place. If jquery validation plugin and jquery transfer plugin is used, which has two select boxes and an optional search box. The error label appended by jquery validation can appear in some very odd places. If jquery validation is used with jquery datepicker plugin normally the error label crops up in between the textfield and the calendar icon making GUI look horrible, this situation can be avoided here. Another possibility this "error-container" offers is to wrap the error into a javascript tooltip. In that case showError() function needs to be used to render the error. Note: Struts 2 framework has templates in server side that gets rendered, but no error container. They do client side validation much like jquery validation plugin. Struts 2 does well for normal containers but as soon as transfer widgets comes their error system goes haywire. It validates the wrong element-1 and shows error messages attached to the wrong element-2 (horrible)
<s:optiontransferselect
    label="Lucky Numbers"
    name="leftNumber"
    list="{'1 - One ', '2 - Two', '3 - Three', '4 - Four', '5 - Five'}"
    doubleName="rightNumber"
    doubleList="{'10 - Ten','20 - Twenty','30 - Thirty','40 - Forty','50 - Fifty'}"
/>
Main element
* Main element of any widget is the [data-spine-prop] and [data-spine-type]. It is mandatory to have an element/div with these two properties. This enables to loop through all the elements of a form that correspond to an from control. This makes sure that sub elements within a control are never parsed while getting values in a generic way. $("*[data-spine-prop]") //will correctly get all the widgets in a form. All set value and get value should refer these HTML elements only.
$("*[data-spine-prop]").each(function(i,v){ //loops through all the elements
 	var val = widgetHelper.getValue($(v));
 	console.log( val );
 });
					  
Validation
* Validation should be done based on the main element. The problem faced by struts2 above can be easily addressed by using main element concept and error container concept. I dont know why they missed it.
Unified way to set and get
* Unified way to set and get value is achieved like this. Any elements to show only display properties, internal states are all ignored while dealing with form data.
render()
* render() function is supposed to render the template and set up the change events within the widget. In this spider widget to show current value one text field is used appended right next to the slider to show its current value. DOM Events needs to be set up to synchronize the text field's values and the slider values. A change can be triggered by user while doing the sliding. A change can be triggered by setting value on that slider from another javascript function. It is well knows that setting by javscript does not automatically fires a "change" event. So this is the place to set that up.
setValue()
* setValue() is used to set value. It also is used to set different forms of a value. For example. In the above Slider both number and text can be set on that value. There is a possibility to show error state of widget in the error container as well.
getValue()
* getValue() is used to get a value from a widget control. Additional serialization of internal state can be done here. widgetHelper.setValue($el, '30');
var retDate = widgetHelper.getValue($el);


Some concepts turned out to be similar to https://github.com/mleibman/SlickGrid/wiki/Writing-custom-cell-editors
Ex.http://mleibman.github.io/SlickGrid/examples/example3-editing.html, http://mleibman.github.io/SlickGrid/examples/example3a-compound-editors.html

Setting all Values
$("#btnSetFormData").click(function(){
	var frmCombiDataSet = eval('"use strict"; var d='+$("#frmCombiDataSet").val()+';d');
	$.each(frmCombiDataSet, function(i,v){
		 widgetHelper.setValue($("[data-spine-prop="+i+"]"), v);
	});
});
Getting all Values

					
var j = {};
$("[data-spine-prop]", $("#frmContainer")).filter("[data-spine-type]").each(function(i,v){
	j[$(v).data("spine-prop")] = widgetHelper.getValue($(v))
});
$("#frmCombiDataGet").html(JSON.stringify(j,null,3));

Example Slider

var field = {name: "myslider", label: "My Slider Label", capturetype: "slider"};
widgetHelper.render($("#slidercontainer"),field, "edit");
widgetHelper.setValue($("[data-spine-prop=myslider]"), ...):
widgetHelper.setValue($("[data-spine-prop=myslider]"),50) :
widgetHelper.setValue($("[data-spine-prop=myslider]"),"12") data conversion:
widgetHelper.getValue($("[data-spine-prop=myslider]")):

Example radio group

var field = {name: "myradiogroup", label: "My radio Label", capturetype: "radiogroup", widgetData:"{\"floatLeftProp\": \"Y\",\"radioList\":\"[{val: 'Y', label: 'Yes'},{val: 'M', label:'Male' }]\"}"};

widgetHelper.render($("#radiocontainer"),field, "edit");
widgetHelper.setValue($("[data-spine-prop=myradiogroup]"), ...):
widgetHelper.setValue($("[data-spine-prop=myradiogroup]"),"Y") :
widgetHelper.setValue($("[data-spine-prop=myradiogroup]"),"M") :
widgetHelper.getValue($("[data-spine-prop=myradiogroup]")):

Example Transfer Widget

field = {name: "mytransfer", label: "My Transfer Label", capturetype: "transferWidget", 
widgetData:"{\"unHidePanelData\":\"\",\"onChangeFnName\":\"\",\"onFocusFnName\":\"\",\"datasource\":\"local\",\"localdata\":\"AF,AFGHANISTAN,selected\\nAX,LAND ISLANDS,selected\\nAL,ALBANIA\\nDZ,ALGERIA\"}" };

widgetHelper.render($("#radiocontainer"),field, "edit");
widgetHelper.setValue($("[data-spine-prop=myradiogroup]"), ...):
widgetHelper.setValue($("[data-spine-prop=myradiogroup]"),"DZ,AL") :
widgetHelper.setValue($("[data-spine-prop=myradiogroup]"),"AL") :
widgetHelper.getValue($("[data-spine-prop=myradiogroup]")):

Example Multi-select Widget Ajax + delayed preselect

field = {name: "mymultisel", 
label: "My Multisel Label", 
capturetype: "multiSelect", 
widgetData:"{\"datasource\":\"remote\",\"unHidePanelData\":\"\",\"onChangeFnName\":\"\",\"onFocusFnName\":\"\",\"localdata\":\"AF,AFGHANISTAN\\nSG,SINGAPORE\\nIN,INDIA,selected\\nAL,ALBANIA,selected\\nDZ,ALGERIA\",\"remoteurl\":\"test/country.txt\"}" };

widgetHelper.render($("#multiselcontainer"),field, "edit");
//-->To simulate that value was set after form was rendered but before the country.txt would have been loaded by ajax.
document.getElementsByName("mymultisel")[0].value = 'AF'; 
//Thus to highlight AF delayed preselection is working
//The other two selected items(IN,AL) are marked as "selected" in country.txt itself
widgetHelper.setValue($("[data-spine-prop=mymultisel]"), ...):
widgetHelper.setValue($("[data-spine-prop=mymultisel]"),"IN") :
widgetHelper.setValue($("[data-spine-prop=mymultisel]"),"AF,AL") :
widgetHelper.getValue($("[data-spine-prop=mymultisel]")):
Link