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>
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-->
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.
I did a lot of searching and tried eleventy-billion different Google search combinations, but all I can find on this issue is how to set a default option in a select box.
I have a page where an admin can select a user from a list of users, and then Angular JS grabs the user_id of that user (using ng-change), sends it to the DB via POST, and then the goal is to change the value of the other inputs to the values from the DB. I have the values, but running into a hitch when using that value to get my state select box to change to the user's state.
The JS in question:
$scope.getUserInfo = function(user_id) {
this.user_id = user_id;
$http.post("lib/scripts/managing_user.php", {func: 'retrieve_user_info', user_id: this.user_id}).
success(function(data) {
console.log(data);
$scope.is_active = data['0']['active'];
//Interacts with the ng-checked directive. It takes a bool, and data returns 0 or 1 in active.
$scope.username = data['0']['username'];
//Assigns the data value to the ng-model directive with the value username
//Have to treat data as a 2D array because that is how it is returned
$scope.email = data['0']['email'];
$scope.fName = data['0']['first_name'];
$scope.lName = data['0']['last_name'];
$scope.schoolList = data['0']['school_id']; (<-Does not work)
}).
I accomplished the same thing using jQuery before. Here is the code if it helps you see what I want to do.
if ($("#school").children("option:selected"))
$("#school").children("option:selected").attr("selected", "false");
$("#school #" + this['school_id'] + "").attr("selected", "true");
Here is the Select Box that I want changed.
<div class="row-fluid">
<span class="adduser_heading">School:</span>
<select id="school" class="adduser_input" ng-model="user.schoolList" ng-options="name.school_name for (key, name) in schoolList" ng-disabled="is_disabled" name="school" style="width: 246px;"></select>
</div>
I get the list of schools from the DB, and that populates that list. After selecting a user, I want this select box to change to that user's school. The ultimate goal is for the admin to be able to change the selected school and submit it, changing the school in the DB.
Hope I described the problem adequately. Basically: I want to select an option in a select box from the JS using Angular JS.
Edit: As per the advice of oware, I created a function that gets just the school name from the object array and returns it to $scope.user.schoolList. Unfortunately, that did not work.
$scope.user.schoolList = $scope.findInSchoolList(data['0']['school_id']);
$scope.findInSchoolList = function(school_id) {
var school_id = school_id;
var school;
school = $scope.schoolList[school_id];
school = school['school_name'];
return school;
};
And here is the format of what is returned from the DB with regards to school. I don't really want to post "data" since that has the information of an actual person. Basically, the information with regards to school is what is below.
school_id: "106"
school_name: "Central Campus High School"
Your ng-model is set to user.schoolList, while you're assigning the default value to $scope.schoolList.
It should be $scope.user.schoolList instead.
If you want to use the find function, you still need to return the right object, not just the name; and you need to fix your function. So something like this should work:
$scope.findInSchoolList = function(school_id) {
for(var i = 0; i < $scope.schoolList.length; i++) {
if ($scope.schoolList[i].school_id == school_id) {
return $scope.schoolList[i];
}
}
};
Here's a working example:
angular.module('app', [])
.controller('Ctrl', function($scope) {
var findInSchoolList = function(school_id) {
for (var i = 0; i < $scope.schoolList.length; i++) {
if ($scope.schoolList[i].school_id == school_id) {
return $scope.schoolList[i];
}
}
};
$scope.schoolList = [{
school_id: "1",
school_name: "Central Campus High School"
}, {
school_id: "106",
school_name: "Another High School"
}, {
school_id: "231",
school_name: "Yet Another High School"
}, {
school_id: "23",
school_name: "The Correct High School"
}, {
school_id: "2",
school_name: "Last High School"
}]
$scope.user = {
schoolList: findInSchoolList(23)
}
})
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div class="row-fluid" ng-app="app" ng-controller="Ctrl">
<span class="adduser_heading">School:</span>
<select id="school" class="adduser_input" ng-model="user.schoolList" ng-options="name.school_name for (key, name) in schoolList" ng-disabled="is_disabled" name="school" style="width: 246px;"></select>
</div>
you have to select the item from the array that populated the list, for example, if you have:
$scope.school_list = [{id:1, name:'harvard'}, {id:2, name:'other'}]
and you want to select with:
$scope.user.schoolList = {id:1, name:'harvard'}
it won't work, you have to make a fucntion that finds the element in the array and then assign it to the $scope.user.schoolList variable (that is bound to the ng-model of your list)
you have to do something like this:
$scope.user.schoolList = findInSchoolList({id:1, name:'harvard'})
and it will select the item from the select list
hope it helps
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
The select lists are not rendering with the correct option selected. I've tried this a number of different ways including a computed selected observable (this.selected = ko.computed(return parseInt(selected(), 10) == this.id; )) and find in array functions.
In production, the dataArea elements would be populated with server side data. Using the divs with "data-" attributes keeps server side and client side scripting separate (I find this helps the designers).
A record would be displayed in non edit mode first with the option to edit by clicking the edit button. In edit mode, the initial values for the record appear in input controls. You would have the option to say, choose another customer and the having the form load new associated projects. Loading a new customer would reset the project list as expected.
So while loading a new customer would work well, its the transition to editing the current values that is causing an issue. The selected project needs to appear in the drop down list. If a new customer is chosen, the list populates with new options and no defaults are required.
http://jsfiddle.net/mathewvance/ZQLRx/
* original sample (please ignore) http://jsfiddle.net/mathewvance/wAGzh/ *
Thanks.
<p>
issue: When the select options are read, the inital value gets reset to the first object in the options. How do I keep the original value selected when transitioning to edit mode?
</p>
<div>
<h2>Edit Quote '1001'</h2>
<div class="editor-row" data-bind="with: selectedCustomer">
<label>Customer</label>
<div data-bind="visible: !$root.isEditMode()">
<span data-bind="text: CompanyName"></span>
</div>
<div data-bind="visible: $root.isEditMode()">
<input type="radio" name="customerGroup" value="1" data-bind="value: id"> Company Name 1
<input type="radio" name="customerGroup" value="2" data-bind="value: id"> Company Name 2
</div>
</div>
<div class="editor-row">
<label>Project</label>
<div data-bind="visible: !isEditMode()">
<span data-bind="text: selectedProject.Name"></span>
</div>
<div data-bind="visible: isEditMode()">
<select data-bind="options: selectedCustomer().projects, optionsText: 'Name', value: selectedProject"></select>
</div>
</div>
<div>
<button data-bind="click: function() { turnOnEditMode() }">Edit</button>
<button data-bind="click: function() { turnOffEditMode() }">Cancel</button>
</div>
</div>
<hr/>
<div data-bind="text: ko.toJSON($root)"></div>
function ajaxCallGetProjectsByCustomer(customerId) {
var database = '[{"CustomerId": 1, "Name":"Company Name 1", "Projects": [ { "ProjectId": "11", "Name": "project 11" }, { "ProjectId": "12", "Name": "project 12" }, { "ProjectId": "13", "Name": "project 13" }] }, {"CustomerId": 2, "Name": "Company Name 2", "Projects": [ { "ProjectId": "21", "Name": "project 21" }, { "ProjectId": "22", "Name": "project 22" }, { "ProjectId": "23", "Name": "project 23" }] }]';
var json = ko.utils.parseJson(database);
//console.log('parseJson(database) - ' + json);
//ko.utils.arrayForEach(json, function(item) {
// console.log('CustomerId: ' + item.CustomerId);
//});
return ko.utils.arrayFirst(json, function(item){
return item.CustomerId == customerId;
});
}
var Customer = function(id, name, projects) {
var self = this;
this.id = ko.observable(id);
this.CompanyName = ko.observable(name);
this.projects = ko.observableArray(ko.utils.arrayMap(projects, function(item) {
return new Project(item.ProjectId, item.Name);
}));
};
Customer.load = function(id) {
var data = ajaxCallGetProjectsByCustomer(id);
var customer = new Customer(
data.CustomerId,
data.Name,
data.Projects);
};
var Project= function(id, name) {
this.id = id;
this.Name = ko.observable(name);
};
var QuoteViewModel = function () {
var self = this;
$customerData = $('#customerData'); // data from html elements
$projectData = $('#projectData');
// intial values to display from html data
var customer = new Customer (
$customerData .attr('data-id'),
$customerData .attr('data-companyName'),
[{"ProjectId": $projectData .attr('data-id'), "Name": $projectData .attr('data-name')}]
)
this.selectedCustomer = ko.observable(customer);
this.selectedProject = ko.observable($projectData.attr('data-id'));
this.isEditMode = ko.observable(false);
this.selectedCustomer.subscribe(function(){
// todo: load customer projects from database api when editing
});
this.turnOnEditMode = function() {
var customerId = self.selectedCustomer().id();
console.log('customerId: ' + customerId);
Customer.load(customerId);
self.isEditMode(true);
};
this.turnOffEditMode = function() {
self.isEditMode(false);
};
};
var viewModel = new QuoteViewModel();
ko.applyBindings(viewModel);
One the initial value you load
this.dongle = ko.observable($dongleData.attr('data-id'));
This would be the string value "3". Where as the dongle html select element is actually saving/expecting to retrieve the object { "Id": "3", "Name": "dongle 3" }.
Here is a working version that gets the correct initial values and allows editing.
http://jsfiddle.net/madcapnmckay/28FVr/5/
If you need to save the a specific value and not the whole dongle/widget object, you can use the optionsValue attribute to store just the id. Here is it working in the same way.
http://jsfiddle.net/madcapnmckay/VnjyT/4/
EDIT
Ok I have a working version for you. I'll try to summarize everything I changed and why.
http://jsfiddle.net/madcapnmckay/jXr8W/
To get the customer info to work
The Customer name was not stored in the ajaxCallGetProjectsByCustomer json so when you loaded a customer there was no way to determine the new name from the data received. I added a Name property to each customer in the json with name "Company Name 1" etc.
To get the projects collection to work
The problem here was as stated originally with the dongles. You initialize the selectedProject observable with $projectData.attr('data-id') which equates to string value of 13. This is incorrect as the select list is configured in such a way that it actually saves/expects to receive the project object itself. Changing this id assignment to an object assignment made the initial value of project work correctly.
var project = ko.utils.arrayFirst(customer.projects(), function(project){
return project.id == Number($projectData.attr('data-id'));
});
this.selectedProject = ko.observable(project);
FYI there was a minor error in the html, the selectedProject.Name needed to be selectedProject().Name. No big deal.
I'm sure you could have figured out those pretty easily. The next bit is where the real issue is. You reload the Customer every time the edit button is clicked. This seems strange and you may want to reconsider that approach.
However what happens is you load a customer object from the server by id. Assign it to the selectedCustomer observable, this actually works fine. But then because the drop down is bound to selectedCustomer().projects and viewModel.selectedProject it expects that selectedProject is a member of selectedCustomer().projects. In the case of objects the equality operator is assessing whether the references match and in your case they do not because the original selectedProject was destroyed with its associated customer when you overwrote the selectedCustomer value. The fact that the ids are the same is irrelevant.
I have put in place a hack to solve this.
var oldProjectId = viewModel.selectedProject().id;
viewModel.selectedCustomer(customer);
var sameProjectDifferentInstance = ko.utils.arrayFirst(customer.projects(), function(project){
return project.id == oldProjectId;
});
viewModel.selectedProject(sameProjectDifferentInstance || customer.projects()[0]);
This saves the old projectId before assigning the new customer, looks up a project object in the new customer object and assigns it or defaults to the first if not found.
I would recommend rethinking when you load objects and how you handle their lifecycle. If you hold the current objects it memory with a full list of projects included you don't need to reload them to edit, simply edit and then send the update back to the server.
You may find it easier to hold json from the server in js variables instead of html dom elements. e.g.
<script>var projectInitialData = '#Model.ProjectInitialData.toJSON()';</script>
Hope this helps.