I've been diving into the scary stuff recently.. :) scary stuff being the source of popular js frameworks like backbone.js, angular.js, vue.js and so on.
I'll take Backbone as an example here. I am trying to figure out how is a model attached to the view?
Here is the code and if someone could just point out the part where this is happening, would be awesome!
https://github.com/jashkenas/backbone/blob/master/backbone.js
Actually, the part I don't understand is that there is not innerHTML called anywhere, so how is the element being populated with the data?
Backbone is not Angular, it doesn't bind model to html for you, also it does not actually renders views for you, you have to implement render methods in your views and call them when you find appropriate. In fact, I think it might be confusing to developer coming from 2-way binding frameworks. Backbone is all about giving all the control to the developer, but you have to do all the work yourself.
Minimal model => view flow example would be something like
var MyModel = Backbone.Model.extend({
defaults: {
test: 'test',
},
initialize: function (options) {
console.log(this.get('test'));
},
});
var MyView = Backbone.View.extend({
el: '#your-el',
initialize: function (options) {
this.template = _.template('<div><%= test %></div>');
this.listenTo(this.model, 'change', this.render);
},
render: function () {
// rendering happens here, innerHTML, $.html(), whichever you prefer
// and you can pass model data as this.model.toJSON()
// or you can pass an object with data
// also, you probably will need a templating library or use
// bundled underscore _.template(html, data) method to render blocks of html
// for example, with underscore
this.$el.html(this.template(this.model.toJSON()));
return this; // for chaining
},
});
var myModel = new MyModel();
var myView = new MyView({
model: myModel,
});
myModel.set('test', 'newValue'); // view should render after this call
Check the list of built-in events at backbonejs.org.
Related
The goal
Call UserModel within AuthenticationView with Backbone + Sprockets.
The problem
I just don't know a good way to do that.
The scenario
This is my view (assets/js/views/AuthenticationView.js):
var AuthenticationView = Backbone.View.extend({
el: $('.authentication-form'),
events: {
'keyup input[name=email]' : 'validationScope',
'keyup input[name=password]' : 'validationScope',
'submit form[data-remote=true]' : 'authenticate'
},
render: function() {
},
authenticate: function() {
// Here I'm going interact with the model.
}
});
And that's my model (assets/js/models/UserModel.js):
var UserModel = Backbone.Model.extend({
url: '/sessions'
});
The question
How can I make the interaction between the view and the model?
Remember: they're in separated files.
Getting the constructors together would be step 1 -- separate files don't matter, you can use Browserify/requirejs or just throw these things in global scope. From there, since passing an object into a view constructor with the property name 'model', automatically assigns the value to the view's this.model. So if we have an initialize method in our view, we can see:
initialize: function (options) {
console.log(this.model); // User instance
this.model.on('update', function () {});
}
And so we can pass in an instantiated model into the view via an object's model property:
var model = new UserModel();
var view = new AuthenticationView({ model: model });
http://www.joezimjs.com/javascript/lazy-loading-javascript-with-requirejs/ here is great article about js modules lazy loading with examples used backbonejs and requirejs
I am working on a single-page app using Backbone.js. An issue that has occurred to me is that since one is not reloading the page, that when one creates a instance of a View, then I assume, that the View object will remain in memory for the life of the app. This does not seem very efficient to me, since a particular view may no longer be needed if another route is called. However, a particular View may later need to be 'displayed' if one returns to that original route. So the question is, how to best manage views in Backbone with regards to routes?
In my app, many of the views are responsible for displaying a particular 'page' and as such share the same DOM element. When one of these 'page' views is called, it will replace the content in the DOM element previously put in place by the previous view. Thus the previous view is no longer needed.
Do I need to somehow manually destroy the previous View (or is this somehow handled by the Router object)? Or is it better to leave the views once they have been initialized?
Following sample code shows how views instances are being creating in the Router in the app.
/**
* View - List of contacts
*/
var ListContactsView = Backbone.View.extend({
el: '#content',
template: _.template($('#list-contacts-tpl').html()),
initialize: function() {
_.bindAll(this, 'render');
this.collection = new Contacts();
this.collection.bind('reset', this.render);
this.collection.fetch();
},
render: function() {
this.$el.hide();
this.$el.html(this.template({ contacts: this.collection }));
this.$el.fadeIn(500);
}
});
/**
* View - Display single contact
*/
var DisplayContactView = Backbone.View.extend({
el: '#content',
events: {
'click #delete-contact-button': 'deleteContact'
},
template: _.template($('#display-contact-tpl').html()),
initialize: function() {
_.bindAll(this, 'deleteContact', 'render');
// Create reference to event aggregator object.
if (typeof this.options.id === 'undefined') {
throw new Error('View DisplayContactView initialized without _id parameter.');
}
this.model = new Contact({ _id: this.options.id });
// Add parse method since parsing is not done by collection in this
// instance, as this model is not called in the scope of collection
// Contacts.
this.model.parse = function(response) {
return response.data;
};
this.model.bind('change', this.render);
this.model.fetch();
},
deleteContact: function(id) {
// Trigger deleteContact event.
this.eventAggregator.trigger('deleteContact', id);
},
render: function() {
this.$el.html(this.template({ contact: this.model.attributes }));
}
});
/**
* Page routes
*/
var $content = $('#content');
var ClientSideRouter = Backbone.Router.extend({
routes: {
'browse': 'browse',
'browse/view/:id': 'browseViewContact',
'orgs': 'orgs',
'orgs/:orgName': 'orgs',
'orgs/:orgName/:id': 'orgs',
'contact/add': 'addContact',
'contact/view/:id': 'viewContact',
'contact/delete/:id': 'confirmDelete',
'*path': 'defaultPage'
},
addContact: function() {
// Display contact edit form.
var editContactFormView = new EditContactFormView();
// Display email field in edit form.
},
browse: function() {
var listContactsView = new ListContactsView();
},
browseViewContact: function(id) {
var displayContactView = new DisplayContactView({ id: id });
},
defaultPage: function(path) {
$content.html('Default');
},
home: function() {
$content.html('Home');
},
viewContact: function(id) {
$.ajax({
url: '/contact/view/' + id,
dataType: 'html',
success: function(data) {
$content.html(data);
}
});
}
});
var clientSideRouter = new ClientSideRouter();
Backbone.history.start();
Routes do not destroy views
Routes provide you convenient manner to interact with url changes. By convenience i mean url semantics and context of current page. For example url #/!/create/ will invoke a method that should display a form to create a model. Context here is the view to create model.
Views should be managed by the developer
there still does not exists a well known manner to manage views in Backbone.js, but i prefer the way of global variables. This would ensure your view instances are available throughout application and all the modules have access to them. For example doing this
window.App.Contacts.ContactView = new App.Contacts.View.ContactView({model:BenContact}); will make view used to display Ben's contact information available to application modules through window object. All you need to do for any views that use same el is to destroy the ContactView and render the new view.
You have methods on view to remove them
Undelegate Events and Remove methods help you remove them. Inside the callback method that handles routes hash change events. For example in the callback method that handles #/!/view/all ( url to view all the contacts list) you might come across situation where both the views now use the same el so you should destroy the ContactView and render ListView so in the callback do this
App.Contacts.ContactView.undelegateEvents();
App.Contacts.ContactView.remove();
Since Backbone.js has no built in support for view compositions, there are several patterns that you could follow when it comes to keeping track of child views.
Derick Bailey illustrates extending Backbone.View to allow views to
clean up after themselves -
http://lostechies.com/derickbailey/2011/09/15/zombies-run-managing-page-transitions-in-backbone-apps/
Another alternative is to add on child views to a property of the
parent view and manually clean them up when the parent view state is
removed.
var ParentView = Backbone.View.extend({
initialize : function(){
this.childViews = [];
},
render: function(){
this.childViews.push(new ChildView);
}
});
A third alternative is to make the child views subscribe to events
that the parent views trigger, so that they can clean up when the
parent view publishes a "close" event.
Also I noticed from your code that you are actually fetching a model within your child view class. Ideally, I would suggest passing the model as a parameter to the constructor as this decouples the view from the data. It's more MVC-ish
I'm doing an app that basically works like the facebook wall.
Essentially Posts and Comments.
It's working, but in order to render the CommentView, I'm using code similar to this in my Post Template
<div class="wall-post">
<div class="wall-post-content">${PostContent}</div>
<div class="wall-post-comments" id="wall-post-comments-${PostId}"></div>
</div>
Then I use the id of the comment area for that post like this.
var comment_view = new PostCommentView({ model: post.get("Comments") });
this.$('#wall-post-comments-'+ post.get("PostId")).append($(comment_view.render()));
This works, but something tells me I shouldn't be manually binding against my own ID. I feel I should be doing something clever with this.el ?
Can anyone point me in the right direction.
I'm using BackBone Relational to manage the relationships.
//EDIT
As requested some more of the implementation
//Some functions relating to click evens and functionality removed, as I don't think they relate to my question.
PostModel = Backbone.RelationalModel.extend({
urlRoot: '/api/post',
idAttribute: 'PostId',
relations: [{
type: Backbone.HasMany,
key: 'Comments',
relatedModel: 'CommentModel',
reverseRelation: {
key: 'Post',
includeInJSON: 'PostId'
}
}]
});
CommentModel = Backbone.RelationalModel.extend({
urlRoot: '/api/comment',
idAttribute: 'PostId'
});
PostCollection = Backbone.Collection.extend({
url: '/api/post',
model: PostModel
});
PostListView = Backbone.View.extend({
tagName: 'div',
className: 'PostListView',
initialize: function(){
_.bindAll(this, 'render', 'render_thread_summary', 'on_submit', 'on_thread_created', 'on_error');
this.model.bind('reset', this.render);
this.model.bind('change', this.render);
this.model.bind('add', this.render_thread_summary);
},
template: $('#wall-post-template').html(),
render: function() {
$(this.el).html($(this.template).tmpl(this.model.toJSON()));
this.model.forEach(this.render_thread_summary);
return $(this.el).html();
},
render_thread_summary: function(post) {
var comment_view = new PostCommentView({ model: post.get("Comments") });
this.$('#wall-post-comments-'+ post.get("PostId")).append($(comment_view.render()));
}
});
PostCommentView = Backbone.View.extend({
initialize: function(){
_.bindAll(this, 'render', 'on_click');
this.model.bind('change', this.render);
},
template: $('#wall-comments-template').html(),
render: function() {
var html = $(this.el).html($(this.template).tmpl(this.model.toJSON()));
return html;
}
});
I've just started digging into Backbone (and haven't done anything with Backbone Relational yet), so with that in mind here are my 2 cents:
Backbone defines id's for its Models, so no need to define your own id attr. If you inspect into a model instance, you'll see its id even if it's not specifically defined in your impl.
It seems to me that you're missing a comments collection, made up by single comment models. Then attach model events within your views appropriately. That way you wouldn't have to manage your comment view rendering manually (it's all done by Backbone based on event triggers).
If you haven't looked into the BB sample Todos app, I'd suggest giving it a look - that should help you design your comment(s) model and view better.
todos.js
todos app - Use Fire/ChromeBug to inspect the code
Hope this helps.
I was greatly underusing the this.el reference. There is no need to generally use id's for most things on the page, as within a view you can just reference $(this.el) and then reference from that part of the page. $(".className", this.el) will select any class's with in the item on the page. el is essentially a reference to the area on the page where the view was rendered to. It's really clean once you get the hang of it.
I got this code:
(function($){
var ListView = Backbone.View.extend({
el: $('body'), // el attaches to existing element
events: Where DOM events are bound to View methods. Backbone doesn't have a separate controller to handle such bindings; it all happens in a View.
events: {
'click button#add': 'addItem'
},
initialize: function(){
_.bindAll(this, 'render', 'addItem'); // every function that uses 'this' as the current object should be in here
this.counter = 0; // total number of items added thus far
this.render();
},
render() now introduces a button to add a new list item.
render: function(){
$(this.el).append("<button id='add'>Add list item</button>");
$(this.el).append("<ul></ul>");
},
addItem(): Custom function called via click event above.
addItem: function(){
this.counter++;
$('ul', this.el).append("<li>hello world"+this.counter+"</li>");
}
});
var listView = new ListView();
})(jQuery);
from this tutorial.
I understand that Backbone.js introduces a MVC pattern to the front end.
But in the code above I can't see that.
Can anyone explain that to me?
There is technically no controller in backbone.js. The main structures are Models, Views, Collections (that act as arrays and contain lots of models), and Routers.
The link you listed - http://arturadib.com/hello-backbonejs/ - is probably the best way to learn Backbone.js - especially with little background in Javascript. So you are on the right track. That project is a direct lead-in to the backbone.js ToDo list tutorial: http://documentcloud.github.com/backbone/docs/todos.html
This site will also explain things at a more basic level - I found it very helpful: http://backbonetutorials.com/
That's just the view part code. See other .js files in the same tutorial. Better check out all the files from 1.js to 5.js
Better check it from first: Hello Backbone
Note that Backbone View isn't the one you expected in MVC its more like a controller or the presenter in MVP. Here is a nice article that describes this differences.
So i'm very new to backbone.js and not so good at JavaScript in general, so I was wondering if someone could explain to me why
I cannot define my EL property, and Template property in my view, and then use this.template in my render. Instead I have to define the template and el in my render function.
var ProductView = Backbone.View.extend({
el: $('#product-list'),
initialize: function() {
this.el.html('<span style="color:white">loading...</span>');
}, // end initialize
render: function(collection) {
// // assign the template
this.template = $('#product_template');
// Where the template will be placed
this.el = $('#product-list');
// Add the collection to the main object
this.collection = collection;
// add tthe data to the html variable
var html = this.template.tmpl(this.collection.toJSON());
// place the html in the element.
this.el.html(html);
// not even sure what the hell this is.
return this;
} // end render
});
The problem isn't in the way you're defining el or template, it's in how you're setting the call back. In Workspace, your router, you're setting the callback for your collection refresh event like this:
// Bind the view and collection
// So when the collection is reset, the view executes the render method
Products.bind("reset", this.view.render);
The problem is, you're setting a method as a callback, but you're not providing a context object as the third argument to bind - so the method is called, but this in the method refers to the global object, not the view. So this.el is undefined, because it's not looking at the view instance at all. Try:
// Bind the view and collection
// So when the collection is reset, the view executes the render method
Products.bind("reset", this.view.render, this.view);
and see how that goes.
(I made a jsFiddle to demonstrate that the el and template were set properly under normal circumstances, though it doesn't actually include the fix above, which is hard to mock up without the server-side data: http://jsfiddle.net/nrabinowitz/QjgS9/)
You can't do this:
var ProductView = Backbone.View.extend({
el: $('#product-list'),
// ...
and get anything useful in el as #product-list probably isn't even present in the DOM when your ProductView is built; so trying to use $('#product-list') for el is simply the classic "I forgot to use $(document).ready()" problem dressed up in Backbone. Using $('#product-list') for el should work if #product-list is around when you define your ProductView though.
You can do this though:
var ProductView = Backbone.View.extend({
el: '#product-list',
// ...
and then say $(this.el) when you need to do things inside your view methods. Not only is $(this.el) the usual way of using el but it also works and that's sort of important.
The same issues apply to #product_template.
Looking at your code I see this:
// INstantiate the view
this.view = new ProductView();
// Bind the view and collection
// So when the collection is reset, the view executes the render method
Products.bind("reset", this.view.render);
Presumably the render is being triggered by the reset event. But, and this is a big but, the render method isn't bound to the right this anywhere so this won't be the ProductView when render is called and this won't have anything that you expected it to; hence your bizarre "undefined" error.
You could use _.bindAll in your initialize:
initialize: function() {
_.bindAll(this, 'render');
// ...
but usually you'd want to give the view a collection when you create it and the view would bind itself to the events so your structure will still be a bit odd.
You can also supply a context (AKA this) when you call bind:
collection.bind('reset', this.render, this);