When using a templeting engine such as Handlebars.js or a data binding one such as Rivets.js, content in the view is generated or updated based on changes in your model. That means that the content itself will lose any event listeners bound to it upon initial generation if the model ever changes.
I could go into the framework itself and customize it to add my specific listener to a specific type of content when it is generated, but this seems like a horrible idea in terms of maintainability and scale etc.
I could bind an event listener to the model object so that when it's changed new listeners are bound to the resulting view, but this seems like it would add a whole lot of overhead.
Or the cleanest way I have thought of is simply having the event listener as an attribute in the html template itself, invoking a globally accessible function.
<script id="foo-template" type="text/x-handlebars-template">
<ul id="NumsSymbs">
{{#each this}}
<li onclick="globalFunction()">{{this}}</li>
{{/each}}
</ul>
</script>
This way whenever the template is re-rendered, or a single item in that list is re-rendered (if using rivets) the listener will be automatically in the rendered html.
My question is if however there is an even better way of doing this, perhaps a standard? How do MVC frameworks like Angular.js or Ember.js handle this sort of thing?
You can delegate in plain javascript as well, it's not really that hard, it just takes a little extra work
document.getElementById('NumsSymbs').addEventListener('click', function(e) {
var target = e.target, // the clicked element that the event bubbled from
search = 'LI'; // element it needs to be inside
do {
if (target.nodeName.toUpperCase() === search) { // if match found
globalFunction();
}
} while (target = target.parentNode); // iterate up through parents
}, false);
I have a custom element in Aurelia that uses a jQuery plugin under the hood (KendoUI). This custom element is being used within a view that uses the if.bind attribute.
The custom element uses an inline template, like so:
#inlineView('<template><input type="text" ref="proxy" class.bind="class"></template>')
Due to the nature of the jQuery plugin, I have to pass an element to initialize. I use the ref that I've defined in my template, like so:
$(this.proxy).doJQueryStuff(...)
After the element is detached, and then reattached, the ref element (this.proxy in my viewmodel) is null.
I initially thought that the problem was that since jQuery mutates the DOM, it was also mutating the view template. I thought that after the element is detached (and destroyed with a jQuery call to remove all KendoUI metadata), the input ref was no longer available resulting in an error upon reattachment because Aurelia was caching the view. However, this is not the case. It's been confirmed that Aurelia doesn't cache views unless you explicitly tell it to do so, and in this simplified plunk, the behavior is as expected.
Why would the reference to my ref element, in my viewmodel, be null after attaching and detaching the element?
Of note, the custom element is part of a page view, inside this construct:
<div id="application" class="au-animate" if.bind="isLoggedIn">
<nav-bar router.bind="router"></nav-bar>
<div class="page-host">
<router-view></router-view>
</div>
</div>
I don't know if anyone has run into any problems with a router-view inside of an if binding.
This issue was fixed this evening by this pull request. Run jspm update and confirm you have github:aurelia/templating-resources#0.17.3 installed.
The issue was the if template controller was not rebinding the view when re-attaching it to the DOM after it had been hidden.
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.
I have a partial view that is loaded via JQuery ajax. It enumerates through properties in the Model and renders HTML elements which need JavaScript events bound.
Here is some simplified razor code I have now
foreach(MyObject item Model.MyObjectList)
{
string containerId = "Container" + item.Id;
string onMouseOut = "DoSomething('"+containerId+"',#Model.Id)";
<div id="#containerId" onmouseout="#onMouseOut">
//Other code here
</div>
}
This works OK however it is often said to be better to bind events in JQuery, if I did this you could also utilise JQuery events such as "onmouseleave".
So another method I could do is place a script block inside each enumeration which sets up the events like so
<script type='text/javascript'>
$('##containerId').mouseout(function(){
DoSomething('#containerId',#Model.Id)
});
</script>
However this results in lots of script block being rendered.
Is there another better solution for setting up events in partial views?
Use the .on method to bind events to things that will not always be inside the DOM.
$('#foo').on('click', function() {
// do stuff
});
You then remove the need to insert script tags in your partials. There was a method similar to this that was .live() but has since been deprecated in v1.7 and removed since v1.9.
jQuery API : .on();
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.