I'm new with knockout.js and trying to fix data binding on a site that is build on Laravel and is using knockout.js.
Observable array works well and items can be pushed and popped without issues. The problem is with the binding to GUI. When items are pushed to array those are added to GUI, but nothing else works, like removing items, and also when adding more items later on those are added on the top of the GUI element list, not added after existing items on the GUI. The observable array is having correct items after push/pop/removeall, its just not reflecting to GUI.
I guess that the problem is that observable array is not binded to GUI, but I cannot figure out what could be wrong.
Stripped code:
Chat.init = function(){
Chat.viewModel = new Chat.ViewModel;
ko.applyBindings(Chat.viewModel, $('#msg_canvas').get(0));
};
Chat.ViewModel = function(){
self.messages = ko.observableArray();
self.setMessages = function(msgs){
_.each(msgs, function(msg){
self.messages.push(msg);
});
};
self.clearMessages = function(data, e){
self.messages.removeAll();
}
}
clearMessages is called via onclick: data-bind="click: $parent.clearMessages
The HTML is this:
<div id="msg_canvas" class="msg-wrap col-md-12"
style="height:274px;overflow-y:scroll;" data-bind="foreach: messages">
<div class="media msg">
<div class="media-body">
<span data-bind="text: sent_at"></span>
<small class="col-lg-10" data-bind="text: message"></small>
</div>
</div>
Any help or pointer to what could be causing the problem would be highly appreciated.
UPDATE: added inner HTML which was not included to post before
You need to have a control inside the div to hold your messages, like a <span> or <p>. Otherwise, you're simply doing the foreach without outputting the values. So your div should look something like this, using $data to access the value:
<div id="msg_canvas" data-bind="foreach: messages">
<p data-bind="text: $data"></p>
</div>
Here's a working snippet based on your code (setMessages slightly modified / hard coded with values):
ViewModel = function(){
self.messages = ko.observableArray([]);
self.setMessages = function(){
var msgs = ['message','message','message'];
_.each(msgs, function(msg){
self.messages.push(msg + ' ' + self.messages().length);
});
};
self.clearMessages = function(data, e){
self.messages.removeAll();
}
self.removeMessage = function(item){
self.messages.remove(item);
}
};
ko.applyBindings(new ViewModel());
<script src="http://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.7.0/underscore.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<div id="msg_canvas" class="msg-wrap col-md-12"
style="height:274px;overflow-y:scroll;border: black solid 1px" data-bind="foreach: messages">
<p data-bind="text: $data"></p>
<input type="button" data-bind="click: removeMessage" value="Remove Item" />
</div>
<input type="button" data-bind="click: setMessages" value="Add Message" />
<input type="button" data-bind="click: clearMessages" value="Remove All" />
Related
I have an object (which is observable) and it has keys, each of which is observable. One of those keys is an array that contains other observables.
I want to be able to modify these values using HTML input fields. What came to my mind to do was to simply foreach through the observable array, and do value/textInput bindings to the inputs to modify them.
However, when modifying the text inputs - the value in the original doesn't change! Am I doing something wrong? Why aren't the bound values updating the way I expect?
I've broken down what I'm doing into a more generic version: http://jsfiddle.net/veqr2q6q/
<div class='liveExample'>
<div class="line-container" data-bind="foreach: {data: text, as: 'line'}">
<input type="text" data-bind="textInput: line" /><br />
</div>
<h2>Hello,</h2>
<ul data-bind='foreach: {data: text, as: "line"}'>
<li data-bind="text: line"></li>
</ul>
</div>
// Here's my data model
var ViewModel = function(first, last) {
this.text = ko.observableArray([
ko.observable(first),
ko.observable(last)
])
};
ko.applyBindings(new ViewModel("Planet", "Earth")); // This makes Knockout get to work
I think in this scenario you want to use $rawData. I updated your fiddle here.
http://jsfiddle.net/veqr2q6q/2/
<div class='liveExample'>
<div class="line-container" data-bind="foreach: text">
<input type="text" data-bind="textInput: $rawData" /><br />
</div>
<h2>Hello,</h2>
<ul data-bind='foreach: {data: text, as: "line"}'>
<li data-bind="text: line"></li>
</ul>
</div>
As referenced in the Knockout JS docs
Usually [$rawData] will be the same as $data, but if the view model provided to Knockout is wrapped in an observable, $data will be the unwrapped view model, and $rawData will be the observable itself.
I am using Typescript to define my Knockout ViewModel.
I have a JSON file, the structure of which can be seen here (Github gist as It's a bit large to paste here).
The structure is basically:
OrderLine (the root) -> milestones -> factory_date
Or orally: (many) order lines have (many) milestones, which each have (one) factory date.
I am trying to build a ViewMOdel with the following:
var FactoryAppViewModel = (function () {
function FactoryAppViewModel(seasonID) {
var self = this;
self.seasonID = seasonID;
self.orderlines = ko.observableArray([]);
this.buildViewModel();
}
FactoryAppViewModel.prototype.buildViewModel = function () {
var self = this;
var getOrderLines = HTTP.get("/season/" + self.seasonID + "/orderlines").done(function (data) {
self.orderlines(JSON.parse(data));
}).fail(function () {
error("Could not get orderlines");
});
};
As far as I know, the JSON.parse on the data here will apply the values to the orderlines ko.observableArray([]), However I need to apply a ko.observable to the order lines children (milestones), and to a milestones child (factory_date) as well. And I don't know how to do this. Least of all from JSON.
I have read this but it didn't seem to help me.
I know that the observable isn't applied, because when i change a factory_date in the view, it doesn't update the viewmodel.
Any help would be appreciated. The javascript above is the compiled TypeScript.
EDIT:
Here is an example of the way I'm accessing the code in the view:
<tbody data-bind="foreach: orderlines">
<tr>
<td data-bind="text: factory.name"></td>
<!-- ko foreach: milestones -->
<!-- ko if: factory_date == null -->
<td>
<span>TBA</span>
</td>
<!-- /ko -->
<!-- ko if: factory_date !== null -->
<td>
<div class="wrapper-wrapper">
<div class="btn btn-primary dateChanger">
<span data-bind="text: moment(factory_date.milestone_date).format('DD-MM-YYYY')"></span>
</div>
<div class="date-update-wrapper text-center">
<input type="text" data-bind="attr: {value: moment(factory_date.milestone_date).format('DD-MM-YYYY')}" class="form-control datetimepicker">
<a class="save-date btn btn-success" data-bind="click: function(){$root.saveDate(factory_date, $parent)}"><i class="fa fa-check"></i></a>
<a class="cancel-date btn btn-danger"><i class="fa fa-times"></i></a>
</div>
</div>
</td>
<!-- /ko -->
<!-- /ko -->
</tr>
</tbody>
The part that made me aware I had an issue was this part:
data-bind="click: function(){$root.saveDate(factory_date, $parent)}"
I made a simple saveDate method, which was console.log(factory_date.milestone_date), and it returned the default JSON data, despite me editing it in the view (using the datepicker).
Knockout does not map children in lists to observables by default. There is a plugin called ko.mapping that can help you achieve what you are looking for.
You can set up a "children" mapping, or loop through your children, manually making them observable (In this case from JavaScript):
var mappedChildren = [];
var children = someObj.children();
for (var i = 0; i < children.length; i++) {
var mappedChild = ko.mapping.fromJS(children[i], mappingFunction);
mappedChildren.push(mappedChild);
}
someObj.children(mappedChildren);
I have the following code that, on a successful AJAX return, displays a popup window with a list of addresses. The knockout version is 2.3.0.
If there is more than 1 address then the html correctly renders with a 'display' string.
The problem is that if there is ONLY 1 address the html list renders but without any text in the span.
In both cases the view model is correctly being populated with data so it looks to me like a problem updating the html.
I have tried pushing the data again and although I can use jQuery to update the html but this doesn't help me understand the problem.
HTML
<div id="reverseGeocodingResults">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Local Addresses</h4>
</div>
<div>
<ul data-bind="foreach: vm.reverseGeocodingViewModel.AddressList" class="locationList">
<li class="locationListItem" data-bind="click: SubmitAddressRequest">
<div>
<span data-bind="text: Display"></span>
</div>
</li>
</ul>
</div>
</div>
</div>
Javascript (from a separate file so only taken what I think is relevant)
var vm;
var masterViewModel = function () {
this.viewModel = { LocationList: ko.observableArray([]), SubQuery: ko.observable() };
this.reverseGeocodingViewModel = { AddressList: ko.observableArray([]) };
};
function SubmitReverseGeocodingRequest(easting, northing, projectId, mouseLocation) {
$.post('url?action=ReverseGeocodingLookup', {
easting: easting,
northing: northing,
pid: projectId
})
.done(function (data) {
spinner.stop();
if (parseInt(data.NumberOfAddressesFound) > 0) {
if (data.AddressList.length == 1) {
alert('just 1 address');
}
// remove all array items before adding new
// Not pretty but gets around an issue the UI seems to have displaying the updated list
if (vm.reverseGeocodingViewModel.AddressList().length > 0) {
vm.reverseGeocodingViewModel.AddressList.splice(0, vm.reverseGeocodingViewModel.AddressList().length);
}
vm.reverseGeocodingViewModel.AddressList(data.AddressList);
}
});
)
$(document).ready(function () {
vm = new masterViewModel();
ko.applyBindings(vm);
})
HTML Result - Multiple Results
<ul class="locationList" data-bind="foreach: vm.reverseGeocodingViewModel.AddressList" style="height: 265px;">
<li data-bind="click: SubmitAddressRequest" class="locationListItem">
<div>
<span data-bind="text: Display">Yates Wine Lodge, SWINDON</span>
</div>
</li>
<li data-bind="click: SubmitAddressRequest" class="locationListItem">
<div>
<span data-bind="text: Display">The Brunel Centre, SWINDON</span>
</div>
</li>
</ul>
HTML Result - Single Result
<ul class="locationList" data-bind="foreach: vm.reverseGeocodingViewModel.AddressList" style="height: 265px;">
<li class="locationListItem" data-bind="click: SubmitAddressRequest">
<div>
<span data-bind="text: Display"></span>
</div>
</li>
</ul>
I have looked at various previous questions on this topic and couldn't come up with an answer hence the question, but please point me to another post if I have missed one.
to remove all before add new use reverseGeocodingViewModel.AddressList.removeAll()
Maybe didn't find it but you have to parse data, $.parseJSON I use for this.
Try to use
$.each($.parsejSON(data), function(i, el){
reverseGeocodingViewModel.AddressList.push(el);
})
I hope it will help you
I've noticed following issues:
function SubmitReverseGeocodingRequest should be closed with } not )
foreach: vm.reverseGeocodingViewModel.AddressList should be foreach: reverseGeocodingViewModel.AddressList, without vm because vm is already binded
vm = new masterViewModel();
ko.applyBindings(vm);
To clean AddressList use vm.reverseGeocodingViewModel.AddressList([]) instead vm.reverseGeocodingViewModel.AddressList.splice(0, vm.reverseGeocodingViewModel.AddressList().length);
Note: Here's possible mistake
<li class="locationListItem" data-bind="click: SubmitAddressRequest">
so AddressList item have to have the SubmitAddressRequest function. I don't think that you get data with the function
I've created test sample based on your code, take a look here
I have a rather sophisticated template for Kendo ListView using knockout-kendo.js bindings. It displays beautifully. My problem is that I need to use the visible and click bindings in parts of the template, but I can't get them to work. Below is a simplified version of my template. Basically, deleteButtonVisible determines whether the close button can be seen, and removeComp removes the item from the array.
<div class='template'>
<div >
<div style='display:inline-block' data-bind='visible: deleteButtonVisible, event: {click: $parent.removeComp}'>
<img src='../../../Img/dialog_close.png'></img>
</div>
<div class='embolden'>#= type#</div><div class='label1'> #= marketArea# </div>
<div class='label2'> #= address# </div>
<!-- more of the same -->
</div>
The view model:
function CompViewModel() {
var self = this;
self.compData = ko.observableArray().subscribeTo("compData");
self.template = kendo.template(//template in here);
self.removeComp = function (comp) {
//do something here
}
}
html:
<div class="row" >
<div class="col-md-12 centerouter" id="compDiv" >
<div class="centerinner" id="compListView" data-bind="kendoListView: {data: compData, template: template}"></div>
</div>
</div>
finally, sample data:
{
type: "Comparable",
marketArea: "",
address: "2327 Bristol St",
deleteButtonVisible: true
},
Take in count that the deleteButtonVisible must be a property on the viewModel linked to the view.You are not doing that right now. The click element can v¡be access from the outer scope of the binding and remove the $parent.He take the method from the viewmodel. Take in count that every thing that you take on the vie must be present on the view model for a easy access.
I'm stuck on an older WebForms project and I'd like to know if there's a recommended approach for my scenario.
Goal
I have a feedback form in a modal dialog that I bound up using KnockoutJS.
I would like the feedback form to be available on all pages, via a link in the footer of the site.
I would like to have several other pages using knockout as well with their own individual scripts & bindings, irrespective of the feedback form bindings in the modal.
I have some pages that do not use knockout at all. I would like them not to have to insert code to accomplish this.
I would like to avoid global variables, if possible, in favor of namespaced JavaScript.
In essence, I would like for the viewmodels on the page and the feedback viewmodel not to be aware of each others' existence.
Current Setup
Our footer links are in a Site.master file, and so that's where I've placed the Feedback.js script and the div for the modal which has the bindings. So on the master page, I call ko.applyBindings(vm, referenceToFeedbackDiv), which works fine to wire up the feedback form.
Our individual pages occasionally have a knockout viewmodel, and so they may call ko.applyBindings(vm), since to their knowledge they'd like to apply the vm to their entire page.
Problem
This causes a conflict in knockout because one vm is being applied to the feedback form via the Site.master call, and one vm is being applied to the entire body by the page after it.
Question
How can I enable these two things -- a modal dialog across all pages that uses knockout, and individual knockout pages -- to work in harmony?
Demonstration of the Issue in (the Current) Code
Remember, the issue is that I want to be able to have one feedback VM that applies only to the feedback div across the client site, and I want to have other VMs able to be applied that aren't required to know anything about the feedback vm.
Master Page file (Site.Master) -- Excerpt
This is on every page:
<div class="page">
<div class="main">
<asp:ContentPlaceHolder ID="MainContent" runat="server" />
</div>
</div>
<div class="footer">
© <%=DateTime.Now.Year.ToString() %> Company, Inc. | Home | About |
<!-- begin feedback area -->
<span id="FeedbackArea">
<a data-bind="click: showModal">Feedback</a>
<div id="feedback-modal" title="What's on your mind?">
<div class="btn-group" id="feedbackButtonGroup">
<button class="btn" data-bind="click: UpdateFeedbackType" style="padding-top: 6px;">
<i class="fa fa-warning fa-2x fa-align-center"></i>
<br />
<span>Problem</span>
</button>
<button class="btn" data-bind="click: UpdateFeedbackType" style="padding-top: 6px;">
<i class="fa fa-question-circle fa-2x fa-align-center"></i>
<br />
<span>Question</span>
</button>
<button class="btn" data-bind="click: UpdateFeedbackType" style="padding-top: 6px;">
<i class="fa fa-lightbulb-o fa-2x fa-align-center"></i>
<br />
<span>Suggestion</span>
</button>
<button class="btn" data-bind="click: UpdateFeedbackType" style="padding-top: 6px;">
<i class="fa fa-thumbs-o-up fa-2x fa-align-center"></i>
<br />
<span>Praise</span>
</button>
<button class="btn" data-bind="click: UpdateFeedbackType" style="padding-top: 6px;">
<i class="fa fa-info-circle fa-2x fa-align-center"></i>
<br />
<span>General</span>
</button>
</div>
<br />
<br />
<textarea rows="5" placeholder="Enter feedback here" data-bind="value: feedbackText, valueUpdate: 'afterkeydown'"></textarea>
<br />
<br />
<button>Send Feedback</button>
<button data-bind="click: CancelFeedback">Cancel</button>
<h3>Other Information: </h3>
<ul>
<li><strong>Feedback Type:</strong> <span data-bind="text: feedbackType"></span></li>
<li><strong>Current URL:</strong> <span data-bind="text: pageUserIsOn"></span></li>
<li><strong>Current User: </strong><%=hdnLoggedInUsername.Value %></li>
<li><strong>Current Client: </strong>[Not yet captured]</li>
<li><strong>Current Tab: </strong>[Not yet captured]</li>
</ul>
</div>
</span>
<!-- End feedback area -->
</div>
Feedback.JS -- This is also included in every page
...a somewhat-namespaced definition of a FeedbackVM:
var FeedbackNamespace = FeedbackNamespace || {};
..the definition of the namespace itself:
FeedbackNamespace = {
ViewModel: function () {
// etc. etc.
}
};
...and the declaration of a VM variable plus wiring it up on document.ready():
var FeedbackVM;
$(document).ready(function () {
FeedbackVM = new FeedbackNamespace.ViewModel();
ko.applyBindings(FeedbackVM, $('#FeedbackArea')[0]);
FeedbackVM.Start();
log('FeedbackVM started');
});
Other Pages without Knockout / JS
Other pages may or may not have any javascript on them at all, let alone knockout. On these pages, the FeedbackVM currently works fine.
Pages with their own Knockout ViewModel
These pages would have their own namespaced JS file with their own document.ready() event, that creates a vm of say invoiceUploaderVM = new InvoiceUploader.ViewModel(), and then calls ko.applyBindings(invoiceUploaderVM).
This is where we run into trouble.
Update: One potential Approach and a little trouble
In the Site.master page, I wrapped my entire footer in a "stopBindings: true" div:
<div data-bind="stopBindings: true">
<div class="footer" id="footerDiv">
<!-- Feedback Viewmodel stuff in here -->
</div>
</div>
I've defined stopBindings as:
ko.bindingHandlers.stopBindings = {
init: function () {
return { controlsDescendantBindings: true };
}
};
My Feedback.js file, loaded on every page as part of a global JS file, has:
var FeedbackNamespace = FeedbackNamespace || {};
FeedbackNamespace = {
// defines viewmodel, etc. etc.
};
var FeedbackVM;
$(document).ready(function () {
FeedbackVM = new FeedbackNamespace.ViewModel();
ko.applyBindings(FeedbackVM, $('#footerDiv')[0]);
FeedbackVM.Start();
log('FeedbackVM started');
});
This approach works perfectly well -- as long as there are no other viewmodels being bound. On the pages that inherit from my master page, I might have something like:
$(document).ready(function () {
'use strict';
vm = new invoiceUploader.ViewModel();
ko.applyBindings(vm);
});
I would expect that this:
Sets up the feedback viewmodel applied to the div, stopping other viewmodels
Sets up the invoiceUploader viewmodel and applies it to the body (which is then stopped by the stopBindings div)
However, instead I get an error upon loading the child page along the lines of:
Commenting the line to apply the feedback bindings makes this work just fine again.
What am I doing wrong?
I think I would put the view model for your modal in a global object and do whatever you need to do with it aside from applying the bindings in a shared script:
window.feedbackModal = {
foo: ko.observable("Whatever you need to do here"),
bar: ko.observable("assuming it can be done the same on every page")
};
Then in the Site.master
<div class="feedback-modal" data-bind="with: feedbackModal">
<p data-bind="text: foo"></p>
<p data-bind="text: bar"></p>
</div>
And in every individual page's script:
function ViewModel() {
this.individualProperty = ko.observable(true);
this.specificAction = function() { /* do something specific to this page */ };
this.feedbackModal = window.feedbackModal;
}
ko.applyBindings(new ViewModel());
So window.feedbackModal could be undefined and it won't cause you problems, but if you ko.applyBindings, you have to have a feedbackModal property exposed in the view model or you'll get errors applying those bindings.
Of course, there are more clever ways you could implement this basic idea in order to fit your patterns the best, but the big point is, as you know, you can't apply bindings twice, so you need to defer that task to your most specific code and expose your reusable code to to it.
Here is another strategy for separation of common modules from page dependant modules:
// An example of a module that runs on everypage
var modalDialog = function(){
this.name = "dialog1";
this.title = ko.observable("My Modal Title");
this.content = ko.observable("My Modal content is also something");
}
// An example of a module that runs on everypage
var modalDialog2 = function(){
this.name = "dialog2";
this.title = ko.observable("My Modal Title 2");
this.content = ko.observable("My Modal content is also something 2");
}
// Either generate it automatically or by hand
// to represent which modules are common
var commonModules = [modalDialog, modalDialog2];
// An example of a module only for this page
var pageModule = function(){
this.pageFunction = function(){
alert("Called page function");
}
}
// Composition is the final object you will actually bind to the page
var composition = {
pageMod: new pageModule()
}
// Let's add the common modules to the composition
ko.utils.arrayForEach(commonModules, function(item){
var module = new item();
composition[module.name] = module;
});
// Bind the composition
ko.applyBindings(composition);
example HTML for this would be:
<div class="modalDialog">
<h2 data-bind="text: dialog1.title"><h2>
<h2 data-bind="text: dialog1.content"><h2>
</div>
<div class="modalDialog">
<h2 data-bind="text: dialog2.title"><h2>
<h2 data-bind="text: dialog2.content"><h2>
</div>
<div id="content">
<h2>Welcome to page</h2>
<div id="somePageStuff">
Click me
</div>
</div>
Link to the jsfille for this
You can set this up by using a technique to not have scope your bindings to a specific area in your page.
Check out: How to stop knockout.js bindings evaluating on child elements
Example:
http://jsfiddle.net/anAgent/RfM2R/
HTML
<div id="Main">
<label data-bind="text: ViewModel.Name">default</label>
<div data-bind="stopBindings: true">
<div id="ChildBinding">
<label data-bind="text: AnotherViewModel.Name">default</label>
</div>
</div>
</div>
JavaScript
$(function () {
ko.bindingHandlers.stopBindings = {
init: function () {
return {
controlsDescendantBindings: true
};
}
};
var data = {
ViewModel: {
Name: "Testing"
}
};
var data2 = {
AnotherViewModel: {
Name: "More Testing"
}
};
ko.applyBindings(data, $("#Main")[0]);
ko.applyBindings(data2, $("#MyModalHtml")[0]);
});