I have a component in knockoutjs for creating list of albums from Facebook using javascript graph api.
How can I call function after render all items?
I have tried foreach: {data: items, afterRender: afterItemsLoaded} - method afterItemsLoaded is called for every row not after render all items.
I have tried to use following code:
HTML:
<h1>List: </h1>
<list></list>
Javascript:
var countOfItemLoaded = 0;
ko.components.register('list', {
viewModel: function() {
},
template: '<div data-bind="foreach: {data: items, afterRender: afterItemsLoaded}"><div data-bind="text: $data"></div></div>'
});
var viewModel = function() {
var self = this;
self.items = ko.observableArray([]);
$.getJSON("https://graph.facebook.com/410564482402231/albums?fields=photos,name,photos.name", function (result) {
$.each(result.data,function(key, item) {
self.items.push(item.name);
});
});
self.afterItemsLoaded = function() {
countOfItemLoaded = countOfItemLoaded + 1;
alert("Loaded item " + countOfItemLoaded);
}
};
ko.applyBindings(viewModel);
Demo in jsFiddle:
jsFiddle link
I don't think support has been added for this yet.
See: https://github.com/knockout/knockout/pull/339
Ryan Niemeyer who works on Knockout did offer this solution to someone else almost 2 years ago
knockoutJS execute callback after foreach finishes rendering
Related
I'm working with an API and Backbone.js at the moment.
I have two views, both render to the same document element #viewContainer. Both of these views render a table with a couple strings to decribe them and a button that opens a form in a modal.
View 1
App.Views.TaskList = Backbone.View.extend({
el: "#viewContainer",
tagName: 'tr',
events: {
"click button": "showTaskForm"
},
showTaskForm: function (event) {
event.preventDefault();
var id = $(event.currentTarget).data("id");
var item = this.collection.get(id);
var formView = new App.Views.Form({
model: item
});
formView.render();
},
render: function () {
changeActive($('#tasksLink'));
var template = _.template($("#taskList").html(), {});
$('#viewContainer').html(template);
// loop and render individual tasks.
this.collection.each(function (model) {
var variables = {
name: model.get('name'),
button: model.getButton()
};
var template = _.template($("#task").html(), variables);
$("#taskTable tbody").append(template);
});
},
collection: App.Collections.Tasks,
});
View 2
App.Views.ProcessList = Backbone.View.extend({
el: "#viewContainer",
tagName: 'tr',
events: {
"click button": "showStartForm"
},
showStartForm: function (event) {
event.preventDefault();
var id = $(event.currentTarget).data("id");
var item = this.collection.get(id);
var formView = new App.Views.Form({
model: item
});
formView.render();
},
collection: App.Collections.Processes,
render: function () {
changeActive($('#processLink'));
var template = _.template($("#processList").html(), {});
$('#viewContainer').html(template);
this.collection.each(function (model) {
var variables = {
processId: model.get('id'),
processName: model.get('name'),
button: model.getButton()
};
var template = _.template($('#process').html(), variables);
$('#processList tbody').append(template);
});
} });
Neither of these views are rendered by default, both need to be activated by a button on the page and they over-write each other in the DOM. However, which ever view is rendered first, the click event of the buttons in that view are the ones that are always fired.
If there is any more information needed from me let me know and I will edit the question.
Be sure to call undelegateEvents() in the first view when you render the second.
Since you're listening for events on the same elements, essentially you attached two listeners for click events on the same button, and when you change your views you are not cleaning up these listeners.
Here's an article that talks about managing events on view change, which should be really helpful to you.
http://lostechies.com/derickbailey/2011/09/15/zombies-run-managing-page-transitions-in-backbone-apps/
As other posters have pointed out, you need to watch out for 'zombie' views (i.e. making sure you undelegate events). If you're building even a moderately complex app, you'll want something that can scale. I find this pattern useful:
var BaseView = Backbone.View.extend({
render: function () {
this.$el.html(this.template());
return this;
},
close: function () {
if (this.onClose) this.onClose();
this.undelegateEvents();
this.$el.off();
this.$el.remove();
}
});
Then whenever you build a view you can do:
var view = BaseView.extend({
//your code
//now the .close() method is available whenever you need to close
//a view (no more zombies!).
});
I am trying to learn Knockoutjs and I am having some issues with adding and removing objects to an observablearray.
I have the following viewmodel in which I fetch some data from a webservice and populate some html. This works fine. But what does not work is removing items from the observablearray since it seems the click-event does not call removeEmployee.
function EmployeeViewModel(){
var self=this;
self.employees=ko.observableArray();
self.removeEmployee = function(item) {
self.employees.remove(item);
};
}
function success(data) {
EmployeeViewModel.employees=ko.mapping.fromJS(data);
ko.applyBindings(EmployeeViewModel);
};
ApiCall({
data: {
[get data]
},
onSuccess: function(data){success(data.result)}
});
and the following html:
<div data-bind="foreach: employees">
<h2>Hello, <span data-bind="text: full_name"> </span>!</h2>
<button data-bind="click: $parent.removeEmployee">Remove</button>
</div>
I have tried setting up a jsfiddle here: http://jsfiddle.net/8yX5M/ in which removing items does work. The difference is, that in the jsfiddle the items are not fetched from an external source and that I use removeEmployee rather than $parent.removeEmployee.
Any ideas why the non-jsfiddle version is not working ?
thanks
Thomas
Because your success function isn't setting the value of the observableArray, it is resetting the object's definition -
function success(data) {
EmployeeViewModel.employees(ko.mapping.fromJS(data));
ko.applyBindings(EmployeeViewModel);
};
Use the setter function on EmployeeViewModel.employees by using the () and passing in a value.
Turned out it was because I did not instantiate EmployeeViewModel to a global variable before mapping the data.
The working code is
'use strict';
var employeeViewModel=new EmployeeModel();
function EmployeeModel(){
var self=this;
self.employees=ko.observableArray();
self.removeEmployee = function(item) {
self.employees.remove(item);
};
}
function getEmployeesSuccess(data,controlIds) {
employeeViewModel.employees=ko.mapping.fromJS(data);
var _i=0;
for (var _total=controlIds.length; _i < _total; _i++) {
ko.applyBindings(employeeViewModel,$("#"+controlIds[_i])[0]);
}
};
/* Databinds employeedata to an array of controls */
/* controlIds=Array of controls*/
function DataBindEmployees(controlIds)
{
ApiCall({
data: {
[get data]
},
onSuccess: function(data){getEmployeesSuccess(data.result, controlIds)} });
};
I am setting some setInterval values on my widget's controller code as follows:
define(['durandal/widget'],function (widget) {
var count = 0;
var intervals = [],
ctor = function (element, settings) {
this.settings = settings;
};
ctor.prototype.updateCount = function( ){
var interval = setInterval(function () {
count = count + 1;
return count;
}, 1000);
intervals.push(interval);
}
return ctor;
}
The above code is being run inside a forEach loop inside the view like:
<div data-bind="foreach: {data: settings.items}">
<span class="count" data-bind="text:$parent.updateCount()"></span>
</div>
What I would like to do is call the clearInterval method on all the items in the intervals array when the widget is destroyed or essentially removed from the dom. I know I could do this using the deactivate on a viewModel but from a reusability point of view, I would like the widget itself to handle the clearing of interval. Is there any way I could achieve this with the widget module in Durandal.
For anyone else looking into the same issue, there's a knockout way of achieving the same. Have a look at the following links https://github.com/BlueSpire/Durandal/issues/139 and https://groups.google.com/forum/#!topic/durandaljs/NqUkY-9us2g . The suggestion is to use:
ko.utils.domNodeDisposal.addDisposeCallback(element, callback)
As long as the widget is removed with JQuery's "remove" function, adding a custom event handler on this "remove" function should go like this:
var self = this;
var self.count = 0;
var self.intervals = [];
self.ctor = function (element, settings) {
$(element).on("remove", function () {
$.each(self.intervals, function(index, ival) {
clearInterval(ival);
});
});
this.settings = settings;
};
The problem is that if the widget is removed without JQuery, simply by manipulating the DOM, the event will not be fired. You could then implement the code for the DOMNodeRemoved event, but it's not gonna work for IE...
Edit: if you're using JQuery pre-1.9.1, you might want to check out the other answers to this question.
Having some issues with a function in Knockout.js. Basically it is a menu where the first menu item "Översikt" should fetch a JSON array and populate the view.
The knockout code:
self.ongoingAuctions = ko.observableArray([]);
self.getOngoingAuctions = function(data) {
$.getJSON("assets/json/auctions.json", function(data) {
self.ongoingAuctions(data);
});
}
My click binding:
The problem is that this only works the first time I click on the menu item. The JSON doesn't get fetched the second, third, n:th time.
What am I doing wrong? Or have I misunderstood something?
Thanks in advance!
I have shared this fiddle for you that shows something else is wrong in your code that you havent not specified in the question:
It makes the call to the non existing json (in my case) every time you click
JS Fiddle to working code
var viewModel = function(){
var self = this;
self.ongoingAuctions = ko.observableArray([]);
self.getOngoingAuctions = function(data) {
$.getJSON("assets/json/auctions.json", function(data) {
self.ongoingAuctions(data);
});
}
self.setHeadline = function(){
console.log('set headline')
}
self.headline = function(){
console.log('headline');
}
}
var myVm = new viewModel();
ko.applyBindings(myVm);
After such a speedy response to my last question I thought I'd try my luck with another :o)
I'm using jQuery UI sortable to sort an ember view. Each item in the view is also a view (like a teaser).
I am adding sortable to the parent view on didInsertElement here.
<script type="text/x-handlebars">
App.SimpleRowListView = Em.View.extend({
didInsertElement: function() {
this.$().sortable({
handle: '.dragger',
items: '.simple-row',
axis: 'y',
update: function(event, ui) {
// update is probably the best event...
}
});
},
});
</script>
Whenever the list is updated I'd like to update my simpleRow.listPosition value to the current index of each .simple-row within it's parent element
I started adding an updateListPosition function to the view used for each row
<script>
updateListPosition : function() {
var index = $('#simple-row-wrapper .simple-row').index(this.$());
this.getPath('content').set('listPosition',index);
},
</script>
With the aim I'd connect my UI update event to trigger this on each of the child views.
I'm now wandering whether the update event should call a function on the controller to loop over all objects and set the listPosition. But in the controller I cannot access this.$() and so can't calculate the index
My plan was to use listPosition as a sorting property on the controller's array. But if there are better ways of sorting the controller array so that it reflects the changes made using .sortable()
Thanks again. I think this might be one quite a few people will want an answer for at some point :)
You need to go through the views. You can either loop each calling your updateListPosition function (which is kind of a heavy job) or you can do something like this
<script type="text/javascript">
App.SimpleRowListView = Em.View.extend({
didInsertElement: function() {
var self = this;
this.$().sortable({
handle: '.dragger',
items: '.simple-row',
axis: 'y',
update: function(event, ui) {
var rows = self.$('.simple-row').toArray();
rows.forEach(function(row) {
var view = Ember.View.views[$(row).attr('id')];
view.updateListPosition();
});
}
});
},
});
</script>
Or one version that seems a little lighter:
<script type="text/javascript">
App.SimpleRowListView = Em.View.extend({
didInsertElement: function() {
var self = this;
this.$().sortable({
handle: '.dragger',
items: '.simple-row',
axis: 'y',
update: function(event, ui) {
var rows = self.$('.simple-row').toArray();
rows.forEach(function(row, position) {
var view = Ember.View.views[$(row).attr('id')];
view. updateListPosition(position);
// pass the new position provided by forEach here and use it instead of calculating again
});
}
});
},
});
</script>