Hi I have been getting investing alot of time in learning Knockout and have come to a point where I have to many properties in my application and I am in need to use the mapping pluggin.
It seems easy enought how it should be used but I mussed be missing something because it does not work.I have created a test example.This is my code:
function vm() {
var self = this;
this.viewModel = {};
this.getData = function() {
$.getJSON('/api/Values/Get').then(data)
.fail(error);
function data(ajaxData) {
console.log(ajaxData);
self.viewModel = ko.mapping.fromJS(ajaxData);
console.log(self.viewModel);
}
function error(jError) {
console.log(jError);
}
};
};
ko.applyBindings(new vm());
This is my html:
<ul data-bind="foreach: viewModel">
<li data-bind="text:FirstName"></li>
<input type="text" data-bind="value: FirstName"/>
</ul>
<button data-bind="click : getData">Press me!</button>
My ajax call succesfully retrieves this data from the server:
[
{
FirstName: "Madalina",
LastName: "Ciobotaru",
hobies: [
"games",
"programming",
"hoby"
]
},
{
FirstName: "Alexandru",
LastName: "Nistor",
hobies: [
"games",
"programming",
"movies"
]
}
]
It seems that after data function is called viewModel get's converted into an array but with no items in it.
What am I doing wrong?
I have taken your expected server data and created a jsfiddle here. You needed to change the viewModel property to be an observable array, and change the way the mapping is performed.
Here is a version of your script that will work:
function vm() {
var self = this;
this.viewModel = ko.observableArray([]);
this.getData = function() {
$.getJSON('/api/Values/Get').then(data)
.fail(error);
function data(ajaxData) {
console.log(ajaxData);
ko.mapping.fromJS(ajaxData, {}, self.viewModel);
console.log(self.viewModel);
}
function error(jError) {
console.log(jError);
}
};
};
ko.applyBindings(new vm());
Related
I have an issue with Knockout.js . What I try to do is filter a select field. I have the following html:
<select data-bind="options: GenreModel, optionsText: 'name', value: $root.selectedGenre"></select>
<ul data-bind="foreach: Model">
<span data-bind="text: $root.selectedGenre.id"></span>
<li data-bind="text: name, visible: genre == $root.selectedGenre.id"></li>
</ul>
And the js:
var ViewModel = function (){
self.selectedGenre = ko.observable();
self.Model = ko.observableArray([{
name: "Test",
genre: "Pop"
}
]);
self.GenreModel = ko.observableArray([
{
name: "Pop",
id: "Pop"
},
{
name: "Alle",
id: "All"
}
]);
};
var viewModel = new ViewModel();
ko.applyBindings(viewModel);
JSFiddle: http://jsfiddle.net/CeJA7/1/
So my problem is now that the select list does not update the binding on the span inside the ul and I don't know why...
The value binding should update the property selectedGenre whenever the select value changes, shouldn't it?
Any ideas are welcome.
There are a lot of issues in your code:
1) self is not a magical variable like this. It's something people use to cope with variable scoping. Whenever you see self somewhere in a JavaScript function be sure there's a var self = this; somewhere before.
2) KnockoutJS observables are not plain variables. They are functions (selectedGenre = ko.observable()). ko.observable() returns a function. If you read the very first lines of documentation regarding observables you should understand that access to the actual value is encapsulated in this retured function. This is by design and due to limitations in what JavaScript can and cannot do as a language.
3) By definition, in HTML, <ul> elements can only contain <li> elements, not <span> or anything else.
Applying the above fixes leads to this working updated sample:
HTML:
<select data-bind="options: GenreModel, optionsText: 'name', value: selectedGenre"></select>
<span data-bind="text: $root.selectedGenre().id"></span>
<ul data-bind="foreach: Model">
<li data-bind="text: name, visible: genre == $root.selectedGenre().name"></li>
</ul>
JavaScript:
var ViewModel = function (){
var self = this;
self.selectedGenre = ko.observable();
self.Model = ko.observableArray([
{
name: "Test",
genre: "Pop"
}
]);
self.GenreModel = ko.observableArray([
{
name: "Pop",
id: "Pop"
},
{
name: "Alle",
id: "All"
}
]);
};
var viewModel = new ViewModel();
ko.applyBindings(viewModel);
Got a bit of a conundrum with a knockout observable array being shared across multiple view models.
Basically, I have a layout as follows
Transport
... textbox fields, etc
Selected Passengers:
<!-- ko foreach: allPassengers -->
<input type="checkbox" />
<!-- /ko -->
<button>Add Transport</button>
Holiday
... textbox fields, etc
Selected Passengers:
<!-- ko foreach: allPassengers -->
<input type="checkbox" />
<!-- /ko -->
<button>Add Holiday</button>
Now the selected passengers for each section is being generated from ONE observable array, idea being if a passenger is deleted/altered everything should fall into place automagically.
So something like this
function page() {
// in actuality this passengers array is a computed observable obtained from the passengers section which is not shown here.
this.allPassengers = ko.observableArray([
{
Id: 1,
name = ko.observable('name'),
checked = ko.observable(false)
},
{
.
.
]);
}
function transport() {
// pageVM is a page object
this.allPassengers = pageVM.allPassengers;
this.transportItems = ko.observableArray();
this.addTransport = function() {
this.transportItems.push({
.
.
selectedPassengers: [...]
.
.
});
};
}
function holiday() {
// pageVM is a page object
this.allPassengers = pageVM.allPassengers;
this.holidayItems = ko.observableArray();
this.addHoliday = function() {
this.holidayItems.push({
.
.
selectedPassengers: [...]
.
.
});
};
}
However, when add transport/holiday is clicked, I need a way to determine which checkboxs where checked so I can add the selected passengers.
I have tried to add a checked = ko.observable(false) property to the passenger item in parent.allPassengers, but the problem with this approach is if a checkbox is checked in the transport section it will also check it in the holiday section since it is using the same observable array.
Any ideas??
Edit:
example fiddle
The checked binding works with observable arrays too. So you can simply bind to $parent.selectedPassengers and specify the value attribute to be the passenger id, like this:
<input type="checkbox" data-bind="attr: { value: id },
checked: $parent.selectedPassengers" />
In each view model you need to have a selectedPassengers observable array used for binding to the checkbox. And the add function should look like this:
function transport(pageVM) {
....
this.selectedPassengers = ko.observableArray([]);
....
this.addTransport = function() {
this.selectedItems.push({
....
selectedPassengers: this.selectedPassengers()
});
};
};
Working Fiddle
You can use a ko.computed to return the selected passengers (and here's a fiddle):
var ViewModel = function () {
this.allPassengers = ko.observableArray([
{ name: 'John', selected: ko.observable(false) },
{ name: 'Jane', selected: ko.observable(false) },
{ name: 'Mark', selected: ko.observable(false) }
]);
this.selectedPassengers = ko.computed(function () {
return ko.utils.arrayFilter(this.allPassengers(), function (item) {
return item.selected();
});
}, this);
};
I'm using backbone.js. I get a json like this:
{
first_name: 'David',
last_name: 'Smith',
family: [{father: 'David', mother: 'Rose', brother: 'Max'}]
}
first_name and last_name shows in through a PersonView (extending Backbone.View) and family data I want to show in a DetailsView.
So, I was trying like this. First:
personView = new PersonView(model: person)//person it's the json above
PersonView shows well. Then I want to pass the model to DetailsView like this:
detailsView = new DetailsView(model: JSON.parse(person.get('family'));
Well, when I try to pass the model to a template in DetailsView implementation, like this:
DetailsView = Backbone.View.extend({
className: 'tab-pane',
template: _.template($('#detail-tpl').html()),
render: function(){
this.$el.html(this.template(this.model.toJSON()));
return this;
},
});
I get this message:
Uncaught TypeError: Object [object Object] has no method 'toJSON'
I don't know how to get or pass the model to solved this.
I'm trying several ways but I can't make it go.
Hope you can help me.
I think the problem is is because of this line.
model: JSON.parse(person.get('family')
It expects model to be an instance of backbone Model . But I don't think that is the case here.. try defining the Model for family or otherwise change the name of the key
Instead try this approach
familyMembers : JSON.parse(person.get('family')
In your view you can access this as
(this.options.familyMembers.toJSON())
The issue is that you model you are passing in is just an array. Therefore doesn't have the .toJSON method. As grant suggested you could use new Backbone.Model when creating the view but I would recommend using a collection and 2 new views for the family. It would look something like this.
var PersonModel = Backbone.Model.extend({
initialize: function(attributes, options) {
if(attributes.family) {
this.family = new FamilyCollection(attributes.family, options);
}
}
});
var FamilyCollection = Backbone.Collection.extend({
model: FamilyMember,
initialize: function(models, options) {
this.view = new FamilyView(options);
}
});
var FamilyMember = Backbone.Model.extend({
initialize: function(attributes, options) {
this.view = new DetailedView({
model: this
});
}
});
Then you would use a view structure something like this..
<div class="person">
<span class="name-first">David</span> <span class="name-last">Smith</span>
<div class="family-members>
<div class="family-member">
<span class="name-first">Rose</span> <span class="name-last">Smith</span>
</div>
<div class="family-member">
<span class="name-first">David</span> <span class="name-last">Smith</span>
</div>
<div class="family-member">
<span class="name-first">Max</span> <span class="name-last">Smith</span>
</div>
</div>
</div>
The "family" property is an array, you could do one of the following...
var familyArray = model.get('family');
new DetailsView({model: new Backbone.Model(familyArray[0])});
...or add a getFamily function to the person model...
var PersonModel = Backbone.Model.extend({
getFamily: function() {
var familyArray = this.get('family');
return new Backbone.Model(familyArray[0]);
}
});
...
new DetailsView({model: person.getFamily()});
The Knockout mapping plugin documentation has a section entitled "Uniquely identifying objects using “keys”". This describes how to update part of an object and then only update that part of the display rather than completely replacing the display of all properties of a partially-modified object. That all works splendidly in their simple example, which I have slightly modified here to make my question more clear. My modifications were to:
Replace the object with a corrected name after a 2 second delay.
Highlight the unchanging part of the display, so you can see that it is actually not replaced when the update happens.
1. Simple object (jsFiddle)
<h1 data-bind="text: name"></h1>
<ul data-bind="foreach: children">
<li><span class="id" data-bind="text: id"></span> <span data-bind="text: name"></span></li>
</ul>
<script>
var data = {
name: 'Scot',
children: [
{id : 1, name : 'Alicw'}
]
};
var mapping = {
children: {
key: function(data) {
console.log(data);
return ko.utils.unwrapObservable(data.id);
}
}
};
var viewModel = ko.mapping.fromJS(data, mapping);
ko.applyBindings(viewModel);
var range = document.createRange();
range.selectNode(document.getElementsByClassName("id")[0]);
window.getSelection().addRange(range);
setTimeout(function () {
var data = {
name: 'Scott',
children: [
{id : 1, name : 'Alice'}
]
};
ko.mapping.fromJS(data, viewModel);
}, 2000);
</script>
But what isn't clear to me is how I would achieve the same behavior for a more complex nested data structure. In the following example, I took the above code and wrapped the data in a list. I would like this to behave the same as above, but it doesn't. The whole display is redone because of the change in one property. You can see this because, unlike the above example, the highlighting is lost after the data is updated.
2. More complex nested object (jsFiddle)
<!-- ko foreach: parents -->
<h1 data-bind="text: name"></h1>
<ul data-bind="foreach: children">
<li><span class="id" data-bind="text: id"></span> <span data-bind="text: name"></span></li>
</ul>
<!-- /ko -->
<script>
var data = {
parents: [
{
name: 'Scot',
children: [
{id : 1, name : 'Alicw'}
]
}
]
};
var mapping = {
children: {
key: function(data) {
console.log(data);
return ko.utils.unwrapObservable(data.id);
}
}
};
var viewModel = ko.mapping.fromJS(data, mapping);
ko.applyBindings(viewModel);
var range = document.createRange();
range.selectNode(document.getElementsByClassName("id")[0]);
window.getSelection().addRange(range);
setTimeout(function () {
var data = {
parents: [
{
name: 'Scott',
children: [
{id : 1, name : 'Alice'}
]
}
]
};
ko.mapping.fromJS(data, viewModel);
}, 2000);
</script>
So basically what I'm asking is, how can I make the second example work like the first, given the more nested data structure? You can assume that ids are unique for each child (so if I added another parent besides Scott, his children would start with id=2, etc.).
Interesting observation there and nice write-up. It appears to work if you define a key on the parent as well as the child. Try this fiddle:
http://jsfiddle.net/8QJe7/6/
It defines instantiable view model functions for the parents and children, where the parent constructor does its child mappings.
I have a simple Handlebars helper which simply formats a money value. The helper works property when I test with static data, but not when I load data asynchronously. In other words, {{totalBillable}} will output the expected amount, but {{money totalBillable}} will output zero. But only when the data is loaded via an ajax call. What the heck am I doing wrong?
I've tried to pare the code down as much as possible, and also created a jsfiddle here:
http://jsfiddle.net/Gjunkie/wsZXN/2/
This is an Ember application:
App = Ember.Application.create({});
Here's the handlebars helper:
Handlebars.registerHelper("money", function(path) {
var value = Ember.getPath(this, path);
return parseFloat(value).toFixed(2);
});
Model:
App.ContractModel = Ember.Object.extend({});
App Controller:
App.appController = Ember.Object.create({
proprietor: null,
});
Contracts Controller (manages an array of contracts):
App.contractsController = Ember.ArrayController.create({
content: [],
totalBillable: function() {
var arr = this.get("content");
return arr.reduce(function(v, el){
return v + el.get("hourlyRate");
}, 0);
}.property("content"),
When the proprietor changes, get new contract data with an ajax request. When getting data asynchronously, the handlebars helper does not work.
proprietorChanged: function() {
var prop = App.appController.get("proprietor");
if (prop) {
$.ajax({
type: "POST",
url: '/echo/json/',
data: {
json: "[{\"hourlyRate\":45.0000}]",
delay: 1
},
success: function(data) {
data = data.map(function(item) {
return App.ContractModel.create(item);
});
App.contractsController.set("content", data);
}
});
}
else {
this.set("content", []);
}
}.observes("App.appController.proprietor")
});
If I use this version instead, then the Handlebars helper works as expected:
proprietorChanged: function() {
var prop = App.appController.get("proprietor");
if (prop) {
var data = [{
"hourlyRate": 45.0000}];
data = data.map(function(item) {
return App.ContractModel.create(item);
});
App.contractsController.set("content", data);
}
else {
this.set("content", []);
}
}.observes("App.appController.proprietor")
View:
App.OverviewTabView = Ember.TabPaneView.extend({
totalBillableBinding: "App.contractsController.totalBillable"
});
Kick things off by setting a proprietor
App.appController.set("proprietor", {
ID: 1,
name: "Acme"
});
Template:
<script type="text/x-handlebars">
{{#view App.OverviewView viewName="overview"}}
<div class="summary">
Total Billable: {{totalBillable}}<br/>
Total Billable: {{money totalBillable}}<br/>
</div>
{{/view}}
</script>
when using a helper, handlebars does not emit metamorph tags around your helper call. this way, this part of the template is not re-rendered because there is no binding
to manually bind part of a template to be re-rendered, you can use the bind helper:
<script type="text/x-handlebars">
{{#view App.OverviewView viewName="overview"}}
<div class="summary">
Total Billable: {{totalBillable}}<br/>
Total Billable: {{#bind totalBillable}}{{money this}}{{/bind}}<br/>
</div>
{{/view}}
</script>