EmberJS - Handling events for multiple elements inside a view - javascript

I have a Handlebars template called 'projects' and a view called 'ProjectsView' which references the template with the templateName property.
This projects template generates a list of divs from a model and each div needs to be expanded on a click.
{{#each project in model}}
<div class="project">
<img src="{{project.logo}}" /> <!-- Execute a different logic for the click event -->
<h4>{{project.title}}</h4> <!-- Execute a different logic for this click
{{/each}}
I initially used the action helper and added a relevant method inside the actions hash of the ProjectsController. However, the action is being executed on all the divs in the list instead of just the clicked one.
Previous versions of Ember used to provide a context argument for the action but it is no longer the case. It turns out the recommended way to handle DOM events is within in the view like below.
App.ProjectsView = Ember.View.extend({
templateName: 'projects',
click: function(event) {
// Do something with the event
}
})
However, it doesn't feel right to write a bunch of if's inside the click handler to find if the click event was triggered on an image or a h4 and act accordingly. Is this is the right way of handling events or am I doing it wrong?

You can instead use App.ProjectsView as a CollectionView with each project as it's child view.
App.ProjectsView = Ember.View.extend({
templateName: 'projects',
itemViewClass: 'App.ProjectView'
})
You can handle actions in your child views directly using the target=view option in action helper.
Since you want to do DOM related tasks (expand/collapse), handling the actions in the views will help as you can get a jQuery object of the view using this.$().

Related

How to bind events to html generated by templating/data binding engines (No Jquery)

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);

Can I respond to a menu item housed in a List Item via its click event?

To set a Session variable in response to the selection of a menu item, should a list item click event replace an anchor tag?
In my Meteor app, I originally intended to use iron router routing in response to menu item selections, like so:
<li>Open Existing</li>
...but now am thinking I would rather use dynamic templates, and use the value of a Session variable to replace the Template in the body (SPA-style). So is this the way to go about that:
html:
<li name="mniOpenExisting" id="mniOpenExisting">Open Existing</li>
Javascript:
"click mniOpenExisting": function (event) {
return 'openExistingTemplate';
// In actuality, I will use:
//Session.set('curTemplate', 'openExistingTemplate');
}
?
IOW, I can just do away with the anchor tag and respond to the click of the menu items?
Yes that is correct, the <a> is not required.
"click #mniOpenExisting": function (event) {
// take action here when element with id 'mniOpenExisting' is clicked
}
I like the coding style set out here, with the separation between ids and classes used for presentation and the data- tags for event handlers.
Iron:router is still 'SPA-style', however the additional functionality it has already built in may save you time and keep your code cleaner that reimplementing this functionality yourself, though that depends on how complex your app will be.
Also see Meteor UI Pattern: Keeping App State on the URL. This is generally a much better option than having Session variables for the same task.

Creating dialog with knockoutjs components

I already use custom binding with knockout for displaying jqueryui dialog, but I would like to use knockout component feature.
So, I would like to write something like:
<window params="isVisible: isVisible">
//there will be some html
</window>
And later somewhere in code:
self.isVisible(true); //That would open window
//or
self.isVisible(false); //That would close window
Problem is that I don't know how to apply $(element).dialog. When I register knockout component, I can only get container element of this component, but not injected element.
ko.components.register('window', {
viewModel: {
createViewModel: function(params, componentInfo) {
// - 'params' is an object whose key/value pairs are the parameters
// passed from the component binding or custom element
// - 'componentInfo.element' is the element the component is being
// injected into. When createViewModel is called, the template has
// already been injected into this element, but isn't yet bound.
// - 'componentInfo.templateNodes' is an array containing any DOM
// nodes that have been supplied to the component. See below.
// Return the desired view model instance, e.g.:
return new MyViewModel(params);
}
},
template: ...
});
So, componentInfo.element is parent node, and if I apply dialog with $(componentInfo.element) I will set as dialog parent node, then my window tag would be:
<div><!-- Dialog will be applyed there-->
<window params="isVisible: isVisible">
//there will be some html
</window>
</div>
I think it will work, but that extra div look unneeded here... Or this is the only way to do job done?
What is knockout.components way to do this? Thanks.
I had a similar requirement when I wanted to build a Knockout component for hosting a dialog window using Bootstrap v2 'modal' functionality. I used an observable boolean value in my page's viewModel, set to false initially. There isn't a simple way to communicate with components after they have been initialized, except by passing in observables via params.
This observable was passed as a param to a <dialog-window> component in the params, e.g.
<dialog-window params="visible: showDialog">[content]</dialog-window>
This then used a special binding handler for the 'modal' dialog, but in your case you could bind it direcly using a data-bind='visible: YourParam' attribute.
The main page could then simply call showDialog(true) to make the dialog visible.
Hope this helps.

How to run js function on element that appears dynamically with Ember?

I'm working on an Ember project and I have a button that inserts some HTML tags into the DOM. I want to call a javascript function to run upon loading of those new DOM elements. Here's my code:
<script type="text/x-handlebars" id="doc">
{{#if isEditing}}
{{partial 'doc/editbox'}}
{{/if}}
...
</script>
The partial doc/editbox simply contains some html tags.
I've tried running the javascript as part of the click button event that initiate the insertion, but the js doesn't work because the DOM elements don't exist yet. I saw this post:
How to Run Some JS on an Element in the Index Template in Ember.js RC2
which suggested using Ember.run.next, however that didn't work since the View 'Doc' originally appears without those additional DOM elements (until button is clicked). How do I get the javascript function to run at the correct time?
add an observes in the controller
watchEditing: function(){
if (this.get('isEditing')){
Ember.run.scheduleOnce('afterRender', function(){
//do it here
});
}
}.observes('isEditing')
additionally you could use render instead of partial and create an associated view, and run it in the didInsertElement of that view.

How to bind a Backbone.View to a 'single' DOM element in a list of similar elements in the DOM

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.

Categories