Basically i have a drop down on selecting which there will be another drop down loaded. I have a computed variable depending on first drop down selected value(I know it can be subscribed,but still). But the computed is executed on page load which i dont want due to an AJAX call inside. 'What is the reason for the execution on page load and how to avoid that?
HTML:
<div>
<select id="selectmenu1" data-bind="options: departments,
optionsValue: 'id',
optionsText: 'name',
optionsCaption: 'Choose...',value: selectedDept">
</select>
<select id="selectmenu1" data-bind="options: contacts,
optionsValue: 'id',
optionsText: 'name',
optionsCaption: 'Choose...'">
</select>
</div>
And JS
// Here's my data model
var ViewModel = function(first, last) {
var self = this;
var deptArray = [];
var deptObj = {
id: "8888",
name: "Electrical"
};
deptArray[0] = deptObj;
deptObj = {
id: "9999",
name: "Admin"
};
deptArray[1] = deptObj;
self.departments = ko.observableArray(deptArray);
self.selectedDept = ko.observable();
self.contacts = ko.observableArray();
self.contactsRetrieve = ko.computed(function() {
var deptId = self.selectedDept();
console.log("entered");
$.ajax({
url: '/echo/js',
complete: function(response) {
console.log("success");
var contactArray = [];
var contactObj = {};
if (deptId == '8888') {
contactObj.id = '1234';
contactObj.name = 'Vivek';
} else if (deptId == '9999') {
contactObj.id = '5678';
contactObj.name = 'Sree';
}
contactArray[0] = contactObj;
self.contacts(contactArray);
}
});
console.log("exited");
});
};
ko.applyBindings(new ViewModel());
https://jsfiddle.net/jtjozkax/37/
if(!self.selectedDept()){return}
Using this if statement to cancel functionality within the computed if there is no selected option.
https://jsfiddle.net/jtjozkax/38/
As far as I know, it is not the page load itself that causes this behavior, but the fact that computed observables rely on other observables, in your case, self.selectedDept. Knockout discovers which other observables the computed relies on, and it is actually the change of value of selectedDept what causes the computed to be recomputed.
You cannot avoid this behavior, but you can avoid the execution of the AJAX call by adding a guarding condition (read: if statement). I assume your goal is to prevent the AJAX-call if nothing is selected in the dropdown, and, afterall, this is the most straightforward way to accomplish just that.
There is no other way around it, simply because if you think about it, the whole purpose of a computed is to reevalue itself whenever any other observable it depends on changes, without manual intervention.
Related
I am a database developer (there's the problem) tasked with emitting JSON to be used with Knockout.js to render sets of dependent list items. I have just started working with Knockout, so this is likely something obvious that I am missing.
Here is the markup:
<select data-bind="options:data,
optionsText:'leadTime',
value:leadTimes">
</select>
<!--ko with: leadTimes -->
<select data-bind="options:colors,
optionsText:'name',
optionsValue:'key',
value:$root.colorsByLeadTime">
</select>
<!--/ko-->
Here is the test data and code:
var data = [
{
key:"1",
leadTime:"Standard",
colors:[
{ key:"11", name:"Red" },
{ key:"12", name:"Orange" },
{ key:"13", name:"Yellow" }
]
},
{
key:"2",
leadTime:"Quick",
colors:[
{ key:"21", name:"Black" },
{ key:"22", name:"White" }
]
}
]
var dataViewModel = ko.mapping.fromJS(data);
var mainViewModel = {
data:dataViewModel,
leadTimes:ko.observable(),
colorsByLeadTime:ko.observable()
}
ko.applyBindings(mainViewModel);
As this stands, it correctly populates the value attribute of the second select list. However, if I add optionsValue:'key' to the first select list then the value attribute for that is set correctly but the second select list renders as an empty list.
All I need is for the value attribute of the option tag to be set to the key value provided in the data, regardless of where the select list is in the set of dependent lists. I've looked at many articles and the docs, but this particular scenario (which I would think is very common) is eluding me.
Here is a jsfiddle with the data, JS, and markup as given above: http://jsfiddle.net/tnagle/Lyxjt11y/
To really see the issue, you can add the following code after the initialization of mainViewModel.
mainViewModel.leadTimes.subscribe(function(newValue) {
console.log(newValue);
debugger;
});
Before adding the optionsValue:'key', line above will log the following output.
Object {key: function, leadTime: function, colors: function}
But after adding optionsValue:'key', it log the following output.
"1"
or
"2"
The reason it failed was because when you assign optionsValue: 'key' to the first select list, leadTimes property of your mainViewModel which before will contain an object that has property color, now will be set to a string object. Then the select list just failed to find color property from leadTimes that has changed to a string object.
One of the way to make it work is by changing to this:
var data = [
{
key:"1",
leadTime:"Standard",
colors:[
{ key:"11", name:"Red" },
{ key:"12", name:"Orange" },
{ key:"13", name:"Yellow" }
]
},
{
key:"2",
leadTime:"Quick",
colors:[
{ key:"21", name:"Black" },
{ key:"22", name:"White" }
]
}
]
var dataViewModel = ko.mapping.fromJS(data);
var mainViewModel = new function (){
var self = this;
self.data = dataViewModel;
self.leadTimes = ko.observable();
self.selectedKey = ko.observable();
self.selectedKey.subscribe(function(selectedKey){
self.selectedData(ko.utils.arrayFirst(self.data(), function(item) {
return item.key() == selectedKey;
}));
}, self);
self.colorsByLeadTime = ko.observable();
self.selectedData = ko.observable();
}
ko.applyBindings(mainViewModel);
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout.mapping/2.3.5/knockout.mapping.js"></script>
<select data-bind="options:data,
optionsText:'leadTime',
optionsValue:'key',
value:selectedKey">
</select>
<!--ko with: selectedData -->
<select data-bind="options:colors,
optionsText:'name',
optionsValue:'key',
value:$root.colorsByLeadTime">
</select>
<!--/ko-->
I have a View model, which has a loaddata function. It has no constructor. I want it to call the loadData method IF the ID field has a value.
That field is obtained via:
self.TemplateId = ko.observable($("#InputTemplateId").val());
Then, at the end of my ViewModel, I have a bit of code that checks that, and calls my load function:
if (!self.CreateMode()) {
self.loadData();
}
My load method makes a call to my .Net WebAPI method, which returns a slighly complex structure. The structure is a class, with a few fields, and an Array/List. The items in that list, are a few basic fields, and another List/Array. And then THAT object just has a few fields. So, it's 3 levels. An object, with a List of objects, and those objects each have another list of objects...
My WebAPI call is working. I've debugged it, and the data is coming back perfectly.
self.loadData = function () {
$.get("/api/PlateTemplate/Get", { id: self.TemplateId() }).done(function (data) {
self.Data(ko.mapping.fromJS(data));
});
}
I am trying to load the contents of this call, into an observable object called 'Data'. It was declared earlier:
self.Data = ko.observable();
TO load it, and keep everything observable, I am using the Knockout mapping plugin.
self.Data(ko.mapping.fromJS(data));
When I breakpoint on that, I am seeing what I expect in both data (the result of the API call), and self.Data()
self.Data seems to be an observable version of the data that I loaded. All data is there, and it all seems to be right.
I am able to alert the value of one of the fields in the root of the data object:
alert(self.Data().Description());
I'm also able to see a field within the first item in the list.
alert(self.Data().PlateTemplateGroups()[0].Description());
This indicates to me that Data is an observable and contains the data. I think I will later be able to post self.Data back to my API to save/update.
Now, the problems start.
On my View, I am trying to show a field which resides in the root class of my complex item. Something I alerted just above.
<input class="form-control" type="text" placeholder="Template Name" data-bind="value: Data.Description">
I get no error. Yet, the text box is empty.
If I change the code for the input box to be:
data-bind="value: Data().Description()"
Data is displayed. However, I am sitting with an error in the console:
Uncaught TypeError: Unable to process binding "value: function
(){return Data().Description() }" Message: Cannot read property
'Description' of undefined
I think it's due to the view loading, before the data is loaded from the WebAPI call, and therefore, because I am using ko.mapping - the view has no idea what Data().Description() is... and it dies.
Is there a way around this so that I can achieve what I am trying to do? Below is the full ViewModel.
function PlateTemplateViewModel() {
var self = this;
self.TemplateId = ko.observable($("#InputTemplateId").val());
self.CreateMode = ko.observable(!!self.TemplateId() == false);
self.IsComponentEditMode = ko.observable(false);
self.IsDisplayMode = ko.observable(true);
self.CurrentComponent = ko.observable();
self.Data = ko.observable();
self.EditComponent = function (data) {
self.IsComponentEditMode(true);
self.IsDisplayMode(false);
self.CurrentComponent(data);
}
self.loadData = function () {
$.get("/api/PlateTemplate/Get", { id: self.TemplateId() }).done(function (data) {
self.Data(ko.mapping.fromJS(data));
});
}
self.cancel = function () {
window.location.href = "/PlateTemplate/";
};
self.save = function () {
var data = ko.mapping.toJS(self.Data);
$.post("/api/PlateTemplate/Save", data).done(function (result) {
alert(result);
});
};
if (!self.CreateMode()) {
self.loadData();
}
}
$(document).ready(function () {
ko.applyBindings(new PlateTemplateViewModel(), $("#plateTemplate")[0]);
});
Maybe the answer is to do the load inside the ready() function, and pass in data as a parameter? Not sure what happens when I want to create a New item, but I can get to that.
Additionally, when I try save, I notice that even though I might change a field in the view (Update Description, for example), the data in the observed view model (self.Data) doesn't change.
Your input field could be this:
<div data-bind="with: Data">
<input class="form-control" type="text" placeholder="Template Name" data-bind="value: Description">
</div>
I prefer using with as its cleaner and should stop the confusion and issues you were having.
The reason that error is there is because the html is already bound before the data is loaded. So either don't apply bindings until the data is loaded:
$.get("/api/PlateTemplate/Get", { id: self.TemplateId() }).done(function (data) {
self.Data(ko.mapping.fromJS(data));
ko.applyBindings(self, document.getElementById("container"));
});
Or wrap the template with an if, therefore it won't give you this error as Data is undefined originally.
self.Data = ko.observable(); // undefined
<!-- ko if: Data -->
<div data-bind="with: Data">
<input class="form-control" type="text" placeholder="Template Name" data-bind="value: Description">
</div>
<!-- /ko -->
Also if you know what the data model is gonna be, you could default data to this.
self.Data = ko.observable(new Data());
Apply Bindings Method:
var viewModel = null;
$(document).ready(function () {
viewModel = new PlateTemplateViewModel();
viewModel.loadData();
});
I trying to develop a little app.
I have made a login view and it works, I am able to bind the data written by the user. After this, I show two select box. The first is binded with a list (correctly) and the second has to be bind with a list populated after a value is selected from the first.
I'm not able to read the value selected from the first list.
I have this html:
<div class="dx-fieldset">
<div class="dx-field">
<div class="dx-field-label">Rete</div>
<div class="dx-field-value"
data-bind="dxLookup: { dataSource: is_retistiSource, value: rete, displayExpr: 'NOME', title: 'Retisti associati', placeholder: 'Selezionare rete',
onSelectionChanged:setRete }" />
</div>
<div class="dx-field">
<div class="dx-field-label">Impianto</div>
<div class="dx-field-value"
data-bind="dxLookup: { dataSource: is_impiantiSource, value: impianto, displayExpr: 'NOME', title: 'Impianti associati', placeholder: 'Selezionare impianto' }" />
</div>
</div>
and this javascript:
OverviewAPP.afterLogin = function (params) {
var isReady = $.Deferred();
var viewModel = {
rete: ko.observable(""),
impianto: ko.observable(""),
is_retistiSource: OverviewAPP.listaReti,
is_impiantiSource: OverviewAPP.listaImpianti,
setRete: function () {
console.log(viewModel.rete);
var nRetisti = OverviewAPP.listaRetiImpianti.length;
for (i = 0; i < nRetisti; i++) {
if (OverviewAPP.listaRetiImpianti[i]["retista"]["NOME"] == this.rete)
{
OverviewAPP.listaImpianti = listaRetiImpianti[i]["listaImpianti"];
break;
}
}
is_impiantiSource = OverviewAPP.listaImpianti;
},
close: function () {
OverviewAPP.app.back();
}
};
return viewModel;
};
In the setRete function, with the line "console.log(viewModel.rete);", I see this output:
d(){if(0<arguments.length)return d.Wa(c,arguments[0])&&(d.X(),c=arguments[0],d.W()),this;a.k.Ob(d);return c}
Why? How can I bind and read the selected value?
UPDATE: I've done in this way, it works:
setRete: function (e) {
OverviewAPP.IDrete = e.value;
var nRetisti = OverviewAPP.listaRetiImpianti.length;
for (i = 0; i < nRetisti; i++) {
if (OverviewAPP.listaRetiImpianti[i]["retista"]["NOME"] == e.value["NOME"])
{
OverviewAPP.listaImpianti = OverviewAPP.listaRetiImpianti[i]["listaImpianti"];
break;
}
}
//ko.applyBindings(viewModel);
},
But I don't know how update my second list, "is_impiantiSource".
Observables are functions. That's why you get a function in the console. Call the rete function to get its value:
viewmodel.rete();
Also see the Knockout: Observables help topic that describes this (under "Reading and writing observables").
This is how you can obtain a new value. Then, you need to update the dependent lookup data source. For this, make the is_impiantiSource property an observable array:
is_impiantiSource: ko.observableArray(OverviewAPP.listaImpianti),
After this, modify it in setRene like:
viewModel.is_impiantiSource(OverviewAPP.listaImpianti)
Also see Observable Arrays for how to work with arrays in Knockout
i'm very new to knockout js and i'm trying my hands out on examples so i have this
<script>
var Country = function(name, population) {
this.countryName = name;
this.countryPopulation = population;
};
var viewModel = {
availableCountries : ko.observableArray([
new Country("UK", 65000000),
new Country("USA", 320000000),
new Country("Sweden", 29000000)
]),
selectedCountry : ko.observable() // Nothing selected by default
};
$(function(){ko.applyBindings(viewModel)});
</script>
and in the view
<p>Your country:
<select data-bind="options: availableCountries, optionsText: 'countryName', value: selectedCountry, optionsCaption: 'Choose...'"></select>
</p>
<div data-bind="visible: selectedCountry"> <!-- Appears when you select something -->
You have chosen a country with population
<span data-bind="text: selectedCountry() ? selectedCountry().countryPopulation : 'unknown'"></span>.
</div>
my question is i want the drop down to have a pre-selected value at initialization so i tried this
selectedCountry : ko.observable(new Country("UK", 65000000))
but its not working "Choose..." still appears as the pre-selected optionsText instead of "Uk" then i tried
selectedCountry : ko.observable(availableCountries[0])
but i keep getting this error
"Uncaught TypeError: Cannot read property '0' of undefined "
what am i doing wrongly and how do i fix it?
after you define the viewModel object, add the following:
viewModel.selectedCountry(viewModel.availableCountries()[0]);
you cant reference a value on an object as its being declared (at least i dont think you can), so you would need to do the assignment after the fact.
another option is to define your viewModel as a function:
var viewModel = function (){
var self = this;
self.availableCountries = ko.observableArray([
new Country("UK", 65000000),
new Country("USA", 320000000),
new Country("Sweden", 29000000)
]);
self.selectedCountry = ko.observable(self.availableCountries()[0])
};
I believe the solution to this is to set your selected value to "UK". As the value binding reads just the value not the entire object.
So when you set the object at index 0 that item isn't recognized as a value.
HTH
Update
My original post is pretty long - here's the tl;dr version:
How do you update all properties of a knockout model after a single property has changed? The update function must reference an observableArray in the viewModel.
-- More details --
I'm using KnockoutJS. I have a Zoo and a Tapir model and three observables in the viewmodel - zoos, tapirCatalog and currentTapir. The tapirCatalog is populated from the server and the currentTapir holds the value of whichever tapir is being edited at the time.
Here's what I'm trying to accomplish: A user has added a tapir from a list of tapirs to his/her zoo. When viewing the zoo, the user can edit a tapir and replace it with another. To do this a popup window is shown with a select form populated by tapir names and a span showing the currently selected GoofinessLevel.
So, when the select element changes this changes the TapirId in currentTapir. I want that to trigger something that changes the currentTapir's Name and GoofinessLevel.
I tried subscribing to currentTapir().GoofinessLevel but cannot get it to trigger:
function Zoo(data) {
this.ZooId = ko.observable(data.ZooId);
this.Tapirs = ko.observableArray(data.Tapirs);
}
function Tapir(data) {
this.TapirId = ko.observable(data.TapirId);
this.Name = ko.observable(data.Name);
this.GoofinessLevel = ko.observable(data.Name);
}
function ViewModel() {
var self = this;
// Initializer, because I get an UncaughtType error because TapirId is undefined when attempting to subscribe to it
var tapirInitializer = { TapirId: 0, Name: "Template", GoofinessLevel: 0 }
self.zoos = ko.observableArray([]);
self.tapirCatalog = ko.observableArray([]);
self.currentTapir = ko.observable(new Tapir(tapirInitializer));
self.currentTapir().TapirId.subscribe(function (newValue) {
console.log("TapirId changed to: " + newValue);
}); // This does not show in console when select element is changed
};
Oddly enough, when I subscribe to the Goofiness level inside the Tapir model I get the trigger:
function Tapir(data) {
var self = this;
self.TapirId = ko.observable(data.TapirId);
self.Name = ko.observable(data.Name);
self.GoofinessLevel = ko.observable(data.Name);
self.TapirId.subscribe(function (newId) {
console.log("new TapirId from internal: " + newId);
}); // This shows up in the console when select element is changed
}
I suspect that this is a pretty common scenario for people using KO but I haven't be able to find anything. And I've searched for a while now (it's possible that I may not have the correct vocabulary to search with?). I did find this solution, but he references the viewmodel from the model itself -- which seems like back coding since I would think the Tapir should not have any knowledge of the Zoo: http://jsfiddle.net/rniemeyer/QREf3/
** Update **
Here's the code for my select element (the parent div has data-bind="with: currentTapir":
<select
data-bind="attr: { id: 'tapirName', name: 'TapirId' },
options: $root.tapirCatalog,
optionsText: 'Name',
optionsValue: 'TapirId',
value: TapirId">
</select>
It sounds like what you need to do is bind the select to an observable instead of the Id
<select
data-bind="attr: { id: 'tapirName', name: 'TapirId' },
options: $root.tapirCatalog,
optionsText: 'Name',
optionsValue: 'TapirId',
value: currentTapir">
</select>