I'd like to
1) Draw create form fields and populate them with data from javascript objects
2) Update those backing objects whenever the value of the form field changes
Number 1 is easy. I have a few js template systems I've been using that work quite nicely.
Number 2 may require a bit of thought. A quick google search on "ajax data binding" turned up a few systems which seem basically one-way. They're designed to update a UI based on backing js objects, but don't seem to address the question of how to update those backing objects when changes are made to the UI. Can anyone recommend any libraries which will do this for me? It's something I can write myself without too much trouble, but if this question has already been thought through, I'd rather not duplicate the work.
////////////////////// edit /////////////////
I've created my own jquery plugin to accomplish this. Here it is. Please let me know if it's useful and if you think it might be worth making it more "official". Also let me know if you have problems or questions.
/*
Takes a jquery object and binds its form elements with a backing javascript object. Takes two arguments: the object
to be bound to, and an optional "changeListener", which must implement a "changeHappened" method.
Example:
// ============================
// = backing object =
// ============================
demoBean = {
prop1 : "val",
prop2 : [
{nestedObjProp:"val"},
{nestedObjProp:"val"}
],
prop3 : [
"stringVal1",
"stringVal12"
]
}
// ===========================
// = FORM FIELD =
// ===========================
<input class="bindable" name="prop2[1].nestedObjProp">
// ===========================
// = INVOCATION =
// ===========================
$jq(".bindable").bindData(
demoBean,
{changeHappened: function(){console.log("change")}}
)
*/
(function($){
// returns the value of the property found at the given path
// plus a function you can use to set that property
var navigateObject = function(parentObj, pathArg){
var immediateParent = parentObj;
var path = pathArg
.replace("[", ".")
.replace("]", "")
.replace("].", ".")
.split(".");
for(var i=0; i< (path.length-1); i++){
var currentPathKey = path[i];
immediateParent = immediateParent[currentPathKey];
if(immediateParent === null){
throw new Error("bindData plugin encountered a null value at " + path[i] + " in path" + path);
}
}
return {
value: immediateParent[path[path.length - 1]],
set: function(val){
immediateParent[path[path.length - 1]] = val
},
deleteObj: function(){
if($.isArray(immediateParent)){
immediateParent.splice(path[path.length - 1], 1);
}else{
delete immediateParent[path[path.length - 1]];
}
}
}
}
var isEmpty = function(str){
return str == null || str == "";
}
var bindData = function(parentObj, changeListener){
var parentObj,
radioButtons = [];
var changeListener;
var settings;
var defaultSettings = {
// if this flag is true, you can put a label in a field,
// like <input value="Phone Number"/>, and the value
// won't be replaced by a blank value in the parentObj
// Additionally, if the user clicks on the field, the field will be cleared.
allowLabelsInfields: true
};
// allow two forms:
// function(parentObj, changeListener)
// and function(settings).
if(arguments.length == 2){
parentObj = arguments[0];
changeListener = arguments[1]
settings = defaultSettings;
}else{
settings = $jq.extend(defaultSettings, arguments[0]);
parentObj = settings.parentObj;
changeListener = settings.changeListener;
}
var changeHappened = function(){};
if(typeof changeListener != "undefined"){
if(typeof changeListener.changeHappened == "function"){
changeHappened = changeListener.changeHappened;
}else{
throw new Error("A changeListener must have a method called 'changeHappened'.");
}
};
this.each(function(key,val){
var formElem = $(val);
var tagName = formElem.attr("tagName").toLowerCase();
var fieldType;
if(tagName == "input"){
fieldType = formElem.attr("type").toLowerCase();
}else{
fieldType = tagName;
}
// Use the "name" attribute as the address of the property we want to bind to.
// Except if it's a radio button, in which case, use the "value" because "name" is the name of the group
// This should work for arbitrarily deeply nested data.
var navigationResult = navigateObject(parentObj, formElem.attr(fieldType === "radio"? "value" : "name"));
// populate the field with the data in the backing object
switch(fieldType){
// is it a radio button? If so, check it or not based on the
// boolean value of navigationResult.value
case "radio":
radioButtons.push(formElem);
formElem.data("bindDataPlugin", {navigationResult: navigationResult});
formElem.attr("checked", navigationResult.value);
formElem.change(function(){
// Radio buttons only seem to update when _selected_, not
// when deselected. So if one is clicked, update the bound
// object for all of them. I know it's a little ugly,
// but it works.
$jq.each(radioButtons, function(index, button){
var butt = $jq(button);
butt.data("bindDataPlugin").navigationResult.set(butt.attr("checked"));
});
navigationResult.set(formElem.attr("checked"));
changeHappened();
});
break;
case "text":
// if useFieldLabel is true, it means that the field is
// self-labeling. For example, an email field whose
// default value is "Enter Email".
var useFieldLabel = isEmpty( navigationResult.value )
&& !isEmpty( formElem.val() )
&& settings.allowLabelsInfields;
if(useFieldLabel){
var labelText = formElem.val();
formElem.click(function(){
if(formElem.val() === labelText){
formElem.val("");
}
})
}else{
formElem.attr("value", navigationResult.value);
}
formElem.keyup(function(){
navigationResult.set(formElem.attr("value"));
changeHappened();
});
break;
case "select":
var domElem = formElem.get(0);
$jq.each(domElem.options, function(index, option){
if(option.value === navigationResult.value){
domElem.selectedIndex = index;
}
});
formElem.change(function(){
navigationResult.set(formElem.val());
})
break;
case "textarea":
formElem.text(navigationResult.value);
formElem.keyup(function(){
changeHappened();
navigationResult.set(formElem.val());
});
break;
}
});
return this;
};
bindData.navigateObject = navigateObject;
$.fn.bindData = bindData;
})(jQuery);
There are tons of libraries out there to achieve what you want.
For starters, you can use DWR to get the Ajax functionality. The method that gets triggered for the form field's onChange event should make a DWR call to the corresponding backing object
Hope this helps!
Microsoft has a proposal for an enhancement to jQuery core that implements decently reach data binding including a two binding (source to target, and target to source).
They call it Data Linking - which is odd since the name of this concept in all other MS technologies (WinForms, WPF, WebForms, etc...) is DataBinding.
Link to the MS Proposal
DataBind - a template plugin for jQuery. The library has a method unbinddata(), which collects the data back to json-array.
Unfortunately, the library works only with form inputs.
If you made some solution, can I see it?
Related
I'm trying to set objects into localStorage with a format similar to the following:
[{"1":{"property1":false,"property2":false}},{"2":{"property1":false,"property2":false}}]
Where I'd be able to set the 1 or 2 based on a dynamic value I'm getting from a REST call. What I have so far is:
// check if session exists and create if not
var StorageObject = JSON.parse(localStorage.getItem("session")) || [];
//see if the current id from the REST call is in storage and push with properties if not
if ( !StorageObject[thisItemsListID] ) {
var itemProperties = {};
itemProperties[thisItemsListID] = {};
itemProperties[thisItemsListID]["property1"] = false;
itemProperties[thisItemsListID]["property2"] = false;
StorageObject.push(itemProperties);
localStorage.setItem('session', JSON.stringify(StorageObject));
}
I can get the data into localStorage using this format but StorageObject[thisItemsListID] always gets into the if statement and generates a duplicate item in localStorage and I'm not sure how to access this with a variable. I'm trying to append the new ID if it doesn't exist so if {1:{} exists but current ID is 2 I need to push the new value.
I'm close here and maybe I need to reevaluate the format I'm storing the data string but I'm going in circles here and could use a point in the right direction.
Well, the duplicate item is happening in StorageObject.push(itemProperties).
Try this to update the object:
//StorageObject.push(itemProperties); <-- remove
StorageObject[thisItemsListID] = itemProperties;
[EDIT]
If you want to keep [{"1":{"property1":false,"property2":false}},{"2":{"property1":false,"property2":false}}]. To conditional would be a bit different.
var haveItem = StorageObject.filter(function(item){
return Objects.keys(item)[0] == thisItemsListID;
}).length > 0;
if ( !haveItem ) {
var itemProperties = {};
itemProperties[thisItemsListID] = {};
itemProperties[thisItemsListID]["property1"] = false;
itemProperties[thisItemsListID]["property2"] = false;
StorageObject.push(itemProperties);
localStorage.setItem('session', JSON.stringify(StorageObject));
}
Are you trying to update the object or just overwrite it? Filipes response illustrates how to update the entire storage object by just reassigning the object with the new value.
If you wanted to update just as section/ value of the object you could do so using a for loop. This would allow you to scan the array locate the one property and then remove it, updated it, overwrite it etc.
Here is an example of the loop. Bear in mind This is a snippet from a report library I was building. It uses angular $scope but it is a complex type doing a similar action to your update (here I am setting a label as a favorite/bookmark)
function OnFavoriteComplete(response) {
var id = response.config.data.reportId; //dynamic values set by client
var isFavorite = response.config.data.isFavorite;//dynamic values set by client
var arrayCount = $scope.reportList.length;
//loop my current collection and look for the property id of the label
//then check to see if true or false/this was a toggle enable disable
if (isFavorite) {
for (var i = 0, iLen = arrayCount; i < iLen; i++) {
if ($scope.reportList[i].reportId == id) {
$scope.reportList[i].isFavorite = false;
}
}
}
//if false update the property with the new value
else {
for (var i = 0, iLen = arrayCount; i < iLen; i++) {
if ($scope.reportList[i].reportId == id) {
$scope.reportList[i].isFavorite = true;
}
}
}
};
If you are using another framework like lowDash it has some really nice helper functions for updating and evaluating arrays.
Im using the following code,
jQuery.each(aDataSel, function(index, oData) {
oPushedObject = {};
aSelectedDataSet.push(fnCreateEnt(aProp, oData, oPushedObject));
});
This is aSelectedDataSet values
and this is the values of OData
What I need is that before I do the push is to fill the listTypeGroup & listTypeGroupDescription (with the red arrow ) with values that Are inside the oData -> ListTypeGroupAssigment -> result (listTypeGroup & listTypeGroupDescription) , The index is relevant since I want to add just the value of the index in each iteration (since this code is called inside outer loop and the index determine the current step of the loop) ,How it can be done nicely?
The result contain 100 entries (always) and the a selected data will have 100 entries at the end...
Update :)
Just to be clear In the pic I show the values which is hardcoded for this run but the values can be any values, we just need to find the match between the both objects values...
I mean to find a match between to_ListTypeGroupAssigment in both object (which in this case exist ) and if in oData there is result bigger then one entry start with the matching ...
UPDATE2 - when I try Dave code the following happen for each entry,
This happen in the Jquery.extend line...any idea how to overcome this?
The following hard-coded of Dave:-) work perfect but I need generic code which doesnt refer to specific field name
jQuery.each(aDataSet, function(index, oData) {
oPushedObject = {};
fnCreatePushedEntry(aProperties, oData, oPushedObject);
var result = oData.to_ListTypeGroupAssignment.results[index];
oPushedObject.to_ListTypeGroupAssignment = {
ListTypeGroup: result.ListTypeGroup,
ListTypeGroupDescription: result.ListTypeGroupDescription
};
aSelectedDataSet.push(oPushedObject);
});
Im stuck :(any idea how to proceed here ?what can be wrong with the extend ?
should I use something else ? Im new to jQuery...:)
I think that this happen(in Dave answer) because the oData[key] is contain the results and not the specified key (the keyValue = to_ListTypeGroupAssignment ) which is correct but we need the value inside the object result per index...
var needValuesForMatch = {
ListTypeGroup: 'undefined',
ListTypeGroupDescription: 'undefined',
}
//Just to show that oPushedObject can contain additional values just for simulation
var temp = {
test: 1
};
//------------------This object to_ListTypeGroupAssigment should be filled (in generic way :) ------
var oPushedObject = {
temp: temp,
to_ListTypeGroupAssignment: needValuesForMatch
};
oPushedObject is one instance in aSelectedDataSet
and after the matching I need to do the follwing:
aSelectedDataSet.push(oPushedObject);
Is this what you're after:
OPTION ONE - DEEP CLONE FROM oData TO aSelectedDataSet
aSelectedDataSet.forEach(function(currentObject,index){
for (var childObject in currentObject) {
if (! currentObject.hasOwnProperty(childObject))
continue;
var objectToClone = oData[childObject]['results'][index];
if(objectToClone)
$.extend(true,currentObject[childObject],objectToClone);
}
});
Here is your data in a fiddle with the function applied: https://jsfiddle.net/hyz0s5fe/
OPTION TWO - DEEP CLONE FROM oData ONLY WHERE PROPERTY EXISTS IN aSelectedDataSet
aSelectedDataSet.forEach(function(currentObject,index){
for (var childObject in currentObject) {
if (! currentObject.hasOwnProperty(childObject))
continue;
if(typeof currentObject[childObject] !== 'object')
continue;
for(var grandChildObject in currentObject[childObject]) {
var objectToClone = oData[childObject]['results'][index][grandChildObject];
if(typeof objectToClone === 'object') {
$.extend(true,currentObject[childObject][grandChildObject],objectToClone);
} else {
currentObject[childObject][grandChildObject] = objectToClone;
}
}
}
Fiddle for option 2: https://jsfiddle.net/4rh6tt25/
If I am understanding you correctly this should just be a small change:
jQuery.each(aDataSel, function(index, oData) {
oPushedObject = {};
fnCreateEnt(aProp, oData, oPushObj);
//get all the properties of oData and clone into matching properties of oPushObj
Object.getOwnPropertyNames(oData).forEach(function(key) {
if (oPushObj.hasOwnProperty(key)) {
//oPushObj has a matching property, start creating destination object
oPushObj[key] = {};
var source = oData[key];
var destination = oPushObj[key];
//can safely assume we are copying an object. iterate through source properties
Object.getOwnPropertyNames(source).forEach(function(sourceKey) {
var sourceItem = source[sourceKey];
//handle property differently for arrays
if (Array.isArray(sourceItem)) {
//just copy the array item from the appropriate index
destination[sourceKey] = sourceItem.slice(index, index + 1);
} else {
//use jQuery to make a full clone of sourceItem
destination[sourceKey] = $.extend(true, {}, sourceItem);
}
});
}
});
aSelectedDataSet.push(oPushedObject);
});
It is unclear what exactly your fnCreateEnt() function returns though. I am assuming it is the populated oPushObj but it's not entirely clear from your question.
Is there a way to programmatically check whether a filter with a given name exists?
I developed a directive to process page content based on a string input, I want it to react differently in case a certain part of the string corresponds to a filter that exists in my system. For example I have a localize filter:
// Somewhere in the code
var myInput = 'localize';
// Somewhere else
var contentToProcess = 'my content';
var result = '';
if ($filter.hasOwnProperty(myInput)) // TODO: this is the part I'm trying to figure out
result = $filter(myInput)(contentToProcess);
else
result = 'something else';
Jonathan's answers is also acceptable, but I wanted to find a way to check if a filter exists without using a try catch.
You can see if a filter exists like this:
return $injector.has(filterName + 'Filter');
The 'Filter' suffix is added by angular internally, so you must remember to add it or you will always return false
Solution
This seems to work for me.
var getFilterIfExists = function(filterName){
try {
return $filter(filterName);
} catch (e){
return null;
}
};
Then you can do a simple if check on the return value.
// Somewhere in the code
var myInput = 'localize';
var filter = getFilterIfExists(myInput);
if (filter) { // Check if this is filter name or a filter string
value = filter(value);
}
Bonus
If you are looking to parse apart a filter string for example 'currency:"USD$":0' you can use the following
var value; // the value to run the filter on
// Get the filter params out using a regex
var re = /([^:]*):([^:]*):?([\s\S]+)?/;
var matches;
if ((matches = re.exec(myInput)) !== null) {
// View your result using the matches-variable.
// eg matches[0] etc.
value = $filter(matches[1])(value, matches[2], matches[3]);
}
Pull it all together
Wish there was a more elegant way of doing this with angular however there doesn't seem to be.
// Somewhere in the code
var myInput = 'localize';
var value; // the value to run the filter on
var getFilterIfExists = function(filterName){
try {
return $filter(filterName);
} catch (e){
return null;
}
};
var filter = getFilterIfExists(this.col.cellFilter);
if (filter) { // Check if this is filter name or a filter string
value = filter(value);
} else {
// Get the filter params out using a regex
// Test out this regex here https://regex101.com/r/rC5eR5/2
var re = /([^:]*):([^:]*):?([\s\S]+)?/;
var matches;
if ((matches = re.exec(myInput)) !== null) {
// View your result using the matches-variable.
// eg matches[0] etc.
value = $filter(matches[1])(value, matches[2], matches[3]);
}
}
You can just do this:
var filter = $filter(myInput);
if (filter)
result = filter(contentToProcess);
else
result = 'something else';
Undefined and null values are treated as false in JS, so this should work in your case.
this is my first post in stackoverflow.. I am trying to iterate over an object(my implementation is an associative array) which in turn has some properties. Now I wish to construct another array out of it in order to use it as a localsource in jquery autocomplete widget for seach operations. Now the problem is that i am using for in loop to that according to the documenations available... However the output is always one less than the original object. The itearation involving the last element is not performed at all. Below is the sample object that I am using as input.
SubTeachPair = object{"5CS1":{SubAbbrev:"CA-L",SubCode:"5CS1",SubName:"Computer Architecture",TeacherId:"1",TeacherName:"Ayush Pandey",label:"Computer Architecture",value:"5CS1"},"5CS2":{SubAbbrev:"CA-P",SubCode:"5CS2",SubName:"Computer Engg",TeacherId:"10",TeacherName:"MAyush Pandey",label:"Computer Engg",value:"5CS2"}}
It has this kind of elements and is dynamically generated so the property names are variable. The loop construct that I have written is
var SubSource = [];
console.log(SubTeachPair);
var count = 0;
for(sub in SubTeachPair){
console.log(count);
SubSource[count] = {};
SubSource[count]['label']=SubTeachPair[sub]['label'];
SubSource[count]['value']=SubTeachPair[sub]['value'];
count++;
}
However, the result for the given input is only:
object{{ label: "Computer Architecture", value: "5CS1"}}
Am I missing something here?
edit-- The function that produces the input object is as follows(It is triggered onclick by the next button).
$('#' + $(this).attr("id")).autocomplete({
source : 'search',
minLength : 1,
change : function(event, ui) {
if( typeof ui.item != 'undefined') {
SubTeachPair[$(this).attr("id")] = {};
// console.log(ui.item);
SubTeachPair[$(this).attr("id")]['value'] = $(this).attr("id");
SubTeachPair[$(this).attr("id")]['label'] = $('label[for="' + this.id + '"]').html();
SubTeachPair[$(this).attr("id")]['SubCode'] = $(this).attr("id");
SubTeachPair[$(this).attr("id")]['SubName'] =$('label[for="' + this.id + '"]').html();
SubTeachPair[$(this).attr("id")]['SubAbbrev'] =$('label[for="' + this.id + '"]').attr('id');
SubTeachPair[$(this).attr("id")]['TeacherId'] = ui.item.id;
SubTeachPair[$(this).attr("id")]['TeacherName'] = ui.item.value;
// console.log(SubTeachPair);
//window.SubTeachPair = SubTeachPair;
}
}
});
I think I have found the cause of the error -- the object that is the input is actually the out put of another form that uses jquery autocomplete . Now when I enter something in the input and then click on the suggestion, the suggestion is filled in the text input, however if i do not click outside the input text and directly click the button which triggers my script, I get that error. Otherwise its fine. Is there any way to avoid that?
In your code, the array SubSource and count are not defined, You have to declare:
var SubSource = [];
var count = 0`
before for(sub in SubTeachPair) {...}
See http://jsfiddle.net/abu5C/
Try this:
SubSource[count] = {};
for(sub in SubTeachPair) {
console.log(count);
SubSource[count]['label']=SubTeachPair[sub]['label'];
SubSource[count]['value']=SubTeachPair[sub]['value'];
count++;
}
What is the certified way to determine the index of the currently selected item in a ComboBox in ExtJS?
Is there a difference on how to do this between ExtJS 3.x and 4?
var combo = new Ext.form.ComboBox(config);
var selectedIndex = combo.selectedIndex; // TODO: Implement
if(selectedIndex > 2) {
// Do something
}
Bonus-points for how to add it as a property to the ComboBox-object.
I think you'll have to use the combo's store for that. Combos have a private findRecord method that'll do a simple search over the store by property and value. You can see an example in the sourcecode itself (Combo.js line 1119).
1) Based on this you could find the selected index this way :
var v = combobox.getValue();
var record = combobox.findRecord(combobox.valueField || combobox.displayField, v);
var index = combobox.store.indexOf(record);
2) Or you could bind yourself to the "select" event which is fired with the combo, the record selected and its index as a parameter.
3) You could also access the view's getSelectedIndexes() but I doubt it's a good solution (in that I'm not sure it's available all the time)
Finally if you want to extend the combobox object I think this should work (if you go with the first solution) :
Ext.override(Ext.form.ComboBox({
getSelectedIndex: function() {
var v = this.getValue();
var r = this.findRecord(this.valueField || this.displayField, v);
return(this.store.indexOf(r));
}
});
In Ext 4.0.2 the same code would look like:
Ext.override(Ext.form.ComboBox, {
getSelectedIndex: function() {
var v = this.getValue();
var r = this.findRecord(this.valueField || this.displayField, v);
return(this.store.indexOf(r));
}
});
Jad, you're missing a closing parenthesis on your return statement... just thought you should know.
If you have a combo where valueField is the id used by the combo's store, you can simply avoid the search:
var v = combobox.getValue();
var record = combobox.findRecord(combobox.valueField || combobox.displayField, v);
var index = combobox.store.indexOf(record);
using this:
var id = combobox.getValue();
var record = store_combobox.getById(id);