Knockout JS update view from json model - javascript

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>

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

Looping over an ArrayController fails as it is not an Array even though the Controller has the correct model with data

I am attempting to use an ArrayController to handle displaying some data that will be swapped out on user clicks. I currently get this error, Uncaught Error: Assertion Failed: The value that #each loops over must be an Array. You passed App.CurrentListController but If I look at Ember Inspector I can see the CurrentListController and it has the model and the data in it. Basically the Stat page lets you see a bunch of stats and clicking on a specific stat pops up a modal and shows all the record that relate to that stat. If I just store the records on the StatController it works fine but then I cant sort/filter using the ArrayController. So it all works except for when I try and display the contents of CurrentListController it freaks out.
Thanks for any help or direction.
CurrentListController:
App.CurrentListController = Ember.ArrayController.extend({
sortProperties: ['name'], //Initial sort column.
sortAscending: true,
});
StatController:
App.StatController = Ember.ObjectController.extend({
needs:['currentList'],
currentList:[],
actions: {
viewBusiness: function(ids) {
console.log(ids)
var self = this
console.log(this.get('controllers.currentList').get('sortProperties'))
this.store.findByIds('business', ids.split(",")).then(
function(results)
{
$('#editModal').modal('show');
//self.set('currentList', results.sortBy(["name"]))
self.get('controllers.currentList').set('model', results)
console.log(self.get('controllers.currentList').get('arrangedContent'))
});
},
sortBy: function(prop){
var clController = this.get('controllers.currentList')
clController.set('sortProperties', prop)
clController.set('sortAscending', !clController.get('sortAscending'));
}
}
});
Stat Template:
{{#each business in App.CurrentListController}}
<tr {{bind-attr id=business.id}}>
<td>{{business.name}}</td>
<td>{{business.city}}</td>
<td>{{business.state}}</td>
<td>{{business.zip}}</td>
<td class="text-center">{{business.numVendors}}{{/link-to}}</td>
<td class="text-center">{{business.numClients}}{{/link-to}}</td>
</tr>
{{/each}}
App.CurrentListController is not an array. It's an object, a controller object. (btw it is not recommended to access the global namepsace [ ie. using anything with an uppercase letter ] in your template)
What you should do instead is:
App.StatController = Ember.ObjectController.extend({
needs:['currentList'],
currentList: Ember.computed.alias('controllers.currentList.model'),
...
This way you can access the underlying model of your currentList controller (which is an array) and make it available to your template as currentList.
{{#each business in currentList}}
...
{{/each}}

Adding more, another loop to my Underscore template with a new backbone model?

UPDATE :
I have got my code working, of sorts, but I have two issues and one problem I am not sure how to fix. I will post my current code below. One The Clients append to the right section within the TimesheetData template. But it wraps the option tag within the ClientData template within another option .
So I get :
<select>
<option> <option value="XX"> XXX </option> </option>
</select>
Now I know this is what it is designed to do, to have a root element but I can not seem to find a solution to this issue, although it still works.
Now the other issue is that I need to select a default client, which is loaded into the Timesheet model, Timesheetrow.client_id holds what the database as saved for that row. I just not sure how to access this in an if statement or some other way within the client template.
Now the problem I have is that the Client data does not always load? So when I reload / refresh the page it sometimes lists all my clients in option tags, sometimes it loads nothing, just giving me an empty select tag. However when it does not load anything, I don't have any console log errors or anything?
All help most welcome :)
So this is my current Backbone code:
Client Data Code :
var ClientModel = Backbone.Model.extend({
defaults: {
Client: "",
}
}); //End of ClientModel
var ClientCollection = Backbone.Collection.extend({
model: ClientModel,
url: '/dashboard/json/clients'
}); //End of ClientCollection
var ClientView = Backbone.View.extend({
tagName: 'option',
template: _.template($('#ClientData').html()),
render: function() {
this.$el.append(this.template(this.model.toJSON()));
return this.$el;
}
});
Timesheet Data Code :
var TimeSheetModel = Backbone.Model.extend({
defaults: {
Timesheetrow: "",
}
}); //End of TimeSheetModel
var TimeSheetCollection = Backbone.Collection.extend({
model: TimeSheetModel,
url: '/dashboard/json/row/' + TimesheetID()
}); //End of TimeSheetCollection
var TimeSheetRowView = Backbone.View.extend({
className: 'TimesheetRowLine',
template: _.template($('#TimesheetData').html()),
render: function() {
this.$el.append(this.template(this.model.toJSON()));
return this.$el;
}
}); //End of TimeSheetRowView
Timesheet & Client Code Section:
var TimeSheetCollectionView = Backbone.View.extend({
el:'#MasterContainer',
template: _.template($('#TimesheetForm').html()),
events: {
"click .actionSubmit": "handleSubmit"
},
initialize: function() {
//Get Client Data & Add To Template
this.clientcollection = new ClientCollection();
this.listenTo(this.clientcollection, "add", this.AddClient);
this.clientcollection.fetch();
//Get Main Timesheet Data & Add To Template
this.collection = new TimeSheetCollection();
this.listenTo(this.collection, "add", this.AddTimesheetRow);
this.collection.fetch();
this.$el.append(this.template());
this.submitButton = this.$(".actionSubmit");
},
AddTimesheetRow: function(model) {
var view = new TimeSheetRowView({model: model});
view.render().insertBefore(this.submitButton);
},
AddClient: function(model) {
var clients = new ClientView({model: model});
$("#TimesheetDataList .TimesheetRowLine #clienttemp").append( clients.render() );
},
handleSubmit: function(){
//in real life, you would validate and save some model
alert("form submit");
return false;
}
}); //End of TimeSheetCollectionView
var collectionView = new TimeSheetCollectionView();
This is my Underscore Template code:
<script type="text/template" id="TimesheetForm">
<form action="#" method="post" id="TimesheetDataList" style="width: auto; padding-left: 50px;">
<input type="submit" class="actionSubmit" value="Send"/>
</form>
</script>
<script type="text/template" id="TimesheetData">
<%= console.log( Timesheetrow.client_id ) %>
<input type="hidden" name="data[Timesheetrow][<%= Timesheetrow.id %>][id]" value="<%= Timesheetrow.id %>">
<input type="type" name="data[Timesheetrow][<%= Timesheetrow.id %>][jobtitle]" value="<%= Timesheetrow.twistjob %>">
<select name="data[Timesheetrow][<%= Timesheetrow.id %>][client_id]" id="clienttemp"></select>
</script>
<script type="text/template" id="ClientData">
<option value="<%= Client.id %>"><%= Client.clientname %></option>
</script>
OLD POST
Ok, I am having an issue with my Backbone view rendering into my Underscore template, again. I thought it would be best to ask it as a new question.
My last question, Underscore Template Looping, Without A Loop?, the guy on there help me very well but I am now trying, with little success to edit this code and extend it a little more.
CODE REMOVE - SEE UPDATE
So I was trying to follow the same methods. I know I have to look up more training to expand my knowledge.
With this code, I have a list of clients, I need to load into a select / option pull down form element. But it only seems to loop around 17 times, which is the number of rows I have in my timesheet. I am console logging 'Client' in the 'ClientData' template, this is what displays my client data but only the client data that is logged in my timesheet rows, not all the clients from the JSON data that the model / collection is pointing to? I am also getting a ref. Error for Client? Even though it is in my model as a default?
I am understanding backbone (a bit), but not Underscore so much.
All help most welcome.
Thanks,
:: EDIT ::
I thought I would post my Underscore templates.
CODE REMOVED - SEE UPDATE
So what I am trying to do is loop around in ClientData template for all the clients, with 'option' tags, then add this to the select elements on the TimesheetData template. Then it is complete by this template being added to the TimesheetForm template, which it already does.
This might need a different method and might have been my fault that it don't work as I forgot to explain on the other question a out my client list.
Thanks,

Getting data from the database and render it on to the table in backbone.js

I am new to the world of backbone.js . I want to communicate with the server using backbone.js and render the employee details on to the table .I am getting the data from the server using following code:
var EmployeeCollection = Backbone.Collection.extend({
model: Person,
url:"http://localhost:4000/get/employee",
parse : function(res)
{
console.log('response inside parse' + res);
return res;
}
});
var employee = new EmployeeCollection();
employee.fetch();
In log statement i am getting :response inside parse[object Object],[object Object],[object Object]
But i don't know what next. How to retrieve the data from the object i am getting and render it on to the table . Do anyone have suggestions ?
Let us assume you have a table in your HTML page with id="employee" and you have defined a template corresponding to a row in the table. For simplicity, we asssume employee row just has firstname and lastname:
<table id="employee">
<thead>
<tr><td>Firstname</td><td>Lastname</td></tr>
</thead>
<tbody>
</tbody>
</table>
<script type="text/template" id="employee-template">
<td><%= firstname %></td><td><%= lastname %></td>
</script>​
You need two views one to render the table, and one to render each row in the table. They may look like:
//a (table) view to render the list of employees
var employee_list_view = Backbone.View.extend({
el: $('#employee'),
initialize: function() {
this.collection.bind("add", this.render, this);
},
//this creates new rows in the table for each model in the collection
render: function() {
_.each(this.collection.models, function(data) {
this.$el.append(new employee_view({
model: data
}).render().el);
}, this);
return this;
}
});
//a (row) view to render each employee
var employee_view = Backbone.View.extend({
tagName: "tr",
template: _.template($("#employee-template").html()),
render: function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
}
});
After you fetch the collection from the server, the items are stored within the collection. You can view the retrieved data using the following code. On success we create a new employee list (table in this case) and pass the employee collection.
var employee = new EmployeeCollection();
employee.fetch({
success: function() {
console.log(employee.toJSON());
new employee_list_view({collection: employee}).render();
},
error: function() {
console.log('Failed to fetch!');
}
});
Note: its recommended to use the success/fail callbacks.
Take a look at this working version on JSFiddle
First of all, you can get much more information if you use console.log in this way console.log('response inside parse', res);. res won't be converted to string, but it will be displayed as a JavaScript object with all its properties and values. Then, check the backbone.js docs Collection.parse and read what the res is in this context and what this method should return.
Probably next step would be to create a View which renders a table using some template and data from your collection.

Refreshing list after ajax call with Knockout JS

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.

Categories

Resources