KnockoutJS losing previous value when loading options after value - javascript

I'm feeding my options off an AJAX request, while the value is in the selection initially. However Knockout seems to delete values that aren't in the options on binding.
Example: http://jsfiddle.net/EVzrH/
Knockout seems to use selectExtensions (line 1699 of v3) to read and write the selected option. In this new values are matched to indexes, and returned by again getting the index and matching to data.
How can I save my data from being lost?

Generally, I handle this by prepopulating the observableArray with the current value (no need for the text, since you wouldn't likely know it yet).
Like:
var viewModel = {
val: ko.observable(1),
opts: ko.observableArray([{ Id: 1 }])
};
Then, let the observableArray get populated with the actual values when it returns.
For a more generic solution, you could use a custom binding as described in the second part of this answer: Knockout js: Lazy load options for select
This would pre-populate the observableArray for you and take into account that you may or may not have optionsValue set.

I can see 2 possible options here. First is to fill opts arrray before applying bindings:
var viewModel = {
val: ko.observable(1),
opts: ko.observableArray([])
};
viewModel.opts([
{ Id: ko.observable(1), Text: ko.observable("abc") },
{ Id: ko.observable(2), Text: ko.observable("someVal") },
{ Id: ko.observable(3), Text: ko.observable("other") }
]);
ko.applyBindings(viewModel);
Here is fiddle: http://jsfiddle.net/EVzrH/1/
Or if for some reason you cannot populate it before applying bindings you can just save value and them assign it again:
var viewModel = {
val: ko.observable(1),
opts: ko.observableArray([])
};
var value = viewModel.val();
ko.applyBindings(viewModel);
viewModel.opts([
{ Id: ko.observable(1), Text: ko.observable("abc") },
{ Id: ko.observable(2), Text: ko.observable("someVal") },
{ Id: ko.observable(3), Text: ko.observable("other") }
]);
viewModel.val(value);
Here is a fiddle: http://jsfiddle.net/EVzrH/2/

Either set the value after populating the options, or subscribe to the options:
viewModel.opts.subscribe(function() {
viewModel.val(1);
});
http://jsfiddle.net/gCyP6/

I've managed to get it working the way I wanted by commenting out some of the knockout code to avoid ko.dependencyDetection.ignore.
http://jsfiddle.net/EVzrH/3/
ko.bindingHandlers['value']['update'] = function (element, valueAccessor) {
ko.bindingHandlers['options']['update'] = function (element, valueAccessor, allBindings) {
Only problem is that it isn't minified, so switching to the minified library does not work.

Related

Dynamically create UI elements via Controller and bind them to a specific properties in Model

I've been trying to add dynamic content to my dialog based on specific object in my JSONmodel, which is an array of objects.
My model has the following structure, I've set it like this(dummy data):
Note: I have multiple models active in this controller's view, each of which has its own model data.
this.setData( emp: [
{
col1: "1.4",
col2: "2.0",
col3: "3.1"
},
{
col1: "4.1",
col2: "5.3",
col3: "6.5"
}
]);
So I've set the model data successfully and now I am able to access it via:
var modelData= this.oView.getModel("myModel").oData;
What I want now is to dynamically create sap.m.Dialog and dynamically fill it with multiple sap.m.Input elements which have values based on a single object from my model:
var getDialogContent = function(modelData){
var arr = [];
var keys = Object.keys(modelData[0]); // property names. I hard-coded first obj for test.
// I want to use these properties and bind a new input on dialog for each property.
jQuery.each(keys, function(i, key) {
// 'myModel>/emp/0/'+key is a supposed full path to property...
// according to this link:
// https://sapui5.hana.ondemand.com/1.36.6/docs/guide/91f0ed206f4d1014b6dd926db0e91070.html
newInput.bindProperty("value", 'myModel>/emp/0/' + key); //key is col1 the first time
newInput.setProperty("description", key);
newInput.setProperty("type", sap.m.InputType.Number);
arr.push(newInput);
});
return arr;
};
I call getDialogContent() in the content property of the dialog to set its content.
Now, everything works save for the binding newInput.bindProperty("value", 'myModel>/emp/0/' + key);, the input fields that are displayed are just empty and show no sign of binding, also newInput.getBindingContext("myModel"); returns undefined.
var dialog = new sap.m.Dialog({
title: 'Dynamic dialog: ',
type: 'Message',
content: getDialogContent(modelData),
buttons: new sap.m.Button({
text: 'Cancel',
press: function () {
dialog.close();
}
}),
afterClose: function() {
dialog.destroy();
}
});
Does anyone have any idea what is wrong here and why can't I bind my property to the input element? I basically just want to bind values of my dynamic input fields to arbitrary object from object array in my JSON Model. Any suggestion is welcome.
Edit(Solution):
On the var keys = Object.keys(modelData[0]); line I replaced modelData[0] with modelData["emp"][0] as I was accessing specific object form JSONModel. Now it works.
Did you add the dialog to the dependents of your view? When I remove that step in our app, the result is exactly as you described: The fields are empty and getBindingContext() returns undefined.
One of the best way to implement a dialog in a reusable manner is the one described in this link. You have to add the dialog as dependent to the "parent" view in order to retrieve the models set on that view.
onDialogOpen: function () {
if (!this.oDialog) {
this.oDialog = new sap.m.Dialog({
title: 'Dynamic dialog: ',
type: 'Message',
content: getDialogContent(modelData),
buttons: new sap.m.Button({
text: 'Cancel',
press: function () {
this.oDialog.close();
}.bind(this)
}),
afterClose: function() {
this.oDialog.destroy();
}.bind(this)
});
//to get access to the view models
this.getView().addDependent(this.oDialog);
}
this.oDialog.open();
},

Angular with Kendo, Using Grid Values Asynchronously

Ok I'm pretty sure I know exactly what I need to do here but I'm not sure how to do it. Basically I have a grid that I want to make a key column bind to an array of key/values, which I've done before with kendo (not using Angular) and I know that when I'm creating my key/value array asynchronously then that needs to complete before I can get them show-up with kendo, which I have done using promises before.
So here I have the same issue only angular is also involved. I need to fetch and format an array of data into the format in which a kendo grid column can digest it, so no problem here is my controller code:
var realm = kendo.data.Model.define({
id: 'realmID',
fields: {
realmID: { editable: false, nullable: true }
realmType: { type: 'string', validation: { required: true } }
}
})
var ds1 = kendoHelpers.dataSourceFactory('realms', realm, 'realmID')
var realmType = kendo.data.Model.define({
id: 'realmTypeID',
fields: {
realmTypeID: { editable: false, nullable: true },
name: { type: 'string', validation: { required: true } }
}
})
var ds2 = kendoHelpers.dataSourceFactory('realms/types', realmType, 'realmTypeID')
$scope.mainGridOptions = {
dataSource: ds1,
editable: true,
navigatable: true,
autoBind:false,
toolbar: [
{ name: "create" },
{ name: 'save' },
{ name: 'cancel' }
],
columns: [
{ field: 'realmID', title: 'ID' }
{ field: 'realmTypeID', title: 'Realm Type', editor: realmTypesDDL, values: $scope.realmTypeValues },
{ command: "destroy" }
]
}
$scope.secondGridOptions = {
dataSource: ds2,
editable: true,
navigatable: true,
toolbar: [
{ name: "create" },
{ name: 'save' },
{ name: 'cancel' }
],
columns: [
{ field: 'realmTypeID', title: 'ID' },
{ field: 'name', title: 'Name' }
{ command: "destroy" }
]
}
ds2.fetch(function () {
$scope.realmTypeValues = [{ text: 'Test', value: "24bc2e62-f761-4e70-804c-bc36fdeced3d" }];
//this.data().map(function (v, i) {
// $scope.realmTypeValues.push({ text: v.name, value: v.realmTypeID})
//});
//$scope.mainGridOptions.ds1.read()
});
function realmTypesDDL(container, options) {
$('<input />')
.appendTo(container)
.kendoDropDownList({
dataSource: ds2,
dataTextField: 'name',
dataValueField: 'realmTypeID'
});
}
I made this dataSourceFatory helper method above to return me a basic CRUD kendo dataSource that uses transport and also injects an authorization header which is working fine so don't get hung up on that, ultimately I'm going to be using this data in another grid as well as for reference values for the main grid, but I've hard coded some values that I can use to test with in the ds2.fetch callback.
My HTML is pretty plain:
<div>
<h2>Realms</h2>
<kendo-grid options="mainGridOptions"></kendo-grid>
<h2>Realm Types</h2>
<kendo-grid options="secondGridOptions"></kendo-grid>
</div>
This all works fine and well except I am only seeing the GUID of the realmTypeID in the grid, I click it and the editor is populated correctly so that's good but I want the text value to be displayed instead of the GUID. I'm sure the issue is that the array of values is empty whenever angular is binding to the grid options. My questions are:
How do I either delay this bind operation or manually rebind it after the fetch call?
Is there a better way to handle a situation like this? I try not to expend finite resources for no reason (IE making server calls when unnecessary)
Note: When I move the creation of the text/value array to happen before the grid options, I get the desired behavior I am after
EDIT A work around is to not use the directive to create the grid and instead defer the grid creation until the callback of whatever data your column is dependent on, I was hoping for a more elegant solution but this is better than nothing. So your HTML becomes something like
<h2>Realms</h2>
<div id="realms"></div>
<h2>Realm Types</h2>
<kendo-grid options="secondGridOptions"></kendo-grid>
Then you can create the grid in the fetch callback for example:
ds2.fetch(function () {this.data().map(function (v, i) {
$scope.realmTypeValues.push({ text: v.name, value: v.realmTypeID})
});
$('#realms').kendoGrid($scope.mainGridOptions);
$scope.mainGridOptions.dataSource.fetch()
});
But this doesn't feel very angularish so I'm really hoping for a better solution!
Ok...well I think I hacked this enough and without another suggestion I'm going to go forward with this approach. I'm just going to move the binding logic to the requestEnd event of the second grid so that the values array can be populated right before the binding even. I'm also reworking the values array in this method. It is a bit weird though, I think there is some kendo black magic going on with this array because I can't just set it to a new empty array without it breaking completely...which is why I'm poping everything out prior to repopulating the array. That way when something is deleted or edited in the second grid, the DDL in the first grid is updated in the callback.
function requestEnd(e) {
for (var i = $scope.realmTypeValues.length; i >= 0; i--) $scope.realmTypeValues.pop();
var data;
if (e.type == "read")
data = e.response;
else
data = e.sender.data();
data.map(function (v, i) { $scope.realmTypeValues.push({ text: v.name, value: v.realmTypeID }); });
if ($('#realms').data('kendoGrid') == undefined) {
$('#realms').kendoGrid($scope.mainGridOptions);
}
else
$('#realms').data('kendoGrid').columns[4].values = $scope.realmTypeValues;
}
ds2.bind('requestEnd', requestEnd);
So I'm going to accept my own answer unless anyone has a better approach!

Data Model's "serialize" function not called on property "set"ting

Also asked on Sencha's site here
My data model's "serialize" function is not called when I call
model.set("<fieldName>", <newValue>);
Here's a fiddle
I'm pretty unclear on why the serialize function isn't called...am I missing something, or is this a bug?
(And here's the code from the fiddle)
Ext.application({
name : 'Fiddle',
requires: [
"Ext.data.Store"
],
launch : function() {
var store = Ext.create("Ext.data.Store", {
data: [{Id: 0, Name: "Bill", Props: "{foo: 2, bar:{pan:5}}"}],
fields:[
{name: "Id", type: "int"},
{name: "Name", type: "string"},
{name: "Props",
convert: function(value, record){
console.log("called convert");
return Ext.JSON.decode(value);
},
serialize: function(value, record){
alert("never getting called!! :(");
console.log("sure, i'll put log here too..not getting called though");
return Ext.JSON.encode(value);
}
}
]
});
console.log(store.getAt(0));
var rec = store.getAt(0);
var newProp = {rec:"junk", foo: "orange"};
console.log(newProp);
rec.set("Props",newProp);
}
});
Mappings from source content (JSON/XML) to business model (Ext.data.Model) are not automatically created in ExtJS's data model system. As such, another step is needed to produce this relationship using mapping/associationsor something similar.
I.e. The data model doesn't store the original JSON to read/write from, which is fine for most cases. When a JSON string needs to be updated via ExtJS, one solution is to, on the model, set
convertOnSet
to false, allowing for custom manipulation of the JSON string via extract/update functions on the data model.

AngularJS Push in Collection, Show from Collection, $watch changes

I use object ITEMS to hold data
$scope.items = [
{value:'title1', text: 'head1',
value:'title2', text: 'head2',
value:'title3', text: 'head3' }
];
When I clicked 'Add option' button I need show 'value' and 'text' in HTML page:
$scope.items.push(
{
value: 'value1',
text: 'text1'
}
);
I can show object length, but I can't show added option.
And $watch ($watchCollection) doesn't work too.
In this example I don't get values from inputs.
enter link description here
Your $scope.items array is improperly declared. You need braces around each separate item in the array, like this:
$scope.items = [
{value:'title1', text: 'head1'},
{value:'title2', text: 'head2'},
{value:'title3', text: 'head3'}
];
Your directive is all kinds of messed up. You don't even need to create a new directive if all you want to do is display the items in a list. You can just do this:
<select ng-model="selectedItem" ng-options="item.text for item in items"></select>
Your textboxes are ok, except for the typo in the ng-model="addoText". Your labels below should be bound to the same variables as the textboxes.
key: {{addVal}} <br>and value: {{addText}}
That will update the labels as you type in the textboxes. If you don't want to update the labels until you add a new item, then bind them to some new variables, like this:
key: {{newVal}} <br>and value: {{newText}}
Finally, your add() function should look like this:
$scope.add = function () {
$scope.items.push(
{
value: $scope.addVal,
text: $scope.addText
}
);
$scope.newVal = $scope.addVal;
$scope.newText = $scope.addText;
};
This pushes the new item to the array, and sets the bindings on your labels to the new values. You don't need to $watch anything.
Here's a Plunker.
There is an issue with how your items array looks at the moment.
I think your $scope.items should look like:
$scope.items = [
{
value: "value1",
text: "text1"
},
{
value: "value2",
text: "text2"
}
]
rather than all in one object, as when you push you'll create a new object.
With your question, calling items.value, will result in an undefined.
You need to call an object in $scope.items. Calling items[$scope.items.length-1] will get the most recent object added, and such items[$scope.items.length-1].value and items[$scope.items.length-1].text the values in that object

AngularUI select2 AJAX set via model

I'm using AngularUI's ui-select2 directive with AJAX.
Here's what I've got in my view:
<label>Group: <input ng-model="group" ui-select2="select2GroupConfig"></label>
Here's what I have in my model:
$scope.select2GroupConfig = {
ajax: {
url: 'theURL',
data: function (term, page)
{
return { q: term };
},
results: function (data, page)
{
return { results: data };
}
}
};
This works as expected.
My question: How can I update the value via the model?
I tried:
$scope.group = 'some group';
I also tried using an object:
$scope.group = { id: 32, text: 'some group'};
but that doesn't either work.
How do you update a select2 that uses AJAX, via the model?
Turns out you can set it to an object, but only after ui-select2 runs; I was trying to give it an initial value.
So, instead of using the regular model, you have to use select2's initSelection function:
$scope.group = 'Dummy Content';
$scope.select2GroupConfig.initSelection = function ( el, fn ) {
fn({ id: 2, text: 'Some group' });
}
Note that you have to give the input an initial value, otherwise initSelection is never called. That's why I'm just setting it to some dummy content.
This works, but it feels like a hack.
Does anybody have any better ideas?
If you have initSelection setup, you can pass just the ID and the directive will pull up the entire row object.
This will also allow you to set the value when the page loads to just the ID too.
If you don't want to use initSelection you can set the entire row (object) as the value and select2 will update accordingly. It all depends on your use-case however.

Categories

Resources