In my project, I have an <div> where I specifically apply my Knockout.js bindings. I have to instantiate different viewmodels in that area depending on what the user clicks.
To prevent getting a cannot call bindings twice on the same element error, I first have to "Clean" the bindings to make the area available again. I call the initial applyBindings() function:
ko.applyBindings(new ViewModel("Planet", "Earth"), document.getElementById("bindings-area"));
Eventually, I will clean the <div> and call the new bindings:
var element = $("#bindings-area")[0];
ko.cleanNode(element);
ko.applyBindings(new ViewModel("NEW", "Bindings"), document.getElementById("bindings-area"));
Problem: When I include an HTML button in the #bindings-area div, it will no longer work after I clean the bindings and instantiate the new model. I'm sure it has to do with the ko.cleanNode() function somehow removing the button bindings as well. How can I re-initiate them or prevent cleanNode() from operating on the button in the first place?
Fiddle: http://jsfiddle.net/jL6L01xs/2/
This issue is nicely described in Knockout documentation. This quote describes what the issue is and what needs to be done:
When removing an element, Knockout runs logic to clean up any data
associated with the element. As part of this logic, Knockout calls
jQuery’s cleanData method if jQuery is loaded in your page. In
advanced scenarios, you may want to prevent or customize how this data
is removed in your application. Knockout exposes a function,
ko.utils.domNodeDisposal.cleanExternalData(node), that can be
overridden to support custom logic. For example, to prevent cleanData
from being called, an empty function could be used to replace the
standard cleanExternalData implementation:
ko.utils.domNodeDisposal.cleanExternalData = function () {
// Do nothing. Now any jQuery data associated with elements will
// not be cleaned up when the elements are removed from the DOM.
};
Here is the updated jsFiddle.
Related
I have a situation in which a plugin that I have on the page will be references more than a few times. The issue is that the elements that will call the plugin will be created on the fly. And then removed etc... for instance.
I have a div structure and it creates some child elements.
i. I add the plugin instance on these children. They do some magic etc..
ii. User clicks a button, new set of child elements are created -- I create the plugin instances.
NOW -- what happens to those created in point "i" above? I am not using "widget" so I don't have the destroy method.
So, can I just call the child elements, and just loop thru them and destroy the data element etc.. ala.
$.each('[some_class_that_represents_all_elements]', function(a,b){
$(b).data('PLUGIN_NAME') = null; // or
delete $(b).data('PLUGIN_NAME'); // or
$(b).data('PLUGIN_NAME', null);
})
If the elements are removed from the DOM, do the plugin instances also get removed/cleaned up?
jQuery by default will cleanup any data you store using $(el).data() when you call the $(el).remove() method.
If you use $(el).detach() the data is preserved.
http://api.jquery.com/jQuery.data/
jQuery ensures that the data is removed when DOM elements are removed
via jQuery methods, and when the user leaves the page.
Right now, I'm binding events to the parent element of my custom tag's rendered content, then using classes to target the event onto the element which my custom tag actually renders. I feel this is likely to cause strange bugs. For instance, if anyone on my team places two custom tags using the same targeting-classes under the same immediate parent element, it would cause multiple events to fire, associated with the wrong elements.
Here's a sample of the code I'm using now:
$.views.tags({
toggleProp: {
template: '<span class="toggle">{{include tmpl=#content/}}</span>',
onAfterLink: function () {
var prop = this.tagCtx.view.data;
$(this.parentElem).on('click', '.toggle', function () {
prop.value(!prop.value());
});
},
onDispose: function () {
$(this.parentElem).off('click', '.toggle');
}
}
// ... other custom tags simply follow the same pattern ...
});
By the time we hit onAfterLink, is there any reliable way to access the rendered DOM Element (or DOM Elements) corresponding to the custom tag itself? With no risk of hitting the wrong element by mistake? I understand that the custom tag may be text without an HTML Element, but it would still be a text node, right? (Could I even bind events to text nodes?)
In other places, and using (far) older versions of JsViews, I've bound events after the render using (sometimes a lot of) targeting logic built into the rendered elements as data- attributes. Not only is this a far more fragile method than I like for accessing the rendered data, it would be incredibly risky and convoluted to try to apply this approach to some of our deeply-nested-and-collection-ridden templates.
I also don't like needing to insert a span with my custom tag, just so I can apply classes to it, but if it's still necessary for the event, I'll cope.
I ask, then, what is a safe, modular way to bind events to the DOM so that I also have access to the data rendered directly against those elements?
Edit: As an additional concern, using onAfterLink won't let me bind events to non-data-linked rendered content. This may be part of the design intent of JsViews vs pure JsRender, but I don't yet understand why that would be the case.
Rather than using this.parentElem, you can use
this.contents()
which is a jQuery object containing all immediate content elements within the tag.
You can also provide a selector argument,
this.contents("someselector")
to "filter" , and include an optional boolean "deep" flag to both "filter" and "find" - i.e.
this.contents("someselector", true).
Using the above APIs ensures you are only taking elements that are actually within the tag content.
You may not need to remove the handlers in onDispose, if the tag is only deleted along with its content, you can rely on the fact that jQuery will dispose handlers when the elements are removed from the DOM.
You can only attach events to elements, not to text nodes. So if your content does not include elements, you would need to add your wrapper element, but not otherwise.
$.views.tags({
toggleProp: {
template: '{{include tmpl=#content/}}',
onAfterLink: function () {
var prop = this.tagCtx.view.data;
this.contents().on('click', function () {
prop.value(!prop.value());
});
},
onDispose: function () {
this.contents().off('click');
}
}
});
Also take a look at samples such as http://www.jsviews.com/#samples/tagcontrols/tabs which use the above approach.
I've got a page with some Javascript / jQuery stuff, for example:
(function()
{
$('.tip').tooltip();
$('.test').click(function()
{
alert('Clicked!')
});
}();
On the page I insert some HTML with jQuery so the DOM changes. For example, I insert a extra element with the class "tip" or "test". The just new inserted elements doesn't work because jQuery is working with the non-manipulated DOM and the just inserted elements aren't there. So I've searched around and came to this solution for the "click":
$('body').on('click','.click',function()
{
alert('Clicked!')
});
I don't understand why, but this way it's working with the manipulated DOM and the jQuery stuff works on the new inserted elements. So my first question is, why does this work and just the click() function not? And the second question, why do I have to point to the "body"?
Finally, my third question is, how get this done with the tooltip?
I know that there is so many information about this subject (previous the delegate() and live() function I've read) but I can't found a explanation about it. And I can't get my third question solved with the information I found.
I'm looking forward to your responses!
Extra question:
4) Is it recommended to point always to the "body" for this kind of situations? It's always there but for possible performance issues?
So my first question is, why does this work and just the click()
function not?
Because the event handler is now delegated to a parent element, so it remains even after replacing/manipulating child elements.
Ancient article on event delegation for your perusal - but the concepts remain the same:
http://www.sitepoint.com/javascript-event-delegation-is-easier-than-you-think/
And the second question, why do I have to point to the "body"
You don't, any suitable parent element will do. For example, any direct parent (a div wrapper, for instance) which does not get replaced.
Finally, my third question is, how get this done with the tooltip?
You need to re-initialize your tooltip plugin on the newly inserted elements. For example:
$.get("foo.html", function (html) {
$("#someDiv").html(html);
$("#someDiv").find(".tip").tooltip();
});
The click() event doesn't work when you manipulate the DOM because JQuery is not watching for DOM changes. When you bind the click() event it is selecting the elements that are on the page at that time. New ones are not in the list unless you explicitly bind the event.
Because you have pointed the click() event on the body. JQuery then checks to see if the target of the click matches any of the event handlers (like what you have created) match the element clicked. This way any new elements will get the event 'associated' with them.
Because the tooltip isn't an event that you can place on the body, you will need to re-initialize it when the element is created.
EDIT:
For your fourth question, is it depends. The advantage of binding to the body is that you don't accidentally bind an event to an element more than once. The disadvantage is that you are adding event handlers that need to be checked on each event and this can lead to performance issues.
As for your concerns about DRY, put the initialization of the tooltips into a function and call that when you add them. Trying to avoid having the same function call is a little overkill in this regard, IMO.
Events are bound to the specific object you are binding it to.
So something like $('.tip').tooltip() will perform the tooltip() functionality on $('.tip') which is actually just a collection of objects that satisfies the css selector .tip. The thing you should take note of is, that collection is not dynamic, it is basically a "database" query of the current page, and returns a resultset of HTML DOM objects wrapped by jQuery.
Therefore calling tooptip() on that collection will only perform the tooltip functionality on the objects within that collection, anything that was not in that collection when tooltip is called will not have the tooltip functionality. So adding an element that satisfies the .tip selector, after the tooltip() call, will not give it the tooltip functionality.
Now, $('body').on('click','.click', func) is actually binding the click event to the body tag (which should always exist :P), but what happens is it captures whether the click event has passed through an element your target css selector (.click in this case), so since the check is done dynamically, new elements will be captured.
This is a relatively short summary of what's going on... I hope it helped
UPDATE:
Best way for your tooltip thing is to bind tooltip after you have added elements, e.g.
$('#container').load('www.example.com/stuff', function() {
$('.tip', $(this)).tooltip();
});
I have the following page structure:
<ul class="listOfPosts">
<li class="post WCPost" data-postId="1">
<div class="checkbox"><input type="checkbox" class="wcCheckbox"/></div>
<div class="PostContainer>
<!-- some other layout/content here -->
<ul class="listOfLabels">
<li class="label"> Label 1</li>
<li class="label"> Label 2</li>
</ul>
</div>
</li>
<li class="post WCPost" data-postId="2">...</li>
<li class="post WCPost" data-postId="3">...</li>
<li class="post WCPost" data-postId="4">...</li>
...
</ul>
Here is the overly simplistic View:
var MyView = Backbone.View.extend({
el:$('.listOfPosts'),
initialize: function() {
_.bindAll(this,"postClicked");
},
events: {
"click .wcCheckbox":"postClicked"
},
postClicked: function() {
alert("Got a a click in the backbone!");
}
});
Question: I want to know the data Id of post that was clicked. With simple JQuery I can just do the following:
$('.wcCheckbox').live('click' function() {
alert("Data id of post = "+$(this).parents('.WCPost').data('data-postId'));
}
Now I know that Backbone does event delegation so it needs a DOM reference in the el property. If I give it the .listOfPosts then the event seems to fire perfectly but to get "which" posts were checked I'll have to keep traversing the DOM and pick out the elements that were selected!!! It would be quite expensive in my opinion! How do I do this in Backbone for each individual post?? i.e., How do I bind the view to each individual li.WCPost??
NOTE: I'm not using plain vanilla jQuery since the code that I need to write is best done with Backbone's modular design, but since I'm not a pro with Backbone (yet ;) just a bit confused...
Using something like $(event.target).data('postId') in your postClicked() function is a normal way to do this kind of stuff, as far as I can tell.
Extensive usage of events might seem unusual at first, but it's a good way to improve code organization, if used properly. You really can get all the data you want from the event in most cases, especially if you have jQuery. (Note that the event passed to your postClicked function is a regular jQuery event object, and everything you can find about it could be applied. Backbone.js uses jQuery's delegate() function to bind events.)
* * *
However, you still can bind events by yourself in the initialize() method of your view.
initialize: function() {
// Custom binding code:
this.$('.wcCheckbox').live('click' function() {
alert("Data id of post = "+$(this).parents('.WCPost').data('data-postId'));
}
// No need for this anymore:
// _.bindAll(this,"postClicked");
},
(this.$(<selector>) is a function that automatically scopes jQuery selectors to your view, equivalent to $(<selector>, this.el).)
* * *
Another solution (perhaps the most natural in the end, however requiring a bit of work at first) is to create a PostView class, and use it for individual posts and for binding checkbox click event.
To display the posts, in your post list view you go through Posts collection, create a view for each Post instance, and append it to the .listOfPosts element.
You won't need to solve a problem of accessing post's ID anymore, since you would bind the checkbox click event on a PostView. So, in the handler function would be able to find post's ID easily—either through related model (this.model.get('id')) or through view's element ($(this.el).data('postId')).
* * * Update
Now that I generated my posts' DOM independently of Backbone how do I 'retrofit' a view to each post like I mentioned in the question?
I don't want to refactor a ton of client code just to accommodate Backbone. But how do I bind a view to each li??
If you decided to go with MVC and object-oriented JavaScript, you shouldn't manage DOM elements for your posts directly: you create PostView instances, and they, in turn, create corresponding elements, like in todos.js, and fill them with rendered template. (And if you don't want to create elements automatically, Backbone allows you to bind newly created view to the element manually, by specifying el attribute when subclassing. This way, you can bind views to existing elements, for example, on the initial page load.) Any DOM modification related to particular posts should take place inside the PostView class.
Some advantages of OOP approach over DOM-based:
Views are more suitable for data storage than DOM elements.
Note that with your current approach you're using data attributes extensively—for example, to store post's ID. But you don't need to do that if you're operating views: each PostView already knows about its related model, and so you can easily get the post id via, e.g., view.model.get('postId'), as well as any other post data that you want.
You can also store view-specific data that doesn't belong to Post model: for example, animation and / or display state (expanded, selected, …).
Views are more suitable for defining the behaviour of elements.
If you want to, for example, specify a checkbox click event handler for each post, you place this code into your PostView class. This way, event handler knows about all post data, because the view has access to it. Also, the code is better structured—what deals with particular posts, belongs to PostView, and doesn't get in your way.
Another example is a convention to define a render() function on the view. The template function or template string is stored in a view attribute template, and render() renders that template and fills the view's element with resulting HTML:
render: function() {
$(this.el).html(this.template({
modelData: this.model.toJSON()
}));
},
Manipulating DOM may be slower than manipulating objects.
However, DOM-based approach (which seems like you were using until now) has its own pros, especially for less complicated applications. See also: Object Oriented Javascript vs. Pure jQuery and .data storage
Is there a way to bind views to current and future DOM elements that are NOT generated by backbone?
Does that imply that backbone needs to be use from the very start and will be difficult to 'retrofit' so to speak?
As follows from above, you don't need to bind views to future DOM elements (use views to manipulate them).
Regarding binding to the DOM elements that are already on the page when it's initialized, I don't know the ‘right’ way to do that. My current approach is to clear existing elements and create views from scratch, which would automatically create corresponding DOM elements.
This means browser will have some extra work to do at page initialization. For me, advantages of OOP justify it. From the user's point of view, it's also fine, I guess: after that initial initialization, everything will work faster, since page won't need to be reloaded until user does something like logout.
(BTW, I think I should clarify the point of using MVC approach, at least, for me: creating a client-side application that, essentially, does the most work by itself, using the API provided by the back-end server. Server provides data in JSON for models, and your client-side app does the rest, extensively using AJAX. If you do normal page loads when user interacts with your site, then such MVC and heavy client-side might be overhead for you. Another thing is that you will have to refactor some code, which may not be an option depending on your requirements and resources.)
Note that events pass reference to themselves and their point of origin, it's the easiest way to access the origination of the event, in my opinion.
Try it like this, and see if this is what you need (and less convoluted):
var MyView = Backbone.View.extend({
el:$('.listOfPosts'),
initialize: function() {
_.bindAll(this,"postClicked");
},
events: {
"click .wcCheckbox":"postClicked"
},
postClicked: function(e) {
alert("Here is my event origin: "+e.target);
}
});
There is a rich amount of data that can be had from the event, as can be seen here: http://www.quirksmode.org/js/events_properties.html
once you get your head wrapped around javascript eventing, you might look into PubSub/Observer pattern for more-decoupled UI components, here is a good introduction:
http://blog.bobcravens.com/2011/01/loosely-coupled-javascript-using-pubsub/
Nothing prevents you from nesting views. That is, the outer view represents the UL. The UL view would render a bunch of inner LI views. You can nest views as deeply as makes sense.
I've been doing things this way very successfully.
Wondered if there was good way to do this, thought I would post to the SO community...
There is a 3rd party web page that I have no control over how it renders, but they allow me to add JQuery.
Using the JQuery, I am creating a nav menu on the side of the page, it will be a list of links. The onclick event of these links I get from existing onclick events already on the page, but when I do a:
var linkLoc = $('#theLink').attr("onclick");
linkLoc returns:
function onclick(event) {
handleJumpTo("com.webridge.entity.Entity[OID[E471CB74A9857542804C7AC56B1F41FB]]", "smartform");
}
instead of what I would expect:
handleJumpTo("com.webridge.entity.Entity[OID[E471CB74A9857542804C7AC56B1F41FB]]", smartform");
I think JQuery is trying to get the event for binding, but I need the actual Javascript markup since I'm creating the HTML dynamically. I guess I could substring the "function onclick(event) {" out, but seems kind of hacky.
Any ideas of an elegant way I could get the onclick markup?
$("#theLink") would return a jQuery object whereas $("#theLink")[0] would give a DOM object. This is a resson that $("#thelink")[0].getAttributeNode('onclick').value would work.
The type of $('#theLink').attr("onclick") is a function, so you can just use that when you bind events to the links.
var linkLoc = $('#theLink').attr("onclick");
$('a#link1').live('click', linkLoc);
Example: http://jsfiddle.net/BdU6f/
You can also run other code in the click handler too, if you need:
var linkLoc = $('#theLink').attr("onclick");
$('a#link1').live('click', function(e){
// Code...
linkLoc(e);
});
Example: http://jsfiddle.net/BdU6f/1/
The "onfoo" attributes have values that are functions, not strings. The semantics of:
<whatever onclick='code code code'>
are that the browser constructs a function object as if you had code that did this:
document.getElementById('whatever').onclick = new Function("event", "code code code");
Thus you don't really need the raw string, since you've got something better: the function itself, ready to be called. You can then bind it as a handler to other elements via JavaScript code, not HTML (which is really a better way to do things anyway). You're using jQuery, you say, so you can use the jQuery ".bind()" API to bind those functions to whatever elements you need.
You should also be aware that there are other ways of binding event handlers to elements, ways that will leave the "onfoo" attributes completely unset.
If I understand where you're going with this, you should be able to assign the returned onclick function straight through to the onclick of your new nav element...
$('#NewNavElement').click($('#theLink').attr('onclick'));
If you need to add additional code to the handler, you can just bind another click handler.
try this;
$('#theLink').getAttributeNode('onclick').value
Revised as per comment:
$('#theLink').get().getAttributeNode('onclick').value