I do something like this :
var profileViewModel = new KoProfile(config.data);
ko.applyBindings(profileViewModel , $('#myDiv')[0]);
injectSomeDynamicElements();
#myDiv contains some elements that I want to push content into after the model is bound.
When applying the new profile Knockout re-renders the html that the model is bound too.
My problem is that when I try to inject the new elements, the DOM has not been updated. I end up injecting images into a div that is about to be cleared by knockout.js
Is there a callback function for DOM update completed without using a template ?
What I doing in this cases is this:
OnElementUpdate(element, callback){
var timer = setInterval(function(){
if(!$(element).is(':empty')) {
clear(timer);
callback();
}
}, 100);
}
OnElementUpdate("#myDiv", function(){
//do whatever you want
}
Related
I started developping a website using backbone.js and after trying during the whole morning, i'm quite stuck on the following problem.
I output here only the relevant code.
I've a View called Navigator, that contains a Collection of Records (initially empty) :
var NavigatorView = Backbone.View.extend({
template: JST['app/scripts/templates/Navigator.ejs'],
tagName: 'div',
id: '',
className: 'saiNavigator',
events: {},
initialize: function () {
this.currentRecords = new RecordsCollection();
this.currentRecords.on('reset', this.onRecordsCollectionReseted.bind(this));
},
onRecordsCollectionReseted: function(){
this.render();
},
render: function () {
var tplResult = this.template({
computeTemplate: this.computeTemplate,
records: this.currentRecords
});
this.$el.html(tplResult);
},
onDOMUpdated: function(){
var me = this;
var data = {
device : 'web',
gridId : this.model.get('gridId'),
filterId : this.model.get('filterId')
};
$.ajax({
url: App.getTokenedUrl() + '/task/getGridData.'+this.model.get('taskId')+'.action',
success: me.onRecordReceived.bind(me),
statusCode: {
500: App.handleInternalError
},
type: 'GET',
crossDomain: true,
data : data,
dataType: 'json'
});
},
onRecordReceived: function(result){
var newRecords = [];
for(var i = 0; i < result.items.length; i++){
var newRecord = new RecordModel(result.items[i]);
newRecords.push(newRecord);
}
this.currentRecords.reset(newRecords);
}
});
I've a View called dossier which html is
<div id="dossier1" class="dossier">
<div id="dossier1-navContainer" class="navigatorContainer"/>
<div class="pagesNavigatorContainer"/>
<div class="pagesContainer"/>
<div class="readOnlyFiche"/>
</div>
When i first render the dossier (and i render it only once) i create the navigator in the following render function
render: function () {
this.$el.html(this.template({
uniqBaseId: this.id,
className: this.className
}));
var nav = this.navigator = new NavigatorView({
model : this.model,
id: this.id+'navigator',
el: $('#'+this.id+'-navContainer')
});
this.navigator.render();
//We notify the navigator that it's ready. This will allow the nav to load records
nav.onDOMUpdated();
}
}
As we can see, i give the '#dossier1-navContainer' id to the navigator so that he renders there
So, here is how it works. When i render the dossier, it creates a navigator and inserts it in the DOM. When done, i notify the navigator that it can load its data from the server trough ajax request. When i receive the answer i reset the collection of data with the incoming record.
Juste before the this.$el.html(tplResult) in the navigator render function i output the resulting string.
First time it's
<div class="items"></div>
Second time when i get records, it's
<div class="items">
<div>item1</div>
<div>item2</div>
<div>item3</div>
</div>
So the template generation is correct. However, when the second rendering occurs, the this.$el.html(tplResult) does NOTHING. If i look at the DOM in the browser NOTHING CHANGED
However if i replace this line by
$('#dossier1-navigator').html(tplResult)
it works. Which means that the first time, $('#dossier1-navigator') and this.$el are the same object, the second time not.
I've NO idea why it doesn't work the second time with the standard this.$el.
Help!!
Thanks in advance
Edit : after discussing a lot with Seebiscuit, i'm adding the few lines that helped answering the question
newTask.render();
var taskHtml = newTask.$el.html();
$('#mainTaskContainer').append(taskHtml);
My hunch is that your having a binding problem. I would suggest that you replace
this.currentRecords.on('reset', this.onRecordsCollectionReseted.bind(this)); },
in your initialize, with:
this.listenTo(this.currentRecords, "reset", this.render);
No need to specially bind. Backbone's listenTo bids the callback to the Backbone object that sets the listener (the this in this.listenTo). Also has the added benefit that when you close the view (by calling this.remove()) it'll remove the listener, and help you avoid zombie views.
Try it out.
I think the problem is that you are not using what your are passing to your navigatorView;
In your navigatorView try this:
initialize:function(el) {
this.$el=el
...
}
Let me know if it helps
After countless minutes of discussion with seebiscuit, we came up with the solution. The problem is all on the definition of the $el element. The formal definition defines it as
A cached jQuery object for the view's element. A handy reference instead of re-wrapping the DOM element all the time
This is actually not very exact from a standard cache point of view. From my point of view at least the principle of a cache is to look for the value if it doesn't have it, and use it otherwise. However in this case this is NOT the case. As Seebiscuit told me,
Because when you first bound this.$el = $(someelement) this.$el will always refer to the return of $(someelement) and not to $(someelement). When does the difference matter?
When the element is not in the DOM when you do the assignment
So actually, $el holds the result of the first lookup of the selector. Thus, if the first lookup misses then it won't succeed ever! Even if the element is added later.
My mistake here is to add the main dossierView into the DOM after rendering its NavigatorView subview. I could have found the solution if the $el was a real cache as the 2nd rendering in the ajax callback would have found the element. With the current way $el works i had just nothing.
Conclusion : make sure every part of your view is properly rendered in the DOM at the moment your try to render a subview.
I've got a page for which I poll for notifications. If there are any notifications I populate a Backbone.js view with the notifications. I currently poll for notifications every 2 seconds and simply repopulate the view every two seconds.
function updateNotificationView(ticketId) {
var ticketNotificationCollection = new TicketNotificationCollection([], {id: ticketId});
$.when(ticketNotificationCollection.fetch()).done(function() {
var notificationView = new NotificationsView({collection: ticketNotificationCollection});
$('#ticket-notifications-area').html(notificationView.render().el);
});
}
window.notificationsIntervalId = setInterval(function() {updateNotificationView(ticketId);}, 2000);
I now want to only populate the view if the Backbone fetched collection has changed, but I have no idea how I could do that?
Could anybody give me a tip on how I could only populate the view on collection change? All tips are welcome!
[EDIT]
I now changed the function to this:
function updateNotificationView(ticketId) {
var ticketNotificationCollection = new TicketNotificationCollection([], {id: ticketId});
var notificationsView = new NotificationsView();
notificationsView.listenTo(ticketNotificationCollection, 'change', notificationsView.render);
notificationsView.listenTo(ticketNotificationCollection, 'add', notificationsView.render);
ticketNotificationCollection.fetch();
}
and I changed the NotificationsView to this:
var NotificationsView = Backbone.View.extend({
addNotificationView: function(notification) {
var singleNotificationView = new SingleNotificationView({model: notification});
this.$el.append(singleNotificationView.render().el);
},
render: function() {
console.log('ITS BEING CALLED!!');
this.collection.each(this.addNotificationView, this);
$('#ticket-notifications-area').html(this);
}
});
I now get the "ITS BEING CALLED!!" in the console every two seconds, but also an error:
"Uncaught TypeError: Cannot read property 'each' of undefined.". I get why this error occurs; its because the collection is never actually inserted into the view. I don't get however, how I can solve this.. Any more tips?
I am assuming that you are getting models in the collection in response to your request as JSON.
Method 1:
You can simply set the collection to the JSON. and bind the collection to change event. In case of any change in the collection, callback function will be triggered. In the callback function you can render your view.
Note: change event on collection will be triggered only if there is any change in the models it contain.
e.g.
someCollection.on('change', function(){
someView.render()
});
Method 2:
You can use Backbone's listenTo method, to render the view on change of collection.
someView.listenTo(someCollection, 'change', someView.render);
Hope it helps...
i need a way to parsing the dynamic content of webpage.
For example if I need to append a class for every element with a class "test" i can do like that :
$(".test").each(function(index, element)
{
if(!$(this).data("parsed"))
{
$(this).data("parsed", true);
//all my operation - Example :
$(this).addClass("new-class");
}
});
That work, but only for the html at this time.
If on second time I append new html to my page, that will not parsed by my script.
I would a way for do that.
There is a method called when the content change? For example on every append, html, ajax request etc?
That could be one own solution :
var original_append = $.fn.append;
$.fn.append = function()
{
console.log(arguments);
//my code
return original_append.apply(this, arguments);
}
I've created a bunch of Backbone.js views. Each view has an associated element (view.el).
Given an element on the page — out of context of the view — what would be the best way to get the view for the element?
For example, say some event affects a bunch of elements on a page and I want to call a method on every view associated with the affected elements.
One way would be to assign the view to the element's data, but I'm wondering if I've missed something smarter:
var myview = BackBone.View.extend({
initialize: function(options) {
$(this.el).data('view', this);
...
}
});
(I'm using Backbone with jQuery 1.5.)
I've just written a jQuery plugin for this. It also uses the .data() method.
Registration:
I have wrapped / proxied the Backbone View setElement method to attach the required data to the view's $el property.
Registration is done behind the scenes like so:
$(myViewsEl).backboneView(myView);
Retrieval:
The plugin traverses up the DOM hierarchy (using .closest()) until it finds an element with the required data entry, i.e a DOM element with an associated view:
var nearestView = $(e.target).backboneView();
In addition, we can specify what type of Backbone View we wish to obtain, continuing up the hierarchy until we find an instance of matching type:
var nearestButtonView = $(e.target).backboneView(ButtonView);
JSFiddle Example:
Can be found here.
Notes:
I hope I am correct in thinking there are no memory leaks involved here; An 'unlink' is performed if setElement is called a second time round, and since removing a view's element calls .remove() by default, which destroys all data as well. Let me know if you think differently.
The plugin code:
(function($) {
// Proxy the original Backbone.View setElement method:
// See: http://backbonejs.org/#View-setElement
var backboneSetElementOriginal = Backbone.View.prototype.setElement;
Backbone.View.prototype.setElement = function(element) {
if (this.el != element) {
$(this.el).backboneView('unlink');
}
$(element).backboneView(this);
return backboneSetElementOriginal.apply(this, arguments);
};
// Create a custom selector to search for the presence of a 'backboneView' data entry:
// This avoids a dependency on a data selector plugin...
$.expr[':'].backboneView = function(element, intStackIndex, arrProperties, arrNodeStack) {
return $(element).data('backboneView') !== undefined;
};
// Plugin internal functions:
var registerViewToElement = function($el, view) {
$el.data('backboneView', view);
};
var getClosestViewFromElement = function($el, viewType) {
var ret = null;
viewType = viewType || Backbone.View;
while ($el.length) {
$el = $el.closest(':backboneView');
ret = $el.length ? $el.data('backboneView') : null;
if (ret instanceof viewType) {
break;
}
else {
$el = $el.parent();
}
}
return ret;
};
// Extra methods:
var methods = {
unlink: function($el) {
$el.removeData('backboneView');
}
};
// Plugin:
$.fn.backboneView = function() {
var ret = this;
var args = Array.prototype.slice.call(arguments, 0);
if ($.isFunction(methods[args[0]])) {
methods[args[0]](this);
}
else if (args[0] && args[0] instanceof Backbone.View) {
registerViewToElement(this.first(), args[0]);
}
else {
ret = getClosestViewFromElement(this.first(), args[0]);
}
return ret;
}
})(jQuery);
Every view can register for DOM events. As such, every view with the kind of element that you are interested in should register for the DOM event and then assign an event-responding function that does what you want. If you need to DRY things up, use mixin techniques to mix in the function.
I think maybe this solution is easier than you may have initially imagined. Just let the views do the work that they are intended to do.
You could maintain a views hash (dictionary) that uses the element as the key and returns the view (or views).
http://www.timdown.co.uk/jshashtable/
I've been using a method inspired by Ed's solution but it does not require the use of jQuery. It does two things:
It sets the attribute data-backbone-view on the root elements of all views. This is convenient when looking at the DOM tree in a debugger. You can immediately see which elements are associated with views. You can also use the CSS selector [data-backbone-view] to find elements that are the roots of Backbone views.
It adds a backboneView property to each root element of a view. It is then possible to get from the DOM element to the view by looking a the DOM element's properties.
I turn this on only when I'm debugging. Here is the code:
var originalSetElement = Bb.View.prototype.setElement;
Bb.View.prototype.setElement = function setElement(element) {
if (this.el && this.el !== element) {
delete this.el.backboneView;
}
element.backboneView = this;
element.setAttribute("data-backbone-view", "true");
return originalSetElement.apply(this, arguments);
};
Since every view has a reference to the model its displaying, what I would do is assign id of the model to the view's associated element(hopefuly that is not affected by the changes by outside event). Also make sure that the model has a reference to its view. Then have these models stored in a Backbone collection.
With this setup, once something happens to an element, you use the elements id to retrieve corresponding model from Backbone collection that you created above and then this model will give you your view reference.
I have a DOM element like this:
<div id='master-value'>Apples</div>
I have many other elements elsewhere on the page that I need to sync with the 'master-value'.
<div class='fruit-value' data-reference='master-value'>Apples</div>
<div class='some-fruit' data-reference='master-value'>Apples</div>
When I change the value of the 'master-value', I want all the synced elements to update with it.
$('#master-value').text('Pears');
Should affect:
<div class='fruit-value' data-reference='master-value'>Pears</div>
<div class='some-fruit' data-reference='master-value'>Pears</div>
What I don't want, is on every change of 'master-value' to have to search through all the elements in order to find the synced elements in order to change them. I think that's quite slow when there are many elements that needs to be searched through.
There should be some way for the child values to be pre-bound to the master value so that the selection goes quickly.
$('.fruit-value, .some-fruit').sync('#master-value');
I have some ideas, for instance: I can create an array of preselected synced objects, bind a custom event on the master value and run that event whenever I change the value. The event would go through the array to update all the child elements.
I'm sure there's a better way of doing it though...
Thanks!
You can store the selector once, like this:
var elements = $('.fruit-value, .some-fruit'); //do this once
elements.text($("#master-value").text()); //when you want to sync
The elements variable/jQuery object will keep an array of references to DOM elements so it won't be traversing to find them each time.
wouldn't it be easier to give them all the same class?
So you coud do
$('.fruit').text('Pears')
If you're looking for plugin type of functionality, try this:
When setting up, it takes an object with one property syncWith to set up the elements it should sync with.
When setting the text, it will set the text for the master and the synced elements.
Try it out: http://jsfiddle.net/GH33J/
Just a first attempt. There would be room for improvement if (for example) the master was more than one element. There should be a global reference to all the elements to synchronize and an option to tell if the masters should be synced too.
$.fn.sync = function(arg) {
// if arg plain object, we are doing an initial setup
if ($.isPlainObject(arg)) {
return this.each(function() {
$.data(this, 'syncWith', $(arg.syncWith));
});
// if arg is jQuery object, we are adding new items
} else if (arg.jquery) {
return this.each(function() {
var $set = $.data(this, 'syncWith');
$.each(arg, function() {
$set.push(this);
});
});
console.log(this.data('syncWith'));
// otherwise assume we have a string, and are syncing a new value
} else {
return this.each(function() {
$(this).text(arg);
$.data(this, 'syncWith').text(arg);
});
}
};
// Set up the sync
$('#master-value').sync({
syncWith: '.fruit-value,.some-fruit'
});
var $new = $('<div class="fruit-value">Apples</div>').appendTo('body');
// Pass a jQuery object containing newly created element(s) to add to the set
$('#master-value').sync($new);
// Activate a sync
$('#master-value').sync("pears");
OK here we go:
This is the official data linking plugin from Microsoft. It's now being supported by the jQuery Core team, so we know it's good. :)
http://weblogs.asp.net/scottgu/archive/2010/05/07/jquery-templates-and-data-linking-and-microsoft-contributing-to-jquery.aspx
http://blog.jquery.com/2010/10/04/new-official-jquery-plugins-provide-templating-data-linking-and-globalization/