Update Knockout Observable and Dropdown Select - javascript

I have an ASPX page with a dropdown select bound with Knockout.JS. On the page load I check the url for a parameter and update the view if their is a parameter which you can see in my API. I've changed the API to leave out unnecessary code because it returns the value needed. My problem is that I cannot get my observable SelectedView to update to "Notes". Any advice?
ASPX:
<asp:DropDownList runat="server" data-bind="value: SelectedView" id="viewselect">
<asp:ListItem>Select A View</asp:ListItem>
<asp:ListItem>Notes</asp:ListItem>
<asp:ListItem>Credit Manager</asp:ListItem>
</asp:DropDownList>
View Model:
function CustomerViewModel() {
this.self = this;
self.SelectedCustomer = ko.observable();
self.SelectedView = ko.observable();
}
API:
$(document).ready(function () {
var custnmbr = "123456";
if (custnmbr != "") {
var notes = "Notes";
self.SelectedView(notes);
}
});

I guess if you look in console you 'll get error:
Uncaught TypeError: Object [object global] has no method 'SelectedView'
Because in your $(document).ready you are using the object self which is only defined inside CustomerViewModel().
To solve this, you need to call .SelectedView(notes); on the object instance you are passing to ko.applyBindings,
UPDATE
for example:
function CustomerViewModel() {
this.self = this;
self.SelectedCustomer = ko.observable();
self.SelectedView = ko.observable();
}
var customerObj=new CustomerViewModel();
ko.applyBindings(customerObj);
// later in your code.
customerObj.SelectedView(notes);

Related

Unable to view data on an oservable

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();
});

AngularJS: TypeError: undefined is not a function with chained selects

I have two select boxes, options for one of the boxes are loaded right away, and the second (child) select will get its options from a callback that queries an API. It is not an option to pre-load all possible options because there are 4200 records that would be loaded without the parent's selected value.
When the ng-change event of the parent box fires, a call is made:
function CertificateSearchCtrl($q, CPSIAService) {
var vm = this;
vm.products = [];
vm.categories = [];
vm.certficate = {};
vm.categoryselect = {};
vm.productselect = {};
I can call this via ng-init, or directly in the controller on first load
vm.loadCategories = function() {
CPSIAService.getCategories().then(function(results){
vm.categories = results;
});
};
OR I can call this in the same fashion (ng-init or direct via controller)
vm.findProducts = function(data) {
CPSIAService.getProductsByCategory(data.id).then(function(results){
vm.products = results;
});
};
...
But I can't call the two together at all, either through ng-change or direct through controller forcing a category ID into the findProducts() call.
This should, in turn, allow the child select to be populated with the "products" array. The controlling html (which is output via a directive) is this:
<div class="small-12 medium-6">
<select ng-model="vm.categoryselect" ng-change="vm.findProducts(vm.categoryselect)" ng-options="categories.category for categories in vm.categories track by categories.id" ng-cloak>
<option value="">(choose a category)</option>
</select>
</div>
<div class="small-12 medium-6">
<select ng-model="vm.productselect" ng-change="vm.loadCertificate(vm.productselect)" ng-show="vm.products.length>0" ng-options="products.description for products in vm.products track by products.sku" ng-cloak>
<option value="">(select a product)</option>
</select>
</div>
Even if I try to load the options for the child select initially (rather than through the ng-change event) - I get the same error. Here is the Chrome stack trace:
TypeError: undefined is not a function
at render (angular.js:25905)
at Scope.$get.Scope.$digest (angular.js:14280)
at Scope.scopePrototype.$digest (hint.js:1468)
at Scope.$get.Scope.$apply (angular.js:14493)
at Scope.scopePrototype.$apply (hint.js:1478)
at HTMLSelectElement.selectionChanged (angular.js:25657)
at HTMLSelectElement.eventHandler (angular.js:3011)angular.js:11598 (anonymous function)angular.js:8548 $getangular.js:14282 $get.Scope.$digesthint.js:1468 scopePrototype.$digestangular.js:14493 $get.Scope.$applyhint.js:1478 scopePrototype.$applyangular.js:25657 selectionChangedangular.js:3011 eventHandler
Here is a sample of the JSON data in question. I've linted/validated it and it is fine.
[{"sku":"2004","description":"ADHSVE PAPR BLK BDR8CT-12"},{"sku":"2005","description":"ADHSVE PAPR BLU BDR8CT-12"},{"sku":"2006","description":"ADHSVE PAPR RED BDR8CT-12"},{"sku":"0043630-5987","description":"BORD 50 CS ASST 60 CT-1"},{"sku":"51671","description":"SLFSTK BORDER BLK 2X12-12"},{"sku":"51672","description":"SLFSTK BORDER BLU 2X12-12"},{"sku":"51673","description":"SLFSTK BORDER RED 2X12-12"}]
Help!
I have noticed that I can, in fact load my child select options only if I don't attempt to make two calls to my service at one time. Maybe I'm misunderstanding promises? I thought they resolve with the .then() function, but it errors out when I try to make the second one complete, even though the API call is fine and data does come back as expected (see JSON above)
JQuery does not affect the error - same reproduction with or without jQuery included.
found the solution guys. In my service, I had this:
function CPSIAService($q, Restangular) {
var deferred = $q.defer();
var CPSIAService = {};
CPSIAService.getProductsByCategory = function(params) {
//original call
// var response = Restangular.one('compliance/products/by/category',params);
var response = Restangular.one('products.json'); //params from category would go here
response.getList().then(function(data) {
deferred.resolve(data);
});
return deferred.promise;
};
CPSIAService.getCategories = function() {
//original call
//var response = Restangular.all('compliance/categories/all');
var response = Restangular.all('categories.json');
response.getList().then(function(data) {
deferred.resolve(data);
});
return deferred.promise;
};
return CPSIAService;
}
Specifically, notice this at the top of the service:
var deferred = $q.defer();
If I were to make a call to the service after initial page load, the error would occur because I wasn't deferring the promise in the actual function I was calling. The solution was to go from this:
CPSIAService.getProductsByCategory = function(params) {
var response = Restangular.one('compliance/products/by/category',params);
response.getList().then(function(data) {
deferred.resolve(data);
});
return deferred.promise;
};
to this:
CPSIAService.getProductsByCategory = function(params) {
var deferred = $q.defer(); //defer here because we're calling it from the ng-change event fire
var response = Restangular.one('compliance/products/by/category',params);
response.getList().then(function(data) {
deferred.resolve(data);
});
return deferred.promise;
};
And now it works like a charm.
Had the same problem and solution was to update angular-mocks.js to the matching version as per this answer.

KnockoutJS Validation with dynamic observables

I am using this plugin https://github.com/ericmbarnard/Knockout-Validation and i am trying to validate an object that is loaded dynamically.
Javascript:
function VM() {
var self = this;
// This is a static observable, just to ensure that basic validation works fine.
self.static = ko.observable();
self.static.extend({required: true});
// This is the observable that will be updated to my model instance.
self.person = ko.observable({});
// This is an handler for manual trigger.
// I'm not even sure this is needed.
self.a = function(){
self.errors.showAllMessages();
self.staticErrors.showAllMessages();
}
// Here i'm loading current person from somewhere, i.e. a rest service.
self.load = function() {
// Update observable
self.person(new Model());
// Define validation rules
self.person().name.extend({required: true});
self.person().email.extend({required: true});
// Set person data
self.person().name('Long');
self.person().email('John');
// Set validators
self.errors = ko.validation.group(self.person);
self.staticErrors = ko.validation.group(self.static);
}
}
// Just a test model.
function Model() {
this.name = ko.observable();
this.email = ko.observable();
}
ko.validation.init();
var vm = new VM();
ko.applyBindings(vm);
Markup
<ul>
<li>1. Hit "Load"</li>
<li>2. Hit "Show errors", or maunally change input data.</li>
</ul>
<button data-bind='click: load'>Load</button>
<br/>
<h1>This is working properly.</h1>
<input type='text' data-bind='value: static' />
<br/>
<h1>This is not working.</h1>
<input type='text' data-bind='value: person().name' />
<input type='text' data-bind='value: person().email' />
<br/>
<button data-bind='click: a'>Show errors</button>
Fiddle
http://jsfiddle.net/qGzfr/
How do I make this work?
The validation plugin only gets applied in your bindings only if by the time when the binding is parsed by Knockout your properties are validate.
In different words: you cannot add validation to a property after the property was bound on the UI.
In your example you are using an empty object in self.person = ko.observable({}); as a default value, so when Knockout executes the data-bind='value: person().name' expression you don't have a name property so the validation won't work even if you later add the name property to your object.
In your example you can solve this with changing your Model constructor to include the validation rules:
function Model() {
this.name = ko.observable().extend({required: true});
this.email = ko.observable().extend({required: true});
}
And use an empty Model object as the default person:
self.person = ko.observable(new Model());
And when calling Load don't replace the person object but update its properties:
self.load = function() {
// Set person data
self.person().name('Long');
self.person().email('John');
}
Demo JSFiddle.
Note: Knockout does not always handles well if you replace whole object like self.person(new Model()); so it is anyway a better practice to only update the properties and not throw away the whole object.
A different solution would be to use the with binding because inside the with binding KO will reevaluate the bindings if the bound property changes.
So change your view:
<!-- ko with: person -->
<input type='text' data-bind='value: name' />
<input type='text' data-bind='value: email' />
<!-- /ko -->
In this case you need to use null as the default person:
self.person = ko.observable();
And in your Load you need to add the validation before assigning your person property so by the time KO applies the bindings your properties have the validation:
self.load = function() {
var model = new Model()
model.name.extend({required: true});
model.email.extend({required: true});
self.person(model);
// Set person data
self.person().name('Long');
self.person().email('John');
}
Demo JSFiddle.
I was able to make it work, this are the changes required:
<head>
<script type="text/javascript" src ="knockout-2.3.0.js"></script>
<script type="text/javascript" src ="knockout.validation.min.js"></script>
</head>
<body>
<!-- no changes -->
<script>
function VM() { ... }
function Model() { ... }
// ko.validation.init();
var vm = new VM();
ko.applyBindings(vm);
</script>
</body>
What was done?
Include KnockoutJS and the validation plugin.
Bind after the elements have been added. Remeber that HTML pages are parsed from top to bottom.
How could you tell? In the console this errors appeared:
Cannot read property 'nodetype' of null
and
Cannot call method 'group' of undefined

Update related properties in response to observable change

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>

knockout.js binding error within foreach block

I am new to knockout.js and am having a problem with binding within a foreach section. I am receiving the error:
Uncaught Error: Unable to parse bindings.
Message: ReferenceError: hideSearchElements is not defined;
Bindings value: click: hideSearchElements
Here is an exert of the html:
<div id="searchResults" data-bind="visible: searchIsVisible">
<label id = "lblSearchResults">select a template:</label>
<div data-bind="foreach: titles">
<div data-bind="text: Title"></div>
<div data-bind="click: hideSearchElements">hide</div>
</div>
And an exert from the viewModel:
var viewModel = function () {
this.searchIsVisible = ko.observable(true);
this.showSearchElements = function () {
this.searchIsVisible(true);
};
this.hideSearchElements = function (
this.searchIsVisible(false); }
}
return new viewModel();
I have both showSearchElements and hideSearchElements working fine outside of the foreach block but when inside it, I get the error.
If I add $parent.hideSearchElements I can bind but then get an error saying:
Uncaught TypeError: Object # has no method 'searchIsVisible'
.
I have probably have two distinct issues but thought the detail may help :)
I'm keen to understand what's going on here? Can anyone help please?
A link to the relevant page in the documentation would also be very helpful - I'm reading through that now.
Thanks
You was right when use $parent.hideSearchElements because hideSearchElements function is in a parent context. You got exception because when knockout calls your function this has another context. You have to use closure to store this pointer. Update your view model as follow:
var viewModel = function () {
var self = this;
self.searchIsVisible = ko.observable(true);
self.showSearchElements = function () {
self.searchIsVisible(true);
};
self.hideSearchElements = function (
self.searchIsVisible(false); }
}

Categories

Resources