Unable to view data on an oservable - javascript

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

Related

Knockout JS update view from json model

Please refer to this question as it helped solving 50% of my issue:
Knockout Mapping reading JSON
the other 50% of issue is updating view, if you call ko.applyBindings(viewModel); twice, you get an error Uncaught Error: You cannot apply bindings multiple times to the same element.
No one online ever proposed a solution to this, even on the official knockout site they mentioned:
// Every time data is received from the server:
ko.mapping.fromJS(data, viewModel);
which is not working either. Anyone knows what the proper method is to update my view each time I fetch new data, knowing that the view is already initialized via ko.applyBindings(viewModel);?
Edit:
HTML :
<select class="input-short artist-list" data-bind="foreach: model">
<option value="1" selected="selected" data-bind="text: name"></option>
</select>
JS
var viewModel = {
model: ko.observableArray()
};
$(window).load(function(){
fetchArtists();
})
function fetchArtists() //this function fetches all artists
{
// load data
$.post( "../db/fetch/", { table: "artists"})
.done(function( data ) {
// artists=JSON.parse(data);
data=JSON.parse(data);//data is array of objects e.g [{name:"xxx"},{name:"yyy"}]
artists.model = ko.mapping.fromJS(data);
ko.applyBindings(artists);
});
}
This should do what you want to do:
JS
var viewModel = {
model: ko.observableArray()
};
$(window).load(function(){
ko.applyBindings( viewModel );
fetchArtists();
})
function fetchArtists()
{
$.post( "../db/fetch/", { table: "artists" } ).done(function( data ) {
ko.mapping.fromJSON( data, {}, viewModel.model );
});
}
As #SVSchmidt mentioned, you can only call ko.applyBindings() once per element. So you will ko.applyBindings once (on page load/ready most likely) and then either update the observableArray (model) directly or use the mapping plugin.
Updating the observableArray (model) directly will mean the values will be plain values. But if you wanted those values to be observable, then ko.mapping will be appropriate.
So to use the mapping plugin, you can call ko.mapping.fromJS or ko.mapping.fromJSON if you have raw JSON data.
The parameters for fromJS and fromJSON will be, in order:
incoming data that you want to map (in your case data)
options object that you can use to control mapping (empty object for now)
destination viewmodel or viewmodel property that you want to update (in your case viewModel.model
Here is a working demo that shows you how this works in action: http://plnkr.co/edit/4g1izaLYraBjganjX2Ue?p=preview
In knockout, a ViewModel is applied to the view once (applyBindings). Everytime the observables bind with data-bind are updated (e.g. assigning new data to them), the view is re-rendered. Your mistake is binding a non-observable (model) and re-defining artists.model with every function call.
You should do it the following way:
var viewModel = {
artists: ko.observableArray()
};
$(window).load(function(){
fetchArtists();
});
function fetchArtists()
{
// load data
$.post( "../db/fetch/", { table: "artists"})
.done(function( data ) {
viewModel.artists(JSON.parse(data)); // assign new values to artists observable
});
}
HTML
<select class="input-short artist-list" data-bind="foreach: artists">
<option value="1" selected="selected" data-bind="text: name"></option>
</select>
<script>
ko.applyBindings(viewModel); // apply ViewModel
</script>

How to create own angular service with XHR properly?

I am very new about AngularJS things. Need to do file upload with other datas in form, I found some scripts and angular plugins but I am using my own service calls $xhr. I was able to send file but i got error, bug(not real error-bug, i just named like that) or i can not use AngularJS properly. Here it is:
.
JS
var app = angular.module('ngnNews', []);
app.factory('posts', [function () {...}]); // I reduced the codes
app.factory('$xhr', function () {
var $xhr = { reqit: function (components) { ... //My Xml HTTP Request codes here }}
return $xhr;
});
app.controller('MainCtrl', ['$http','$scope','$xhr','posts',
function ($http, $scope, $xhr, posts) {
$scope.posts = posts.posts;
$scope.files = [];
var newPost = { title: 'post one', upvotes: 20, downvotes: 5 };
$scope.posts.push(newPost);
$scope.addPost = function () {
$xhr.reqit({
form: document.getElementById('postForm'),
callbacks: {
success: function (result) {
if (result.success) {
console.log($scope.posts); //[FIRST OUT]
$scope.posts.push(result.post);
$scope.title = '';
console.log($scope.posts); //[SECOND OUT]
}
}
},
values: { upvotes: 0, downvotes: 0 },
files: $scope.files
});
...
}
}]);
.
HTML
<form action="/Home/FileUp" id="postForm" method="post" enctype="multipart/form-data">
<div class="form-group input-group">
<span class="input-group-addon">Post Title</span>
<input name="title" class="form-control" type="text" data-ng-model="title" />
</div>
<ul>
<li ng-repeat="file in files">{{file.name}}</li>
</ul>
<button class="btn btn-primary" type="button" data-ng-click="addPost()">Add New</button>
</form>
SCREEN
Sample post displayed in list
.
PROBLEMS
When I click first time Add New button everything works well until $scope.posts.push(result.post);. In console, [SECOND OUT] is here:
First object has $$hashKey but second object which sent from server(added by $scope.posts.push(result.post); function) doesn't have. I want to know why is this happening? But it's not only weird thing, when I second time click Add New button, everything completed successfully (No new logs in console, adding new post to list shown screen image above).
MAIN PROPLEM
I pushed returned value from the server but post list(in screen) is not affected when first click.
QUESTIONS
- What is happening? or
- What am I doing wrong? Thanks for any explanation.
You are doing nothing wrong with respect to $$hashkey if that is your concern. When you use ng-repeat with array of objects angular by default attaches a unique key to the items which is with the property $$hashkey. This property is then used as a key to associated DOM elements with the corresponding item in the array by identity. Moving the same object in array would move the DOM element in the same way in the DOM. You can avoid this (addition of additional property on the object by angular) by using track by with ng-repeat by providing a unique key on the object or a mere $index. So with that instead of creating a unique key and attaching it to $$haskey property angular will use the unique identifier you have provided to associate the DOM element with the respective array item.
ng-repeat="post in posts track by $index"
or (id you have a unique id for each of the object in the array, say id then)
ng-repeat="post in posts track by post.id"
And since you say you are using my xml http request code here, i am assuming it is not within the angular context so you would need to manually perform the digest cycle by using $scope.$apply() is on of those ways.
$scope.addPost = function () {
$xhr.reqit({
form: document.getElementById('postForm'),
callbacks: {
success: function (result) {
if (result.success) {
$scope.posts.push(result.post);
$scope.title = '';
$scope.$apply();//<-- here
}
}
},
But ideally you could wrap your xhr implementation with a $q and if you pass $q promise from your api, you wont need to perform a manual $scope.$apply() everywhere. Because $q promise chaining will take care of digest cycle invocation.

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.

Update Knockout Observable and Dropdown Select

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

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

Categories

Resources