I took over this legacy .NET webforms application, where KO has been used extensively, and I am trying to get an understanding of KO bindings.
The setup:
- a master page containing:
- a usercontrol Announcements.ascx
- a reference to a javascript file named after which page it has been inserted to, lets call it helppageinit.js
The helppageinit.js contains code like this:
$(function () {
var basketViewModel = new AjaxBasket();
var quickBuy = new QuickBuyViewModel(basketViewModel);
$.when(basketViewModel.OnLoad, quickBuy.OnLoad).always(function () {
LeftBasketView(basketViewModel);
ko.applyBindings({ BasketViewModel: basketViewModel, QuickBuyViewModel: quickBuy });
});
});
I want to add KO code to a new file called AnnouncementsInit.js, which is referenced on the master page. The contents of AnnouncementsInit.js is:
$(window).load(function () {
var announcementsViewModel = new AnnouncementsViewModel();
$.when(announcementsViewModel.OnLoad).always(function () {
ko.applyBindings(announcementsViewModel, document.getElementById('announcementContainer'));
});
});
ko.bindingHandlers.stopBinding = {
init: function () {
return { controlsDescendantBindings: true };
}
};
AnnouncementsViewModel looks like this:
AnnouncementsViewModel = function (languange) {
var _onLoad = $.Deferred();
var _announcements = ko.observableArray();
var _singleAnnouncement = ko.observable();
var _imageWidth = ko.observable("40px");
var onClose = function () {
...code removed...
};
_onLoad.resolve();
return {
Announcements: _announcements,
SingleAnnouncement: _singleAnnouncement,
ImageWidth: _imageWidth,
OnClose: onClose,
OnLoad: _onLoad.promise()
};
};
Announements.ascx contains this markup:
<div data-bind="stopBinding: true">
<div id="announcementContainer">
<div class="popContainer" style="display: none">
<!-- ko if: SingleAnnouncement -->
...markup here
<!-- /ko -->
</div>
<div class="divModal" style="display: none" data-cookiename="<%= CookieName %>">
<!-- ko if: Announcements -->
...code here...
<!-- /ko -->
</div>
</div>
</div>
The problem:
The problem is that stopBinding does not seem to work properly. 'Sometimes', I get this error in the console:
Uncaught Error: Unable to parse bindings.
Message: ReferenceError: Announcements is not defined;
Bindings value: if: Announcements
I guess the error occurs because applyBindings in helppageInit.js, applies bindings after announcementInit.js has called applyBindings, even though stopBinding should hinder that.
Any help is much appreciated...
Related
I'm using python to create a dummy server storing JSON data. I'm trying to fetch the data to display it in a dashboard. I keep getting
cannot read property html of undefined
and
cannot read property render of undefined
What am I missing?
My backbone script:
// Create a Model
var Dashboard = Backbone.Model.extend({});
// Create a collection
var DashboardCollection = Backbone.Collection.extend({
model: Dashboard,
url: 'http://localhost:8889/api/test'
});
// create an instance of the collection object
var jobList = new DashboardCollection();
jobList.fetch({success:function(){
test.render();
}});
// Create a jobList view
var jobListView= Backbone.View.extend({
el: $('.jobsList'),
template: _.template($('#test-template').html()),
initialize: function(){
this.render();
//this.listenTo(this.model, 'change', this.render);
//this.listenTo(this.model, 'destroy', this.remove);
},
render : function(){
this.$el.html(this.template({'last_name':'test'}));
return this;
}
});
var test = new jobListView;
And my HTML:
<main>
<div class="row">
<div class="left glass">
<!--[if lt IE 9]>
<div class="legacy-ie-fix"></div>
<![endif]-->
<h1>Job List</h1>
<div class ="jobsList">
</div>
</div>
<div class="right glass">
<!--[if lt IE 9]>
<div class="legacy-ie-fix"></div>
<![endif]-->
<h1>Metrics</h1>
<div id="metrics">
<div class="row">
</div>
</div>
</div>
</div>
</main>
</body>
<script type="text/template" id="test-template">
<table class="table striped">
<thead>
<tr>
<th>Data</th>
</tr>
</thead>
<tbody>
<tr>
<td><%= last_name %></td>
</tr>
</tbody>
</table>
</script>
It seems to be an ordering problem.
Make sure the document is ready
If you use jQuery in your script to grab an element from the document (like el: $('.jobsList')), you must ensure that the HTML is ready. You can wrap your code in a jQuery style document ready function:
$(function() {
var JobListView = Backbone.View.extend({
el: $('.jobsList'),
template: _.template($('#test-template').html()),
render: function() {
this.$el.html(this.template({ 'last_name': 'test' }));
return this;
}
});
});
Or just load the scripts at the bottom of the <body> but inside of it.
<script type="text/template" id="test-template">
Put the template above the scripts loading and inside the body.
</script>
<script src="jquery.js">
<script src="underscore.js">
<script src="backbone.js">
<script src="script/my-app.js">
</body>
The order of the <script> tags on the page is important. Backbone requires jQuery and Underscore.js to be loaded before and your own code requires Backbone (and jQuery, but that's already taken care of in the dependency chain).
Declare and assign variable before using them
You call fetch on the collection, which uses the view before it is assigned. While it could work (see var hoisting), it's best to declare and assign variables before using them when possible.
// Create a list view class
var JobListView = Backbone.View.extend({
el: '.jobsList', // no need to use jQuery here.
template: _.template($('#test-template').html()),
render: function() {
this.$el.html(this.template({ 'last_name': 'test' }));
return this;
}
});
// instantiate the view first.
var test = new JobListView();
// then create an instance of the collection object
var jobList = new DashboardCollection();
// and fetch it when everything is ready.
jobList.fetch({
success: function() {
test.render();
}
});
Notice that JS custom types (classes) should be in PascalCase rather than in snakeCase as a generally approved standard, but that's not going to make the code fail.
Pass the element to the view
To be able to easily reuse your views within different views and templates, you should avoid hard-coding the el property.
Instead, pass the element to the view:
var JobListView = Backbone.View.extend({
// ...
});
// ...somewhere else
var view = new JobListView({ el: '.jobsList' });
Or use the element created by a Backbone view.
var JobListView = Backbone.View.extend({
className: 'jobList',
});
// ...inside a parent view's render
var ParentView = Backbone.View.extend({
template: '<div class="job-list-1"></div><div class="job-list-2"></div>',
render: function() {
this.$el.html(this.template);
this.$('.job-list-1').html(new JobListView().render().el);
this.$('.job-list-2').html(new JobListView().render().el);
// ...
return this;
}
});
This would result in:
<div class="job-list-1">
<div class="jobList"></div>
</div>
<div class="job-list-2">
<div class="jobList"></div>
</div>
I'm writing a twitter aggregator and I need some help on solving the error 'Uncaught ReferenceError: sqTweetData is not defined.' It looks like console is pointing me to my for loop. I have set up a partial that is compiled and loaded in #main-content using underscore js.
For Loop Code
<!-- Main Content -->
<main class="main">
<div class="container-flex" id="main-content"></div>
</main> <!-- End Main Content -->
<!-- Current Tweet Partials -->
<script id="active-tweet-partial" type="underscore/template">
<section class="tweetFlexItem">
<% for (var i = 0; i < sqTweetData.length; i++) { %>
<div class="activeTweet">
<div class="activeTweet__avatar"><img src="<%= sqTweetData[ i ].user.profile_image_url %>"></div>
<div class="activeTweet__wrapper">
<div class="activeTweet__name"> <%= sqTweetData[ i ].user.name %> </div>
<div class="activeTweet__message"><%= sqTweetData[ i ].text %></div>
</div>
</div>
<% } %>
</section>
</script>
home.js Compiling Code
var Home = (function() {
var sqTweetData = {
user: [{
profile_image_url : "assets/avatar.png",
name : "#johnsnow"
}],
text : "Someone once said that I know nothing..."
};
console.log("this is sqTweetData", sqTweetData);
// Partials
var tweetPartial = $('#active-tweet-partial').html();
tweetPartialCompiled = _.template( tweetPartial );
// DOM Handlers
// KICKSTART VIEW
function initHome() {
// load main content
$('#main-content').html(tweetPartialCompiled( sqTweetData ));
// bind events
}
return {
init: initHome
};
})();
The console.log on line 11 works just fine, so I'm assuming my variable object is set up correctly. There just seems to be a disconnect between the partial and the rest of the javascript.
Any thoughts?
This is a scoping issue. sqTweetData says it's undefined because it's exactly that. window["sqTweetData"] does not exist. When you declare a variable outside of a function it's inserted into the global namespace, in this case the browser window is the namespace.
Since you're declaring the variable inside of home using the var keyword, the variable will only be accessible within the Home function. So you'd have to add it as either a this.sqTweetdata and return it with the object, or add a separate getTweetData() that return the variable, or something along those lines.
Check out this answer that covers scoping very comprehensively:
https://stackoverflow.com/a/500459/3629438
Yours falls under:
Advanced: Closure
var a = 1;
var six = (function() {
var a = 6;
return function() {
// JavaScript "closure" means I have access to 'a' in here,
// because it is defined in the function in which I was defined.
alert(a);
};
})();
EDIT:
In your case you would do something along the lines of
var Home = (function() {
// ....... //
function getTweetData() {
return sqTweetData;
}
return {
init: initHome,
get: getTweetData
};
})();
I am new to knockout.js and am trying to create a simple notification message component. What's happening is that the binding appears to occur, but there are no updates happening to the UI. Below is the code, and I would appreciate any help in locating where this is falling down.
Please note: I am using ASP.NET MVC 5, Knockout.js 3.2.0, Require.js 2.1.14 with AMD for accessing scripts and views.
View - Hub
<div class="row">
<notification-hub></notification-hub>
</div>
<button type="button">Push Me!</button>
#section scripts {
<script src="~/Scripts/Views/Home/index.js" type="text/javascript"></script>
}
Hub Script
require(["ko", "jquery", "ViewModels/notificationMessage", "Components/Notifications/hub", "Plugins/jquery.timeago"], function (ko, jquery, notificationMessage, notificationHub) {
try {
// Register the component.
ko.components.register("notification-hub", {
viewModel: notificationHub,
template: { require: "text!/Components/Notifications/HubItemView" }
});
// Create an instance of the hub and add an inital message.
var hub = new notificationHub();
hub.addMessage(new notificationMessage("Test", "This is a test message.", "2015-02-06 11:00 AM"));
// Bind things up.
ko.applyBindings(hub, $("notification-hub")[0]);
// Create a handler for the button click.
$("button").on("click", function () {
hub.addMessage(new notificationMessage("New Message", "This is a new message", new Date().toLocaleDateString()));
});
}
catch (e) {
$("#displayValues").html("Something went wrong...");
Debug.writeln("Script error: " + e.message);
}
});
ViewModel - Hub
define(["ko", "jquery"], function (ko, jquery) {
// Create the hub's main ViewModel.
function notificationHub() {
var self = this;
// Define the Properties.
self.messages = ko.observableArray();
self.count = ko.computed(function () { return self.messages().length; });
}
// Define the addMessage method.
notificationHub.prototype.addMessage = function (msg) {
var self = this;
// Pop message to the top of the stack.
self.messages().push(msg);
Debug.writeln("Count of message array: " + self.messages().length);
}
return notificationHub;
});
View - Message Model
<p data-bind="if: messages().length == 0">Unfortunately we didn't find any records.</p>
<ul data-bind="foreach: messages">
<li class="notificationMessage">
<span class="timeAgo" data-bind="text: createdDate"></span>
<h2 data-bind="text: title"></h2>
<p data-bind="text: message"></p>
</li>
</ul>
<!-- For debugging purposes -->
<input type="text" data-bind="value: count" />
ViewModel - Message Model
define(["ko"], function (ko) {
// Define the ViewModel for the messages themselves.
return function notificationMessage(title, message, date) {
var self = this;
// Define the Properties.
self.title = title;
self.message = message;
self.createdDate = date;
};
});
I'm just getting started with Knockout.js and i have a view(html) which is supposed to be populated by data from a rest api via jquery's $.getJSON method.
When i run the app, nothing shows but using firebug i can see that the 'GET' query returns a status code of 200 and the right data.
I'm at a fix as to why nothing shows in the view since the bindings in Knockout.js are supposed to be automatic.
Below is my code.
Thanks
<div id ='main'>
<!-- ko foreach: posts -->
<p>Hello</p><span data-bind="text: title"></span></p><p data-bind="text: content"></p>
<p data-bind="text: author"></p><p data-bind="text: date"></p>
<!-- /ko -->
</div>
</body>
<script type="text/javascript">
function Post(data){
this.title = ko.observable(data.title);
this.content = ko.observable(data.content);
this.author = ko.observable(data.author);
this.date = ko.observable(data.date)
}
function PostListViewModel(){
var self = this;
self.posts = ko.observableArray([]);
$.getJSON("/posts", function(getPost){
var mappedPost = $.map(getPost, function(item){
return new Post(item)
});
self.posts(mappedPost);
});
}
var postlistviewmodel = new PostListViewModel();
ko.applyBindings(postlistviewmodel);
</script>
This should be:
$.getJSON("/posts", function(getPost){
var mappedPosts = $.map(getPost, function(item){
return new Post(item)
});
self.posts(mappedPosts);
});
wouldn't do self.posts.push(mappedPosts[i]) at all. You should just pass mappedPosts through the ko binding in order to update the listeners.
If your just getting the latest posts and want to update your current list simply do:
var allPosts = self.posts().concat(mappedPosts);
self.posts(allPosts);
You don't need the model to have ko.observable if you're just displaying them. If you want to edit model as well, then leave as.
Also, I tend to do this for single or multiple view models:
ko.applyBindings({viewModel : new viewModel() };
This allows for having multiple named view models. Access scope using: $root.viewModel
This is what I did earlier: http://jsfiddle.net/jFb3X/
Check your code against this fiddle then.
Script tags also need to be above the closing body tags
<html>
<head>
</head>
<body>
<!-- all your html content -->
<script type="text/javascript">
var viewModel = function () {
}
ko.applyBindings({viewModel : new viewModel()});
</script>
</body>
</html>
Is it something as simple as waiting for the DOM to be ready?
Are you able to try the following:
$(function () {
ko.applyBindings(postlistviewmodel);
});
Source: I've done this a few times and been stumped for a bit trying to see what I did wrong. :-)
(As a style thing, I'd also move the /body to after the /script - probably not related to your issue though).
I suspect you get multiple posts from /posts. You only push a single item (array).
...
$.getJSON("/posts", function(getPost){
var mappedPosts = $.map(getPost, function(item){
return new Post(item)
});
for(var i = 0; i < mappedPosts.length; i++) {
self.posts.push(mappedPosts[i]);
}
});
...
Here is my markup:
<section id="Application">
<!-- ko ifnot: initialized -->
<span>Loading...</span>
<!-- /ko -->
<ul data-bind="template:{name: 'clientList', foreach:clients}">
</ul>
<ul data-bind="template:{name: 'storyList', foreach:stories}">
</ul>
</section>
Here is my templates (they are in separate files):
function IncompleteStoriesViewModel() {
//data
var self = this;
self.initialized = ko.observable(false);
self.stories = ko.observableArray();
(function () {
$.ajax({
url: lucidServer.getIncompleteStory(1),
success: function (data) {
ko.mapping.fromJS(data, {}, self.stories);
self.initialized(true);
}
});
})();
};
ko.applyBindings(new IncompleteStoriesViewModel());
function ClientViewModel() {
//data
var self = this;
self.initialized = ko.observable(false);
self.clients = ko.observableArray();
(function () {
$.ajax({
url: lucidServer.getClients(1),
success: function (data) {
ko.mapping.fromJS(data, {}, self.clients);
self.initialized(true);
}
});
})();
};
ko.applyBindings(new ClientViewModel());
My template files are fine, if I call one or the other in the html, they each work individually, but when I try to get them both to show up, only the first one renders, and the second one throws an error that 'stories is not defined' or 'clients is not defined'
I am sure I need to execute this differently, I just can't figure out how. My goal is to have up to 10-15 viewmodels rendering different templates on the same page.
You have to create a view model which will contains all your view models:
function ViewModel(){
var self = this;
self.clientViewModel = new ClientViewModel();
self.storyViewModel = new IncompleteStoriesViewModel();
}
Then apply bindings to the entire page:
ko.applyBindings(new ViewModel());
And adjust html accordingly:
<section id="Application">
<ul data-bind="template:{name: 'clientList', foreach: clientViewModel.clients}">
</ul>
<ul data-bind="template:{name: 'storyList', foreach:storyViewModel.stories}">
</ul>
</section>
Artem's answer is probably the better way, but you do have another option. You can call applyBindings and use the second parameter to specify an element to target.
ko.applyBindings(viewModelA, document.getElementById("one"));
ko.applyBindings(viewModelB, document.getElementById("two"));
This will allow multiple viewmodels to be bound on the page.