How do you dynamically create Backbone view elements? - javascript

I'd like to create some view elements in a Backbone js application dynamically. When a new view is initialized, I want it to insert the new element into the DOM, store the reference to the element in view.el, and delegate events as usual.
I understand that I can put in my html, and then setup a view with el: "#test" but this seems like overkill for modals and other views that aren't central to the web application. Is there a prescribed way to do this I'm missing in the docs? Am I just misunderstanding how views are supposed to work?

A backbone view will generate an el for you, without you having to do anything. By default, it creates a <div>. You can generate any tag name you want, though. Once you have the view instantiated, implement a render method on the view, and populate the el with your HTML.
MyView = Backbone.View.extend({});
var v = new MyView();
console.log(v.el); // => "<div></div>"
// define your own tag, and render contents for it
MyTagView = Backbone.View.extend({
tagName: "ul",
render: function(){
this.$el.html("<li>test</li>");
}
});
var v2 = new MyTagView();
v2.render();
console.log(v2.el); // => "<ul><li>test</li></ul>"
It's common practice to use a template system to render your view's HTML, like Underscore.js template, Handlebars, or any of the other dozen or more template JavaScript template engines.
Once you have the content generated from the view, you need to stick it in to the DOM somewhere before it will be visible. This is usually done with jQuery or another plugin:
$("#some-element").html(v2.el);

Related

What are el and $el in backbone's view?

I'm trying to avoid wrapping with empty div when render view in backbone.
I do it with the following code
this.$el.replaceWith(this.template(this.model.attributes));
return this;
but I get empty div when I append this view by
$("#product-pannel").append(productsView.render().el);
someone give the solution like this
render: function(){
var html = this.template(this.model.toJSON()));
var newElement = $(html)
this.$el.replaceWith(newElement);
this.setElement(newElement);
return this;
}
but I can't understand why should I do this so complicatedly above
can someone tell me the mystery of el an $el?
el points to the the view element (the one that holds rest of template) and $el is a jQuery object represeting el element So that you don't have to do $(this.el) all the time.
This is clearly mentioned in the documentation.
You can either specify an existing DOM element as the view element using el option, or backbone creates a DOM element for every view. By default this will be a <div>. If you don't want an empty <div>, customize the element backbone creates as the top level element of your view template using options like tagName, attributes etc.
setElement is for dynamically changing the view element to something else... I've rarely (or never) seen it actually being used.

Backbone.js - render a new element to the page

Say you create a new element in your View - i.e. you don't target an existing element on the page with el.
var StudentView = Backbone.View.extend({
tagName: 'article',
className: 'student-name',
template: _.template($('#name-tpl').html()),
render: function(){
var student_tpl = this.template({name: 'Rikard'});
this.$el.html(student_tpl);
}
});
You then instantiate the View and call its render method:
var student_view = new StudentView();
student_view.render();
Your HTML contains the following template:
<div class="target">
<script id="name-tpl" type="text/template">
<%= name %>
</script>
</div>
This does not print the newly created element to the page. If we set el to .target, then it would work, but then it wouldn't print your tag name or class name that you set in the View.
Is there any way to print your newly created element to the page? I thought about using jQuery append, but it seems like there should be a more backbone-oriented solution.
Unless you use the el option to attach the view to an existing element in DOM, backbone.js creates an HTMLElement in memory, detached from DOM. It's your responsibility to attach it to where you want (backbone creates a new element for you, but it doesn't know where in DOM you want it to be added).
If you already have a Backbone.View and you want to set it to a different element, you can use setElement, but in this case as well, it is your responsibility to make sure that the element is part of DOM.
If backbone gave you a way to specify the position of an element in DOM, that'd look something like:
{
tagName: 'article',
elPosition: '#container > .content+div:nth-child(3)'
}
Again, there will be confusion like whether the element should be added before elPosition, or after etc. That looks ugly, no wonder why backbone leaves it to you rather than setting up more rules. In my opinion backbone sets less rules compared to other frameworks and give you more freedom (freedom can be a pain when you don't know what to do with it :)
What we normally do is, have the main parent view point to the containing element in DOM like <body> , using the el option.
Then append the child view's to it like this.$el.appendTo(parentView.$el);

DOM location of ItemView when passed in CompositeView - Marionette

I have an ItemView like this:
var Userview= Marionette.ItemView.extend({
template: "#user1",
el: "#imp1"
});
Question: Does this mean that template of ItemView: #user1 will go into the (say) div with id=imp1 in the DOM, when rendered?
(If not, then I guess I can use regions to render Userview in a certain div in the DOM, so its fine!)
Now, in case of rendering a CompositeView, I use Region1.show() for CompositeView. In this case, how can I render a (childView) ItemView of the CompositeView at a specific location (say a div with an id) in the DOM.
(As I am calling Region1.show() on the CompositeView and not on the ItemView, I dont know how to render the ItemView at a specific location on the DOM)
If the ItemView is the CompositeView child, then you cannot render that at a specific location in the DOM.
You can only render on a specific location inside the CompositeView(childViewContainer), and the CompositeView will render directly into DOM via a region.
So, if you want to render an ItemView at a specific location in the DOM, you should use only a Region and the ItemView, without compositeView.
var myView = new MyView();
myRegion.show(myView);
So, the trick is to define a childViewContainer: "#some_id" inside the CompositeView definition. It will define location of the (childView) itemView.
Also, using the word itemView inside CompositeView will cause an error, we need to use childView: child_view_name

backbone remove view deletes the el

I am creating a single page application, and I am quite new to backbone. I have a problem with creating multiple views which uses the same wrapper-div.
My setup:
I have added a close function to all views:
Backbone.View.prototype.close = function(){
this.remove();
this.off();
if (this.onClose){
this.onClose();
}
}
I have a wrapper-div where I want to render views, remove them and render new ones. So my SetupView looks like this:
app.SetupView = Backbone.View.extend({
el: '#my_view_wrapper',
...
});
From the function where I swap views I close the current (open) view like this:
var v = this.model.get('view');
v.close();
Question
My problem is that I have multiple view's using the same wrapper-div. But when I close a view, this wrapper-div seems to be removed, and the next view I try to create can't find this div.
I guess there is an easy solution? I want to reuse the same wrapper, and only remove the view inside it, not the wrapper itself.
Just as an addition (for later reference) : another option is to overwrite the subviews remove so that it just empties $el instead of removing it. Eg.
remove: function() {
this.$el.empty().off(); /* off to unbind the events */
this.stopListening();
return this;
}
Personally I prefer this, as it removes the need to insert wrapper elements that have no real use.
In your scenario don't use an existing DOM element as your "el" value. Backbone will create the element for you. When you instantiate your view you can do the following to attach it to your existing wrapping element.
$(viewName.render().el).appendTo('#my_view_wrapper');

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