KnockoutJS Observable being rebound appropriately, but calling gives an empty string? - javascript

So I've got a section that updates based on a ko.observable called toClicked, see below:
<div id="longInfoWindow">
<div id="longInfo_goBack"><span class="fa fa-arrow-left"></span> Go Back</div>
<div id="location_mainInfo">
<h1 id="location_title" data-bind="text: toClicked.title">Title</h1>
<h2 id="location_address" data-bind="text: toClicked.address">Address</h2>
<h6 class="location_latlng">LAT: <span data-bind="text: toClicked.lat">LATITUDE</span></h6>
<h6 class="location_latlng">LNG: <span data-bind="text: toClicked.lng">LONGITUDE</span></h6>
<p id="location_description" data-bind="text: toClicked.content">
Empty
</p>
</div>
</div>
toClicked is data-bound via a for-each ul that passes in bits of information from an observableArray, so this data is constantly changing - here's what the click function looks like in the viewmodel.
var viewModel = {
// Nav open and close via knockoutjs
self : this,
userQuery : ko.observable(''),
toClicked : ko.observable({}),
logClick : function(clicked){
var toClicked = ko.observable({});
toClicked().title = clicked.title;
toClicked().lat = clicked.lat;
toClicked().lng = clicked.lng;
toClicked().placeID = clicked.placeID;
toClicked().address = clicked.address;
toClicked().content = clicked.content;
return toClicked();
}
};
// at the end of the document...
ko.applyBindings(viewModel);
For some reason, I can call any toClicked parameter, like toClicked.title, and I get the proper output. But I can't get anything to bind in the longInfoWindow bit of code, it removes the filler text with empty strings. Is there something I'm missing here with Knockout that's keeping it from updating appropriately?
As a side note, I've tried setting the databinds to viewModel.toClicked.title with no joy. Have also tried $root, $parent. either comes back as not defined or gives the same result.

You need to change the way toClicked is accessed. Given that it is an observable, it must access the properties using syntax like toClicked().property. Another problem is that you should specify toClicked as an object, before setting properties on it.
Also, since clicked is an array, it should be accessed by index, like clicked[0].title.
Note the use of self.toClicked.valueHasMutated(); in function logClick. It tells the view model that observable variable toClicked has some non-observable properties that might have changed. As a result the view model is updated. You should avoid using it excessively.
var viewModel = function() {
// Nav open and close via knockoutjs
var self = this;
self.test = ko.observable('text');
self.userQuery = ko.observable('');
self.toClicked = ko.observable({});
self.markers = ko.observableArray([
{ title: 'Eagle River Airport', lat: 45.934099, lng: -89.261834, placeID: 'ChIJdSZITVA2VE0RDqqRxn-Kjgw', content: 'This is the Eagle River Airport. Visit us for fly-ins!' }
]);
self.logClick = function(clicked) {
// var toClicked = ko.observable({});
self.toClicked().title = clicked[0].title;
self.toClicked().lat = clicked[0].lat;
self.toClicked().lng = clicked[0].lng;
self.toClicked().placeID = clicked[0].placeID;
self.toClicked().address = clicked[0].address;
self.toClicked().content = clicked[0].content;
self.toClicked.valueHasMutated();
return self.toClicked();
};
};
// at the end of the document...
var vm = new viewModel();
ko.applyBindings(vm);
var markers = vm.markers();
vm.logClick(markers);
Your view model must also change slightly. Note the () brackets after toClicked, they are used to access the properties of an observable.
<div id="longInfoWindow">
<div id="longInfo_goBack"><span class="fa fa-arrow-left"></span> Go Back</div>
<div id="location_mainInfo">
<h1 id="location_title" data-bind="text: toClicked().title">Title</h1>
<h2 id="location_address" data-bind="text: toClicked().address">Address</h2>
<h6 class="location_latlng">LAT: <span data-bind="text: toClicked().lat">LATITUDE</span></h6>
<h6 class="location_latlng">LNG: <span data-bind="text: toClicked().lng">LONGITUDE</span></h6>
<p id="location_description" data-bind="text: toClicked().content">
Empty
</p>
</div>
I'd also suggest that instead of accessing toClicked directly within logClick you use something like self.toClicked to avoid ambiguity. See this.
Here's the working fiddle: https://jsfiddle.net/Nisarg0/hx0q6tt6/13/

The more obvious way without having to use valueHasMutated would be to assign directly to the observable:
self.logClick = function(clicked) {
self.toClicked({
lat: clicked[0].lat,
lng: clicked[0].lng,
placeID: clicked[0].placeID,
adress: clicked[0].address,
content: clicked[0].content
});
};
You normally do not need to use valueHasMutated when using knockout. Also there is no need to return the observable from the click handler. In your bindings you need then to access the properties as already stated like this:
<h1 id="location_title" data-bind="text: toClicked().title">Title</h1>
Knockout will automatically update the heading, whenever toClicked changed.

Related

Knockout js binding on properties of nullable objects

I have a knockout.js page which loads data from an API and uses the knockout mapping plugin to turn the data into a parameter on the ViewModel.
The data contains nested objects e.g.
[{
id: 1,
targetField: {
id: 132,
name: 'Field ABC',
...
},
conditionalOperator: {
id: 8,
display: 'Less Than'
},
conditionalValue:13
},
...
]
Loaded into the page view model
var PageViewModel = function() {
...
this.allConditionLogic = ko.observableArray();
}
var pageViewModel = new PageViewModel();
$.get('api/...')
.done(function(data) {
pageViewModel.allConditionLogic(ko.mapping.fromJS(data));
});
The html contains bindings to the objects
<div data-bind="foreach: allConditionLogic">
<p>Field id <span data-bind="text: targetField().id"></span> <span data-bind="text: conditionalOperator().display"></span> <span data-bind="text: conditionalValue"></span></p>
</div>
This however errors as before the ajax call has returned, targetField and conditionalOperator are null.
It is possible to use extra span elements and the with binding which doesn't create the internal html if the bound object doesn't exist - e.g.
<p>Field id <span data-bind="with: targetField"><span data-bind="text: id"></span> <span data-bind="with: conditionalOperator"><span data-bind="text: display"></span></span> <span data-bind="text: conditionalValue"></span></p>
however this seams somewhat overkill. I could define a blank object in allConditionLogic with the correct fields, but that requires a lot more typing and needs updating if the API changes.
Is there a better way of getting this to work?
A simple solution could be to create a knockout observable variable and set it to false until you have the data returned by the API. Then wrap your div with that observable inside a ko if: binding -
var PageViewModel = function() {
this.allConditionLogic = ko.observableArray();
//Set it to false initially
this.hasAPIreturnedData = ko.observableArray(false);
}
var pageViewModel = new PageViewModel();
$.get('api/...')
.done(function(data) {
pageViewModel.allConditionLogic(ko.mapping.fromJS(data));
//make it true after data is returned and is transformed
pageViewModel.hasAPIreturnedData(true);
});
<!--ko if: hasAPIreturnedData -->
<div data-bind="foreach: allConditionLogic">
<p>Field id <span data-bind="text: targetField().id"></span> <span data-bind="text: conditionalOperator().display"></span> <span data-bind="text: conditionalValue"></span></p>
</div>
<!--/ko-->
There could be more elegant ways of handling this but that depends on a lot of things. As I said, this is the simplest solution I could think of :)

knockout js cannot delete after adding to observable array

I am trying to complete some basic add and delete functionality using knockout js.
I am using asp mvc, the knockout mappings plugin and returning a simple list of strings as part of my viewModel
Currently I get three items from the server and the functionality I've created allows me to delete each of those items. I can also add an item. But if I try to delete an item i have added in the KO script I cannot delete it.
After completing research and some tests I guess i'm using the observable's incorrectly. I altered my code to pass ko.observable(""), but that has not worked. What am I doing wrong?
values on load
Array[4]0: "test 1"1: "test 2"2: "test 3"length: 4__proto__: Array[0]
values after clicking add
Array[4]0: "test 1"1: "test 2"2: "test 3"3: c()length: 4__proto__: Array[0]
ko script
var vm = function (data) {
var self = this;
ko.mapping.fromJS(data, {}, self);
this.deleteBulletPoint = function (bulletPoint) {
self.BulletPoints.remove(bulletPoint)
}
this.addEmptyBulletPoint = function () {
self.BulletPoints.push(ko.observable(""));
console.log(self.BulletPoints())
}
}
HTML
<div class="col-lg-6 col-md-6 col-sm-12">
<h4>Bullet Points</h4>
<div id="oneLinedescriptions" class="input_fields_wrap">
<!-- ko foreach: BulletPoints -->
<div class="form-group">
<div class="input-group">
<input type="text" data-bind="value: $data" class="form-control">
<span data-bind="click: $parent.deleteBulletPoint" class="input-group-addon btn">-</span>
</div>
</div>
<!-- /ko -->
</div>
<button id="btnAddDescription" data-bind="click: addEmptyBulletPoint" type="button" class="btn btn-default add_field_button col-lg-12 animate-off">Add New Bullet Point</button>
</div>
EDIT
I have removed $parent but the below error was returned
Uncaught ReferenceError: Unable to process binding "foreach: function (){return BulletPoints }"
Message: Unable to process binding "click: function (){return deleteBulletPoint }"
Message: deleteBulletPoint is not defined
In addition to this I have been able to add new empty elements but they are not updated when a user changes the value. is this because the element I am adding is not observable? And if so how do I get round it?
I've added a snippet to ilustrate how you add and remove using an Observable Array with Knockout, the majority of the code is straight from yours so you are in the right track.
Note that when binding to the methods, in this case, there's no need to reference the $parent as you are not using nested scopes. Also, when adding, you just need to pass in plain text, as the observableArray is expecting an object.
If you work with more complex types, when adding you'll need to pass the object itself, and reference its properties from it inside the scope of the iterator, you can read more about it here.
Hope this helps.
var vm = function (data) {
var self = this;
this.BulletPoints = ko.observableArray(["test1", "test2", "test3"]);
this.deleteBulletPoint = function (bulletPoint) {
self.BulletPoints.remove(bulletPoint)
}
this.addEmptyBulletPoint = function () {
const c = self.BulletPoints().length + 1;
self.BulletPoints.push("test "+c);
}
}
ko.applyBindings(vm);
a {
cursor: pointer;
color: red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<h4>Bullet Points <span><a data-bind="click: addEmptyBulletPoint">Add +</a></span></h4>
<div data-bind="foreach: BulletPoints">
<div class="input-group">
<h3 data-bind="text: $data"></h3>
<a data-bind="click: deleteBulletPoint">Delete</a>
</div>
<div>
The problems you're having are related to the way ko.mapping treats arrays of primitive types. By design only properties that are part of an object get mapped to observables so an array of strings will become an observableArray of strings. It will not become an observableArray of observable strings.
In order to add/remove items from an array where the items themselves are observable you'll have to make your BulletPoints array an array of objects having the string as a property within:
data = { BulletPoints: [{value: "test1"}, {value: "test2"}] }
Here's a working example: jsfiddle

Async loading a template in a Knockout component

I'm pretty experienced with Knockout but this is my first time using components so I'm really hoping I'm missing something obvious! I'll try and simplify my use case a little to explain my issue.
I have a HTML and JS file called Index. Index.html has the data-bind for the component and Index.js has the ko.components.register call.
Index.html
<div data-bind="component: { name: CurrentComponent }"></div>
Index.js
var vm = require("SectionViewModel");
var CurrentComponent = ko.observable("section");
ko.components.register("section", {
viewModel: vm.SectionViewModel,
template: "<h3>Loading...</h3>"
});
ko.applyBindings();
I then have another HTML and JS file - Section.html and SectionViewModel.js. As you can see above, SectionViewModel is what I specify as the view model for the component.
Section.html
<div>
<span data-bind="text: Section().Name"></span>
</div>
SectionViewModel.js
var SectionViewModel = (function() {
function SectionViewModel() {
this.Section = ko.observable();
$.get("http://apiurl").done(function (data) {
this.Section(new SectionModel(data.Model)); // my data used by the view model
ko.components.get("dashboard", function() {
component.template[0] = data.View; // my html from the api
});
});
}
return SectionViewModel;
});
exports.SectionViewModel = SectionViewModel;
As part of the constructor in SectionViewModel, I make a call to my API to get all the data needed to populate my view model. This API call also returns the HTML I need to use in my template (which is basically being read from Section.html).
Obviously this constructor isn't called until I've called applyBindings, so when I get into the success handler for my API call, the template on my component is already set to my default text.
What I need to know is, is it possible for me to update this template? I've tried the following in my success handler as shown above:
ko.components.get("section", function(component) {
component.template[0] = dataFromApi.Html;
});
This does indeed replace my default text with the html returned from my API (as seen in debug tools), but this update isn't reflected in the browser.
So, basically after all that, all I'm really asking is, is there a way to update the content of your components template after binding?
I know an option to solve the above you might think of is to require the template, but I've really simplified the above and in it's full implementation, I'm not able to do this, hence why the HTML is returned by the API.
Any help greatly appreciated! I do have a working solution currently, but I really don't like the way I've had to structure the JS code to get it working so a solution to the above would be the ideal.
Thanks.
You can use a template binding inside your componente.
The normal use of the template bindign is like this:
<div data-bind="template: { name: tmplName, data: tmplData }"></div>
You can make both tmplData and tmplName observables, so you can update the bound data, and change the template. The tmplName is the id of an element whose content will be used as template. If you use this syntax you need an element with the required id, so, in your succes handler you can use something like jQuery to create a new element with the appropriate id, and then update the tmplname, so that the template content gets updated.
*THIS WILL NOT WORK:
Another option is to use the template binding in a different way:
<div data-bind="template: { nodes: tmplNodes, data: tmplData }"></div>
In this case you can supply directly the nodes to the template. I.e. make a tmplNodes observable, which is initialized with your <h3>Loading...</h3> element. And then change it to hold the nodes received from the server.
because nodesdoesn't support observables:
nodes — directly pass an array of DOM nodes to use as a template. This should be a non-observable array and note that the elements will be removed from their current parent if they have one. This option is ignored if you have also passed a nonempty value for name.
So you need to use the first option: create a new element, add it to the document DOM with a known id, and use that id as the template name. DEMO:
// Simulate service that return HTML
var dynTemplNumber = 0;
var getHtml = function() {
var deferred = $.Deferred();
var html =
'<div class="c"> \
<h3>Dynamic template ' + dynTemplNumber++ + '</h3> \
Name: <span data-bind="text: name"/> \
</div>';
setTimeout(deferred.resolve, 2000, html);
return deferred.promise();
};
var Vm = function() {
self = this;
self.tmplIdx = 0;
self.tmplName = ko.observable('tmplA');
self.tmplData = ko.observable({ name: 'Helmut', surname: 'Kaufmann'});
self.tmplNames = ko.observableArray(['tmplA','tmplB']);
self.loading = ko.observable(false);
self.createNewTemplate = function() {
// simulate AJAX call to service
self.loading(true);
getHtml().then(function(html) {
var tmplName = 'tmpl' + tmplIdx++;
var $new = $('<div>');
$new.attr('id',tmplName);
$new.html(html);
$('#tmplContainer').append($new);
self.tmplNames.push(tmplName);
self.loading(false);
self.tmplName(tmplName);
});
};
return self;
};
ko.applyBindings(Vm(), byName);
div.container { border: solid 1px black; margin: 20px 0;}
div {padding: 5px; }
.a { background-color: #FEE;}
.b { background-color: #EFE;}
.c { background-color: #EEF;}
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="byName" class="container">
Select template by name:
<select data-bind="{options: tmplNames, value: tmplName}"></select>
<input type="button" value="Add template"
data-bind="click: createNewTemplate"/>
<span data-bind="visible: loading">Loading new template...</span>
<div data-bind="template: {name: tmplName, data: tmplData}"></div>
</div>
<div id="tmplContainer" style="display:none">
<div id="tmplA">
<div class="a">
<h3>Template A</h3>
<span data-bind="text: name"></span> <span data-bind="text: surname"></span>
</div>
</div>
<div id="tmplB">
<div class="b">
<h3>Template B</h3>
Name: <span data-bind="text: name"/>
</div>
</div>
</div>
component.template[0] = $(data)[0]
I know this is old, but I found it trying to do the same, and the approcah helped me come up with this in my case, the template seems to be an element, not just raw html

Using Knockout with repeated user controls

I have a web page that contains a list of games. Each game is presented by a user control, that contains a few labels that hold the properties of the game (time, scores, players, etc.). So the same user control is repeated a few times on the page.
The data changes every minute to support live covarage of the game.
I was hoping to use knockout to update all labels in the user control, but since every user control should bind to a different game data, and a user control cannot have its own view model, I dont know what is the best approach to this scenario.
I need something like a dynamic ViewModel and a dynamic data-bind attributes, but I couldnt find any information on the subject.
Here is a demonstration of the template binding using both data and foreach with the same template. You can see in the JS that the data is the type, a game, but we are dislpaying them separately in the HTML.
HTML
<!-- ko if: favoriteGame -->
<h1>Favorite Game</h1>
<div data-bind="template: { name: 'gameTemplate', data: favoriteGame }"></div>
<!-- /ko -->
<h1>All Games</h1>
<div data-bind="template: { name: 'gameTemplate', foreach: games }"></div>
<script type="text/ko" id="gameTemplate">
<div>
<span class="gameName" data-bind="text: name"></span>
<span data-bind="text: publisher"></span>
<input data-bind="value: score" />
<button data-bind="click: $parent.favoriteGame">Favorite</button>
</div>
</script>
Javascript
var Game = function(data) {
this.name = ko.observable(data.name || "");
this.publisher = ko.observable(data.publisher || "");
this.score = ko.observable(data.score || 0);
};
var ViewModel = function(init) {
var self = this;
self.favoriteGame = ko.observable();
self.games = ko.observableArray(ko.utils.arrayMap(init, function(g) {
return new Game(g);
}));
};
Note that the click: $parent.favoriteGame binding selects the favorite game directly. Knockout passes the current context as the first parameter to function bindings, and since observables are functions, this updates the observable directly, without needing a wrapper function.
You can take a look at this in this fiddle. Its not perfectly clear what you where after, you don't have any code in your question. I hope this isn't too far off.

Passing values to ko.computed in Knockout JS

I've been working for a bit with MVC4 SPA, with knockoutJs,
My problem is I want to pass a value to a ko.computed. Here is my code.
<div data-bind="foreach: firms">
<fieldset>
<legend><span data-bind="text: Name"></span></legend>
<div data-bind="foreach: $parent.getClients">
<p>
<span data-bind="text: Name"></span>
</p>
</div>
</fieldset>
</div>
self.getClients = ko.computed(function (Id) {
var filter = Id;
return ko.utils.arrayFilter(self.Clients(), function (item) {
var fId = item.FirmId();
return (fId === filter);
});
});
I simply want to display the Firmname as a header, then show the clients below it.
The function is being called, but Id is undefined (I've tried with 'Firm' as well), if I change:
var filter = id; TO var filter = 1;
It works fine,
So... How do you pass a value to a ko.computed? It doesn't need to be the Id, it can also be the Firm object etc.
Each firm really should be containing a list of clients, but you could use a regular function I think and pass it the firm:
self.getClientsForFirm = function (firm) {
return ko.utils.arrayFilter(self.Clients(), function (item) {
var fId = item.FirmId();
return (fId === firm.Id());
});
});
Then in html, $data is the current model, in your case the firm:
<div data-bind="foreach: $root.getClientsForFirm($data)">
Knockout doesn't allow you pass anything to a computed function. That is not what it is for. You could instead just use a regular function there if you'd like.
Another option is to have the data already in the dataset on which you did the first foreach. This way, you don't use $parent.getClients, but more like $data.clients.

Categories