KnockoutJS Hover on UL doesn't trigger, works with CSS:hover? - javascript

So I've got a organizational system for a geocaching site I'm working on, and for some reason I cannot for the life of me get a onmouseover to fire on this UL.
<ul data-bind="foreach: markers, event { onmouseover: highlightLocation }" class="locationList">
<h3 class="resultTITLE"> <span data-bind="text: title"></span></h3>
<p class="resultLATLONG">LAT <span data-bind="text: lat"></span>, LONG <span data-bind="text: lng"></span></p>
</ul>
If I do a simple CSS :hover pseudo the UL styles will change, but if I try to use it through KnockoutJS, I can't get anything to fire. As a reference with the other code removed, here's what my viewModel looks like:
var viewModel = {
//other code inbetween, linted
highlightLocation : function() {
console.log("hovering!");
},
};
ko.applyBindings(viewModel);
I've been staring at this for the past few hours and I feel like I'm msissing something simple that's staring me right in the face that I'm missing. Any thoughts?

Not really familiar with knockout but noticed a few things and got it to work.
You are missing a colon after event and the event is actually called mouseover, not onmouseover.
var viewModel = {
title: "title",
lat: 5,
lng: 5,
highlightLocation: function() {
console.log("hovering!");
},
};
ko.applyBindings(viewModel);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<ul data-bind="event: { mouseover: highlightLocation }" class="locationList">
<h3 class="resultTITLE"> <span data-bind="text: title"></span></h3>
<p class="resultLATLONG">LAT <span data-bind="text: lat"></span>, LONG <span data-bind="text: lng"></span></p>
</ul>
(fiddle)

Related

knockout click binding failing to call JS function

I have a list of results from an API call rendering in a list (HTML below) and have added a click binding with Knockout to a JS function that basically takes the name of the result, searches an array of google maps markers for a match, then pans to the marker on the map element and opens the previously stored infoWindow. When I open developer tools I can see that there is a listener on the element for a 'click' action that points to a Knockout function, but when I click, nothing happens. Here is the gitHub repo : https://github.com/ZackBoyd/BrewMap, if that is useful and the specific code is below. I feel like I'm overlooking something very basic, thank you in advance for any help.
HTML
<ul class="results list" data-bind="foreach: filteredBreweries">
<li data-bind="click: $parent.goToMarker">
<div class="item">
<a class="image loaded">
<img data-bind="attr: { src: iconImage }">
</a>
<div class="wrapper">
<h4 data-bind="text: name"></h4>
<figure data-bind="text: address"></figure>
<figure data-bind="text: yearEstablished"></figure>
<figure data-bind="text: type"></figure>
</div>
</div>
</li>
</ul>
JS
function goToMarker(clickedBrewery){
var clickedBreweryName = clickedBrewery.name;
console.log(clickedBrewery);
console.log(clickedBreweryName);
for (var key in self.mapMarkers()) {
if (clickedBreweryName === self.mapMarkers()[key].marker.title) {
map.panTo(self.mapMarkers()[key].marker.position);
infowindow.setContent(self.mapMarkers[key].marker.content);
infowindow.open(map, self.mapMarkers()[key].marker);
}
}
};
This code is within the scope of the 'appViewModel' function, you can see it in the repo, I didn't want to include all of it for simplicity's sake.
Try to assign the function to a variable in your viewmodel. For instance:
self.goToMarker = function(clickedBrewery){
var clickedBreweryName = clickedBrewery.name;
console.log(clickedBrewery);
console.log(clickedBreweryName);
for (var key in self.mapMarkers()) {
if (clickedBreweryName === self.mapMarkers()[key].marker.title) {
map.panTo(self.mapMarkers()[key].marker.position);
infowindow.setContent(self.mapMarkers[key].marker.content);
infowindow.open(map, self.mapMarkers()[key].marker);
}
}
};
I don't know what your viewmodel looks like tho.
But yeah that internal function wont be exposed to the bindings i guess.

Knockout binding with a handle to templated element

Knockout is just great but I'm a little bit confused on how to deal with DOM elements after they are generated. For example I have a collection of users. Each user has an Id:
var user = {
id : 123,
name : 'testUser',
age: 45
};
Using Knockout I bind my collection of described above data structure with the following html template:
<div data-bind="foreach: users">
<div class='user-wrapper'>
<span data-bind="text: name"></span>
<span data-bind="text: age"></span>
</div>
</div>
and now I want to change background color on user click:
$(".user-wrapper").click(function (e) {
//doesn't work - toggelClass is not a function
e.target.toggleClass("user-selected");
});
Once I hit a user target could be different (span or div), I need to make sure that I'm getting the right div. Moreover e.target doesn't work with "not a fucntion" error.
How can I access calling element to toggle the class?
How can I get a user id of that element to access other controls related to that id?
You should use the click binding in conjunction with the css binding:
<div data-bind="foreach: users">
<div class='user-wrapper' data-bind="click: toggleSelected, css: { 'user-selected': isSelected }">
<span data-bind="text: name"></span>
<span data-bind="text: age"></span>
</div>
</div>
Note that if you're ever tempted to use jQuery to manipulate DOM while you're using KnockoutJS (or client side MVVM libraries in general): don't. If you absolutely must, you probably need a custom binding handler, much like you'd use a directive for DOM manipulation in "that other" mvvm framework.
Here's a demo:
var user = {
id : 123,
name : 'testUser',
age: 45
};
var UserVm = function(data) {
var self = this;
self.name = data.name;
self.age = data.age;
self.isSelected = ko.observable(false);
self.toggleSelected = function() {
self.isSelected(!self.isSelected());
}
};
ko.applyBindings({ users: [new UserVm(user)] });
.user-selected { background-color: red; }
.user-wrapper:hover { cursor: pointer; background-color: pink; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<div data-bind="foreach: users">
<div class='user-wrapper' data-bind="click: toggleSelected, css: { 'user-selected': isSelected }">
<span data-bind="text: name"></span>
<span data-bind="text: age"></span>
</div>
</div>

ObservableArray not binding to GUI

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" />

KnockoutJS - foreach not working with a single entry in viewmodel

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

How can I apply KnockoutJS on a master page and an individual page?

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

Categories

Resources