I am using knockoutjs for my single page application, and I am currently stuck on a mysterious problem.
I am trying to display a dropdown menu, and populate it using knockout bindings. For this purpose, I am using a foreach that iterates over all elements:
<div data-bind="foreach: favPlaces" class="dropdown-menu" aria-labelledby="dropdownMenuButton">
<a data-bind="text: name, click: $parent.openInfoWindow" class="dropdown-item">Place Name</a>
</div>
Then, in my viewModel, I have the openInfoWindow function (which is supposed to be called when a dropdown-item is clicked):
// View Model
var TokyoViewModel = function() {
var self = this;
// All the favorite places
this.favPlaces = ko.observableArray([]);
mFavPlaces.forEach(function(place) {
self.favPlaces.push(new FavPlace(place));
});
this.openInfoWindow = function(favPlace) {
console.log("Success!");
}
}
The problem is, when I add the click: openInfoWindow binding to the dropdown-item element, I get the following error:
Uncaught TypeError: Unable to process binding "foreach: function (){return favPlaces }"
Message: Unable to process binding "click: function (){return $parent.openInfoWindow }"
Message: u(...).bind is not a function
at Object.p (knockout-3.4.1.js:17)
at knockout-3.4.1.js:89
at Object.b (knockout-3.4.1.js:9)
at init (knockout-3.4.1.js:89)
at init (knockout-3.4.1.js:103)
at knockout-3.4.1.js:72
at Object.w (knockout-3.4.1.js:39)
at knockout-3.4.1.js:72
at Object.q (knockout-3.4.1.js:11)
at m (knockout-3.4.1.js:71)
The text: name binding works perfectly on its own.
Where did I commit a mistake?
EDIT:
Here is more details about the implementation. Note that the map div is a map that uses the Google Maps API.
<body>
<div id="full-height">
<div id="map"></div>
<nav class="navbar navbar-inverse bg-inverse navbar-toggleable-md navbar-light bg-faded">
<a class="navbar-brand" href="#">
<i id="foursquare-logo" class="fa fa-foursquare" aria-hidden="true"></i>
</a>
<div id="location-dropup" class="btn-group dropup">
<button type="button" class="btn btn-secondary">Best locations</button>
<button type="button" class="btn btn-secondary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="sr-only">Toggle Dropdown</span>
</button>
<div data-bind="foreach: favPlaces" class="dropdown-menu" aria-labelledby="dropdownMenuButton">
<a data-bind="text: name, click: $parent.openInfoWindow" class="dropdown-item">Place Name</a>
</div>
</div>
</nav>
</div>
<!-- Foursquare logo -->
<script src="https://use.fontawesome.com/5228693ec0.js"></script>
<!-- Bootstrap -->
<script src="https://code.jquery.com/jquery-3.1.1.slim.min.js" integrity="sha384-A7FZj7v+d/sdmMqp/nOQwliLvUsJfDHW+k9Omg/a/EheAdgtzNs3hpfag6Ed950n" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/tether/1.4.0/js/tether.min.js" integrity="sha384-DztdAPBWPRXSA/3eYEEUWrWCy7G5KFbe8fFjk5JAIxUYHKkDx6Qin1DkWx51bBrb" crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/js/bootstrap.min.js" integrity="sha384-vBWWzlZJ8ea9aCX4pEW3rVHjgjt7zpkNpZk+02D9phzyeVkE+jo0ieGizqPLForn" crossorigin="anonymous"></script>
<!-- KnockoutJS -->
<script src="js/lib/knockout-3.4.1.js"></script>
<script src="js/app.js"></script>
<script async defer src="https://maps.googleapis.com/maps/api/js?key=AIzaSyDtODGjlNobKNCo4OX_voxjIkNkHCfQ3I4&callback=initMap"></script>
</body>
Here is more details about the relevant javascript used.
var mFavPlaces = [
{
name: "Takeshita Street",
lat: 35.6715659,
lng: 139.7031469,
imgSrc: "img/favPlaces/takeshita.jpg"
}, {
name: "Nakamise Street",
lat: 35.7113873,
lng: 139.794207,
imgSrc: "img/favPlaces/asakusa.jpg"
}, {
name: "Yodobashi-Akiba",
lat: 35.6995227,
lng: 139.7734171,
imgSrc: "img/favPlaces/akihabara.jpg"
}, {
name: "Meiji Jingu",
lat: 35.6763976,
lng: 139.6993259,
imgSrc: "img/favPlaces/meiji.jpg"
}, {
name: "Shibuya Crossing",
lat: 35.6594087,
lng: 139.6981677,
imgSrc: "img/favPlaces/shibuya.jpg"
}
];
// Stores the Google maps markers for the favPlaces
var mMarkers = {};
var mQueryInfo = {
"near": "Tokyo",
"client_id": "AG5MATDOQ5HAXLODDIV1YALJZA4IN3LS5XEUOPWQIGHG0BHL",
"client_secret": "PPJYHED0SI5WLWC05LXGD1E3T1JDQI23EWNSTQLI2MO0WEAF",
"version": "20170220"
};
function httpGetAsync(theUrl, callback, infoWindow, placeIndex, marker) {
var xmlHttp = new XMLHttpRequest();
xmlHttp.onreadystatechange = function() {
if (xmlHttp.readyState == 4 && xmlHttp.status == 200) {
callback(xmlHttp.responseText, infoWindow, placeIndex, marker);
}
}
xmlHttp.open("GET", theUrl, true); // true for asynchronous
xmlHttp.send(null);
}
function setFavPlaceInfo(placeName, infoWindow, placeIndex, marker) {
var url = "https://api.foursquare.com/v2/venues/search?limit=1&near=" + mQueryInfo.near + "&query=" + placeName + "&v=" + mQueryInfo.version + "&client_id=" + mQueryInfo.client_id + "&client_secret=" + mQueryInfo.client_secret;
httpGetAsync(url, setInfoWindowContent, infoWindow, placeIndex, marker);
}
function setInfoWindowContent(placeDetailsText, infoWindow, placeIndex, marker) {
var placeDetails = JSON.parse(placeDetailsText);
// Extract info to display in infoWindows
var completeName = placeDetails.response.venues[0].name;
var address = placeDetails.response.venues[0].location.address;
var websiteUrl = placeDetails.response.venues[0].url;
var numberCheckins = placeDetails.response.venues[0].stats.checkinsCount;
var contentString = '<div class="infoWindow">' + '<img class="img-fluid img-thumbnail" src="' + mFavPlaces[placeIndex].imgSrc + '" style="margin-bottom:1rem;" alt="' + completeName + '" />' + '<h4>' + completeName + '</h4>' + '<b>Checkins: </b>' + numberCheckins + '<br>' + '<b>Website: </b> <a href="' + websiteUrl + '">' + websiteUrl + '</a>' + '<br>' + '<b>Address: </b> ' + address + '</div>';
infoWindow.setContent(contentString);
infoWindow.open(mMap, marker);
}
// Object representation of a favorite place
var FavPlace = function(data) {
this.name = ko.observable(data.name);
this.imgSrc = ko.observable(data.imgSrc);
}
// View Model
var TokyoViewModel = function() {
var self = this;
// All the favorite places
this.favPlaces = ko.observableArray([]);
mFavPlaces.forEach(function(place) {
self.favPlaces.push(new FavPlace(place));
});
this.openInfoWindow = function(favPlace) {
console.log("Success!");
}
}
// Initialize the map and adds markers with infoWindows
function initMap() {
var center = { lat: 35.6809814, lng: 139.7538745 };
mMap = new google.maps.Map(document.getElementById('map'), {
zoom: 12,
center: center
});
// Get details of favorite places
var placeIndex;
for (placeIndex = 0; placeIndex < mFavPlaces.length; placeIndex++) {
var marker = new google.maps.Marker({
position: { lat: mFavPlaces[placeIndex].lat, lng: mFavPlaces[placeIndex].lng },
map: mMap,
});
var infowindow = new google.maps.InfoWindow({});
// Use a closure to add listeners
google.maps.event.addListener(marker, 'click', (function(marker, placeIndex) {
return function() {
// Set infoWindow's content
setFavPlaceInfo(mFavPlaces[placeIndex].name, infowindow, placeIndex, marker);
// Set marker animation (lasts for 1 cycle == 750ms)
marker.setAnimation(google.maps.Animation.BOUNCE);
setTimeout(function() { marker.setAnimation(null); }, 750);
}
})(marker, placeIndex));
// Store the marker
mMarkers[mFavPlaces[placeIndex].name] = marker;
}
}
// Activates knockout.js
ko.applyBindings(new TokyoViewModel());
EDIT 2:
With your updated source code I was able to create a jsFiddle that reproduced the problem. fiddle
It seems the slim version of jQuery you're loading is missing a few functions that knockout assumes to be there. Specifically in this case the ".bind" function appears to be used inside of the "foreach" binding. If you replace this script with the standard jquery that should clear up.
EDIT 1:
Thank you for your answer. This is actually a mistake from my side, I forgot to update my code. I actually already had the $parent.openInfoWindow. I have updated the error log as well.
In that case the problem isn't with any of the code you posted. Here's a working snippet which I threw together from your code above except for the initial "mFavPlaces.forEach" which I don't have access to.
// View Model
var TokyoViewModel = function() {
var self = this;
// All the favorite places
this.favPlaces = ko.observableArray([]);
//...
self.favPlaces.push(new FavPlace("name goes here?"));
this.openInfoWindow = function(favPlace) {
console.log("Success!");
}
}
var FavPlace = function(name){
//unknown view-model
var self = this;
self.name = ko.observable(name);
}
ko.applyBindings(new TokyoViewModel());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<div data-bind="foreach: favPlaces" class="dropdown-menu" aria-labelledby="dropdownMenuButton">
<a data-bind="text: name, click: $parent.openInfoWindow" class="dropdown-item">Place Name</a>
</div>
Original Answer:
The context for the binding at that point is the individual place. The name binding works because name is a property on each place. Your click function, however, appears to be on the parent view-model. You can change the click binding to be: data-bind="text: name, click: $parent.openInfoWindow"
and it should function.