I am applying JSON data to my ViewModel using ko.mapping.fromJS. This JSON data has nested objects within it, like so:
var data = {
item : "#1234",
description: "This is item #1",
moreinfo: {
condition: "Good"
}
}
When I try to re-map new data to the object it works fine, updating all the values. However, if I remap the value of moreinfo to nullbecause this item doesn't have moreInfo, like so:
var data2 = {
item : "#4567",
description: "This is item #2",
moreinfo:null
}
it doesn't update the DOM but instead keeps the previous value condition:"Good". If I then update the value one more time to data that has moreinfo, like so:
var data3 = {
item : "#7890",
description: "This is item #3",
moreinfo: {
condition: "Bad"
}
}
it still updates the item and description, but still it doesn't update the DOM but instead keeps the value condition:"Good".
Am I using mapping incorrectly or can you just not allow the value to become null?
Javascript:
var viewModel;
$("#button1").on("click", function(){
viewModel = ko.mapping.fromJS(data);
ko.applyBindings(viewModel, $("itemWrapper")[0]);
});
$("#button2").on("click", function(){
ko.mapping.fromJS(data2, {}, viewModel);
});
$("#button3").on("click", function(){
ko.mapping.fromJS(data3, {}, viewModel);
});
HTML:
<div id="itemWrapper">
<p data-bind="text: item"></p>
<p data-bind="text: description"></p>
<p data-bind="text: moreinfo.condition"></p>
</div>
<button id="button1">
Bind Data #1
</button>
<button id="button2">
Bind Data #2
</button>
<button id="button3">
Bind Data #3
</button>
JSfiddle: https://jsfiddle.net/hrgx5f1y/
If you applyBindings with Button #1, then map the null with #2, then map new values with #3 you will see the issue I describe.
If you applyBindings with Button #1, then map new values with #3, then map new values with #4 (none of these are null) it works perfectly.
The way that Knockout binding handlers work is that they hook up to an observable, and they respond when that observable changes. It does not bind to your binding expression but to your observable object So your binding:
<p data-bind="text: moreinfo.condition"></p>
...takes the condition property of moreinfo, which is an observable object and subscribes to it. When you do this:
var data4 = {
item : "#0000",
description: "This is item #4",
moreinfo: {
condition: "Meh"
}
}
ko.mapping.fromJS(data4, {}, viewModel);
...it works because Knockout can tie the structure of data4 to your view model and update that exact same observable object with the new value 'Meh'.
If you do this instead:
var data2 = {
item : "#4567",
description: "This is item #2",
moreinfo:null
}
$("#button2").on("click", function(){
ko.mapping.fromJS(data2, {}, viewModel);
});
...it is not updating that observable, but rather is updating the moreinfo property so it's null. Since a binding is to an observable object, not an expression, even if you update the view model so moreinfo isn't null anymore, the binding expression is not re-evaluated; your DOM is still bound to that same original observable.
You can work around this by having moreinfo as an observable, binding to it, and subsequently binding to condition; that way, if either updates, your DOM will update as expected. For example:
<!-- ko with:moreinfo -->
<p data-bind="text: condition"></p>
<!-- /ko -->
Related
Here is the fiddle demonstrating the problem http://jsfiddle.net/LkqTU/31955/
I made a representation of my actual problem in the fiddle. I am loading an object via web api 2 and ajax and inserting it into my knockout model. however when I do this it appears the attributes are no longer observable. I'm not sure how to make them observable. in the example you will see that the text box and span load with the original value however updating the textbox does not update the value.
here is the javascript.
function model() {
var self = this;
this.emp = ko.observable('');
this.loademp = function() {
self.emp({
name: 'Bryan'
});
}
}
var mymodel = new model();
$(document).ready(function() {
ko.applyBindings(mymodel);
});
here is the html
<button data-bind="click: loademp">
load emp
</button>
<div data-bind="with: emp">
<input data-bind="value: name" />
<span data-bind="text: name"></span>
</div>
You need to make name property observable:
this.loademp = function(){
self.emp({name: ko.observable('Bryan')});
}
I am new at angularjs and getting data from api like this:
function response(data){
$scope.data = data
}
<<<< data format is ilke this >>>>>
[
{"id":"1", "name":"item1"},
{"id":"2", "name":"item2"},
{"id":"3", "name":"item3"}
];
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>
Using this in view page with ng-repeat:
<button ng-repeat="item in data">{{item.name}}</button>
I will change selected item button color. But I need selected property on items. But it does not comes from database.
How can I add property named selected to items? in view or in controller?
I think you can use ng-init in repeat as,
<button ng-repeat="item in data" ng-init="item.selected = false">{{item.name}}</button>
this will add a selected property for each and every repeating object with the value of false.
Did you try a simple loop ?
$.each($scope.data, function( index, item ) {
item.selected = false;
});
You can initialize the selected attribute for all data items:
for (var i in data){
data[i].selected = false;
}
and then change it on click:
<button ng-repeat="item in data" ng-click="item.selected=true">{{item.name}}</button>
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}}
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>
I have a model that has an observable array, I can display the data in a text box, but I can't figure out how to bind it back to the original array.
Here is the working sample I have.
<ul data-bind='foreach: frameworks'>
<li>
<button class='btn' value='pick me'
data-bind='text: name, click: $parent.selectFramework'>
</button>
</li>
</ul>
<input type='text' data-bind='value: selectedFramework().name' />
<pre data-bind='text: ko.toJSON($root.selectedFramework, null, 4)'>
</pre>
var Framework = {
name: ''
};
var App = new function () {
var self = this;
self.frameworks = ko.observableArray();
self.selectFramework = function (item) {
self.selectedFramework(item);
};
self.selectedFramework = ko.observable(Framework);
};
App.frameworks([{name: 'foo'}, {name: 'bar'}]);
ko.applyBindings(App);
You are almost there. You need to make the 'name' properties on each framework observable. I have updated your JsFiddle here
App.frameworks([{
name: ko.observable('foo')
}, {
name: ko.observable('bar')
}]);
The value is only stored in your selectedFramework observable, so you would be able to access it via App.selectedFramework(). The observable doesn't take any variable and make it observable, it will store whatever value you pass it internally. If you want to update the external Framework variable, you would do that in your selectFramework function.
self.selectFramework = function (item) {
self.selectedFramework(item);
Framework = item;
};