I have a list of attachments on a page which is generated using a jQuery $.ajax call and Knockout JS.
My HTML looks like (this is stripped back):
<tbody data-bind="foreach: attachments">
<tr>
<td data-bind="text: Filename" />
</tr>
</tbody>
I have a function that gets the list of attachments which is returned as a JSON response:
$(function () {
getFormAttachments();
});
function getAttachments() {
var request = $.ajax({
type: "GET",
datatype: "json",
url: "/Attachment/GetAttachments"
});
request.done(function (response) {
ko.applyBindings(new vm(response));
});
}
My view model looks like:
function vm(response) {
this.attachments = ko.observableArray(response);
};
There is a refresh button that the use can click to refresh this list because over time attachments may have been added/removed:
$(function () {
$("#refresh").on("click", getAttachments);
});
The initial rendering of the list of attachments is fine, however when I call getAttachments again via the refresh button click the list is added to (in fact each item is duplicated several times).
I've created a jsFiddle to demonstrate this problem here:
http://jsfiddle.net/CpdbJ/137
What am I doing wrong?
Here is a fiddle that fixes your sample. Your biggest issue was that you were calling 'applyBindings' multiple times. In general you will call applyBindings on page load and then the page will interact with the View Model to cause Knockout to refresh portions of your page.
http://jsfiddle.net/CpdbJ/136
html
<table>
<thead>
<tr><th>File Name</th></tr>
</thead>
<tbody data-bind="foreach: attachments">
<tr><td data-bind="text: Filename" /></tr>
</tbody>
</table>
<button data-bind="click: refresh">Refresh</button>
javascript
$(function () {
var ViewModel = function() {
var self = this;
self.count = 0;
self.getAttachments = function() {
var data = [{ Filename: "f"+(self.count*2+1)+".doc" },
{ Filename: "f"+(self.count*2+2)+".doc"}];
self.count = self.count + 1;
return data;
}
self.attachments = ko.observableArray(self.getAttachments());
self.refresh = function() {
self.attachments(self.getAttachments());
}
};
ko.applyBindings(new ViewModel());
});
--
You may also want to look at the mapping plugin - http://knockoutjs.com/documentation/plugins-mapping.html. It can help you transform JSON into View Models. Additionally it is able to assign a property to be the "key" for an object... this will be used to determine old vs new objects on subsequent mappings.
Here is a fiddle I wrote a while back to demonstrate a similar idea:
http://jsfiddle.net/wgZ59/276
NOTE: I use 'update' as part of my mapping rules, but ONLY so I can log to the console. You would only need to add this if you wanted to customize how the mapping plugin updated objects.
Related
I explain myself: I have a Sharepoint custom list and I'm using AngularJS to call this list. With the data I obtain from the list, I'm making a "single bar chart" for each item in this list. I'm using jquery.lineProgressbar.js to make the charts.
I'm doing the table with ng-repeat. And I need to provide a different ID name to each "chart div", otherwise the jquery.lineProgressbar.js won't work. Here's my HTML:
<table>
<tr>
<td>Name</td>
<td>Productivity percentage</td>
</tr>
<tr ng-repeat="item in items">
<td>{{item.Name}}</td>
<td>
<!-- The "ng-attr-id" provides the div a different ID depending the name they introduce. i.e.: "chart-Person1" -->
<div ng-attr-id="{{'chart-' + item.Name}}" data-percentage="{{item.Productivity_x0020_percentage}}"></div>
</td>
</tr>
</table>
and my main problem, the SCRIPT:
<script>
//I need to call each chart, one by one like this:
chartFunction('chart-Person1');
chartFunction('chart-Person2');
chartFunction('chart-Person3');
chartFunction('chart-Person4');
//and I need to make this automatically,
//because people will submit new items whenever they want,
//and I can't be updating the script each time someone uploads something.
function chartFunction(elementID) {
var dataPercentage = $("#" + elementID).data('percentage');
//this calls the chart code
$("#" + elementID).LineProgressbar({
//it says that the div selected will have a "percentage" equals to the percentage they wrote in the list.
percentage: dataPercentage
});
}
</script>
I have a Plunker. It is a little different because it has a function which runs the charts only when they're in the viewport, and it doesn't use AngularJS. But it's only so you can see how it works: my Plunker
So, as I said in my code, I need to call each new element added to the sharepoint list, but I can't be creating new calls in my code each time someone uploads an item. Thanks in advance for your help.
I've found the solution to this.
I needed to do it directly in the call of the list (in the Angular code).
<script>
var myApp = angular
.module('myApp', [])
.controller('myController', function ($scope, $http) {
//first I call the Sharepoint List with AngularJS
$http({
method: 'GET',
url: "SiteURL/_api/web/lists/getByTitle('Employees List')/items?$select=*",
headers: { "Accept": "application/json;odata=verbose" }
}).then(function onSuccess(response) {
//If the call is successful I create an empty Array called "peopleArray"
//Here I will store the names of the divs which will run the chart's code
$scope.items = response.data.d.results;
var theItems = $scope.items,
peopleArray = [];
//I run a loop to go through all the items in the Sharepoint list
//I assign a name for each person in the "peopleArray"
for(var i=0; i<theItems.length; i++) {
var currentItem = theItems[i];
peopleArray.push('chart-' + currentItem.Name);
};
//I run another loop, this one goes through the "peopleArray"
//each item executes the function below with the div name assigned in the previous loop
for(var z=0; z<peopleArray.length; z++) {
chartFunction(peopleArray[z]);
}
//and this is the function to make the charts for each person
function chartFunction(elementID) {
var dataPercentage = $("#" + elementID).data('percentage');
$("#" + elementID).LineProgressbar({
percentage:dataPercentage
});
}
}).catch(function onError(response) {
//In case of Error, it will pop an alert
alert("Error! The charts can't be loaded.");
});
});
</script>
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 have built a view model representing time slices with the following structure:
function TimeslotViewModel() {
this.timeslots = ko.observableArray();
this.updateTimeslots = function(timeslots) {
this.timeslots.destroyAll();
}
this.clearTimeslots = function() {
this.timeslots.destroyAll();
}
this.addTimeslot = function(timeslot) {
this.timeslots.push(timeslot);
}
}
function Timeslot(time, available) {
this.time = time;
this.available = available;
}
I'm trying to render this in a tabular format like so:
<div class="container">
<table class="table">
<thead>
<tr><th>Time</th><th>Status</th>
</thead>
<tbody data-bind="foreach: timeslots">
<td data-bind="text: time"></td>
<td data-bind="text: available"</td>
</tbody>
</table>
</div>
I've bound on page load:
$(function() {
ko.applyBindings(new TimeslotViewModel());
});
I'm trying to populate this table based on the callback result from an ajax call, but it doesn't seem to be working as expected. Here is what I tried:
$.getJSON(
"/myAjaxCall",
function (jsonData) {
var timeslotViewModel = new TimeslotViewModel();
timeslotViewModel.clearTimeslots();
$.each(jsonData, function (i, ts) {
var tsData = JSON.parse(ts);
var timeslot = new Timeslot(tsData.time, tsData.booked);
timeslotViewModel.addTimeslot(timeslot);
});
});
Unfortunately, I'm not seeing my view model's array get populated at all from this code. What is the right way to populate a view model based on a callback function's response?
You are creating a new viewmodel instead of updating the current one.
Replace this line
var timeslotViewModel = new TimeslotViewModel();
Either create a global viewmodel:
var myVm = new TimeslotViewModel();
ko.applyBindings(myVm);
//...
var timeslotViewModel = myVm;
Or get the current one from a node:
var timeslotViewModel = ko.contextFor($('.container').get(0)).$root
I'm following John Papa's jumpstart course about SPA's and trying to display a list of customers loaded via ASP.NET Web API the knockout foreach binding is not working. The Web API is working fine, I've tested it on it's own and it is returning the correct JSON, because of that I won't post the code for it. The get method simply returns one array of objects, each with properties Name and Email. Although not a good practice, knockout is exposed globaly as ko by loading it before durandal.
I've coded the customers.js view model as follows
define(['services/dataservice'], function(ds) {
var initialized = false;
var customers = ko.observableArray();
var refresh = function() {
return dataservice.getCustomers(customers);
};
var activate = function() {
if (initialized) return;
initialized = true;
return refresh();
};
var customersVM = {
customers: customers,
activate: activate,
refresh: refresh
};
return customersVM;
});
The dataservice module I've coded as follows (I've not wrote bellow the function queryFailed because I know it's not being used)
define(['../model'], function (model) {
var getCustomers = function (customersObservable) {
customersObservable([]);
var options = {url: '/api/customers', type: 'GET', dataType: 'json'};
return $.ajax(options).then(querySucceeded).fail(queryFailed);
function querySucceeded(data) {
var customers = [];
data.forEach(function (item) {
var c = new model.Customer(item);
customers.push(c);
});
customersObservable(customers);
}
};
return {
getCustomers: getCustomers
};
});
Finaly the model module was built as follows:
define(function () {
var Customer = function (dto) {
return mapToObservable(dto);
};
var model = {
Customer: Customer
};
return model;
function mapToObservable(dto) {
var mapped = {};
for (prop in dto)
{
if (dto.hasOwnProperty(prop))
{
mapped[prop] = ko.observable(dto[prop]);
}
}
return mapped;
}
});
The view is then simply a list, it is simply:
<ul data-bind="foreach: customers">
<li data-bind="text: Name"></li>
</ul>
But this doesn't work. Any other binding works, and I've looked on the console window, and it seems the observable array is being filled correctly. The only problem is that this piece of code doesn't show anything on screen. I've reviewed many times the files but I can't seem to find the problem. What's wrong with this?
You can use the knockout.js context debugger chrome extension to help you debug your issue
https://chrome.google.com/webstore/detail/knockoutjs-context-debugg/oddcpmchholgcjgjdnfjmildmlielhof
Well, I just spent a lot of time on an local issue to realize that the ko HTML comment format, if used, should be like this:
<!-- ko foreach: arrecadacoes -->
and NOT like this:
<!-- ko: foreach: arrecadacoes -->
: is NOT used after ko...
I know this question is a little old but I thought I'd add my response in case someone else runs into the same issue I did.
I was using Knockout JS version 2.1.0 and it seems the only way I can get the data to display in a foreach loop was to use:
$data.property
so in the case of your example it would be
$data.Name
Hope this helps
I don't see anywhere in your code that you've called ko.applyBindings on your ViewModel.
KO has a known issue while using foreach in a non-container element like the one above <ul> so you have to use containerless control flow syntax.
e.g.
<ul>
<!-- ko foreach: customers-->
<li data-bind="text: Name"></li>
<!-- /ko -->
</ul>
Ref: http://knockoutjs.com/documentation/foreach-binding.html
Not sure what's going wrong here, but KnockoutJS is having some issues finding my observable array that's inside my MasterViewModel. Using 2.2.1 with jQuery 1.8.x as well as not my first KJS app. Here it is:
Initialize
$(function() {
window.vm = new MasterViewModel();
ko.applyBindings(vm);
});
ViewModel
function MasterViewModel(data) {
var self = this;
self.currentAppView = ko.observable();
// Users
self.userList = ko.observableArray([]);
self.templateListGetter = ko.computed(function() {
$.getJSON("/user/list"), function(data) {
var mapped = $.map(data, function(item) { return new userModel(item) });
self.userList(mapped);
};
});
self.goToAppView = function(appView) {
location.hash = '!/' + appView;
};
Sammy(function() {
this.get('#!/:appView', function() {
self.currentAppView(this.params.appView);
$('.appview').hide();
ko.applyBindings(new window[this.params.appView+'VM']());
});
this.notFound = function(){
location.hash = "!/dashboard";
}
//this.raise_errors = true;
}).run();
}
The View
<table class="table table-bordered table-striped">
<tbody data-bind="foreach: userList">
<tr>
<td data-bind="text: guid"></td>
<td data-bind="text: firstName"></td>
<td data-bind="text: lastName"></td>
<td data-bind="text: email"></td>
<td data-bind="text: updated"></td>
<td data-bind="text: suspended"></td>
</tr>
</tbody>
</table>
I have a simple table that I am loading
Even after double-checking a couple things like adding defer="defer" to my JS tag and ensuring the userList exists, it simply cannot find the observableArray. It gives the error:
Message: ReferenceError: userList is not defined;
Bindings value: foreach: userList Error {}
Anyone have any idea what's going on?
Update
For those wondering what gets called every time the hash changes:
function usersVM() {
// Data
var self = this;
// Behaviours
$('#users').show();
}
It looks like you're initializing knockout with an undefined viewmodel?
ko.applyBindings(new window[this.params.appView+'VM']());, yet your actual viewmodel is window.vm. Case sensitivity ftw. Also, the viewmodel on window is already created / initialized. So you don't need the new operator.
So, change the applyBindings line to be
ko.applyBindings(window[this.params.appView+'vm']());
Updated Answer: By Poster
There was no necessity to keep running ko.applyBindings every time the route changed since it was already applying bindings on page load. So Sammy.js was changed to:
Sammy(function() {
this.get('#!/:appView', function() {
self.currentAppView(this.params.appView);
$('.appview').hide();
window[this.params.appView+'Route']();
});
this.notFound = function(){
location.hash = "!/dashboard";
}
//this.raise_errors = true;
}).run();
It does look like ko.computed or a regular function call to window.vm.getUserList() isn't running properly, but this will be saved for a different question.
function usersRoute() {
// Data
var self = this;
// Behaviours
$('#users').show();
window.vm.getUserList();
}