So I have two views here, their structure relies on $(window).height(). So when I re-size the browser or mess with the console, I need the height to be calculated. The below works for me, but I haven't seen any one mention doing it like this.
So I want to know if this code is awful or okay or great. Obviously all I am doing is telling my program when to instantiate my views.
// Play
$(document).ready(function() {
intro = new App.Views.Intro();
flight = new App.Views.Flight();
});
$(window).on('resize', function() {
intro = new App.Views.Intro();
flight = new App.Views.Flight();
});
Not sure if this fits the guidelines, but I do not want this to come back to hurt me later on.
Edit: Not sure why I didn't think of this, but below seems pretty practical. The reason I didn't want to re-render inside the view is because it seems I would have to add a remove() method and "bind" the resize. Not very pretty and organized if you ask me. example How do I add a resize event to the window in a view using Backbone?
// Play
$(document).ready(function() {
intro = new App.Views.Intro();
flight = new App.Views.Flight();
});
$(window).on('resize', function() {
intro.render();
flight.render();
});
You can create a base view class to encapsulate this behavior. This can be useful for a few reasons. One benefit is you can separate the resize behavior from the render code. Sometimes render does alot more work, such as serializing a model and rewriting large parts of the dom, which you don't want to re-run.
Here is a pen demonstrating a pattern I've used so that after the initial render only the width/height are adjusted and you don't need to re-render any of the views simply to resize them. The code is copied below for convenience.
This is just a sample. In a real app where I might be resizing a few views I typically refactor the window.resize event out so I only wire up one listener.
var ResizeView = Backbone.View.extend({
template: '#view-tpl',
initialize: function() {
$(window).on('resize.resizeview', this.onResize.bind(this));
},
remove: function() {
$(window).off('resize.resizeview');
Backbone.View.prototype.remove.call(this);
},
render: function () {
var $tmpl = $(this.template);
var tmpl = _.template($tmpl.html());
this.$el.append(tmpl());
this.onResize();
return this;
},
onResize: function () {
var w = $(window).width()
, h = $(window).height();
console.log('resize', w, h);
this.resize(w, h);
},
resize: function (w, h) {
this.$el.css({
'width': w,
'height': h
});
}
});
var view = new ResizeView();
$('body').append(view.render().$el);
Related
I am working with Backbone and before I worked with Jquery, before I used HTML Elements attributes as a variable to set data and when I need that information that was accessible in that HTML element.
For example: I had a image gallery and as you know each Image should have a title, Alt, src these are originally from HTML but I added a new attribute what called mk-author, mk was a prefix and when user clicked on that a image, The image maximized and the name of author will show beside of Image. sometimes I get the ID and do something maybe on server-side and of curse I validate it from server-side.
Questions:
1-Is this a secure, good way?
2-What is the betterway?
First of all, adding your own attributes gives you something that isn't quite HTML; it may work just fine but you're better off sticking to the standards (for future-proofing, portability, ...). Also, there's no need for them when the standard offers data-* attributes that everything will understand, jQuery even has special support for them through the data function.
For your image gallery example, a more idiomatic Backbone approach would have:
A model for each image in the gallery:
var Image = Backbone.Model.extend({
//...
});
Instances of this model would hold the image's URL, size, name, author, ...
A collection to hold the images:
var Images = Backbone.Collection.extend({
model: Image,
//...
});
A view for each image, this would provide the interaction and display for one Image model:
var ImageView = Backbone.View.extend({
events: {
'click': 'show_details',
},
render: function() {
// Build the HTML for `this.model` (an `Image`)
// and add it to `this.$el`...
return this;
},
show_details: function() {
// The clicked image model will be `this.model`
// so do whatever you need to.
}
});
A view to manage the entire collection, this mostly creates ImageViews:
var ImagesView = Backbone.View.extend({
render: function() {
this.collection.each(function(image) {
var v = new ImageView({ model: image });
this.$el.append(v.render().el);
}, this);
return this;
}
});
Then you'd create your collection, populate it, and say something like:
var v = new ImagesView({ collection: images_collection });
$(container).append(v.render().el);
Demo: https://jsfiddle.net/ambiguous/6Lcwfh98/
A little time with a Backbone tutorial and the Backbone documentation would be a good idea right about now.
I am trying to implement endless scrolling with Backbonejs. My view initializes a collection and calls fetch fetch function.
My view
var app = app || {};
app.PostListView = Backbone.View.extend({
el: '#posts',
initialize: function( ) {
this.collection = new app.PostList();
this.collection.on("sync", this.render, this);
this.collection.fetch();
this.render();
},
render: function() {
/*render posts*/
}
});
In my page I added the following code. It checks if the the user at the bottom of the page. If yes then it checks if the view is initialized. If yes then call that view fetch function of the view's collection object.
var app = app || {};
$(function() {
var post_view;
$(window).scroll(function() {
if(($(window).scrollTop() + $(window).height() == getDocHeight()) && busy==0) {
if(!post_view){
post_view = new app.PostListView();
} else {
post_view.collection.fetch();
}
}
});
});
So far this code is working. I am not sure if this is the right approach or not?
It's not a bad option; it works, and Backbone is making that collection available for you. But there's a couple of other options to consider:
Move that collection.fetch into a method getMoreItems() inside your PostListView, and call it within your else block. That way you're encapsulating your logic inside the view. Your app is more modular that way, and you can make your scrolling smarter without updating the rest of your app.
Move the scroll listener inside your PostListView. I'd probably put this within your PostListView's initialize function. Again, this reduces dependencies between the various parts of your app - you don't have to remember "Whenever I create a PostListView, I must remember to update it on scroll." By setting that event listener within the PostListView itself, all you have to do is create it. Backbone's general philosophy is to have small, independent components that manage their own state; moving the event listener inside would fit with that.
From what I understand of the way Backbone.js is intended to be used, Views are supposed to be rendered in their own $el element, which is not necessarily attached to the page. If it is so, the higher level view they depend on usually takes care of inserting the $el in the page.
I am making this statement after having read the Todo sample application. In this case, the TodoView renders element in a default div element that is not attached to the page.
var TodoView = Backbone.View.extend({
// [...]
render: function() {
this.$el.html(this.template(this.model.toJSON()));
this.$el.toggleClass('done', this.model.get('done'));
this.input = this.$('.edit');
return this;
},
The AppView, upon a Todo creation, takes care of creating the TodoView and appending its $el to the page after rendering.
var AppView = Backbone.View.extend({
// [...]
addOne: function(todo) {
var view = new TodoView({model: todo});
this.$("#todo-list").append(view.render().$el);
},
My question is: If a view not attached to the page needs adjustments after being inserted (e.g. calculating its position in the viewport and performing DOM manipulation accordingly), where would you place the corresponding code?
Would you create a afterInsertion() method that the sublevel View should call after inserting, would you put the code at the same emplacement that where the insertion takes place (i.e. in the sublevel View) or would you change the way the view works to have it rendering directly in the page? I'm sure there are other solutions I can't think of right now. I would like to know what you consider being a best practice/optimized in the way Backbone should work, or if this question doesn't make sense to explain why.
I keep track of my sub-views. In a base view definition, create an add method:
var BaseView = Backbone.View.extend({
// It is really simplified... add the details you find necessary
add: function (name, viewDef, options) {
options = options || {};
if (!this.children)
this.children = {};
var view = new viewDef(options);
this.children[name] = view;
this.listenToOnce(view, 'ready', this.onSubViewReady);
view.render(options);
view.$el.appendTo(options.appendTo || this.$el);
}
});
With this, you can keep track of your subviews and make anything you want with them later.
If you feel like making things "automatic", you can trigger a ready event after your render method doing this:
var extend = BaseView.extend;
BaseView.extend = function (protoProps, staticProps) {
var child = extend.apply(this, arguments);
child.prototype.__render__ = protoProps['render'] || this.prototype.__render__ || function() {};
child.prototype.render = function () {
this.__render__.apply(this, arguments);
this.trigger('ready', this);
}
};
With this you can do a lot already.
Just remember that the DOM won't be drawn by the time that ready is triggered. So, if you are willling to do any calculations with the subview height or anything that needs the DOM to be drawn, use setTimeout(function () { ... }, 0) to put your code to the end of queue.
Hope I've helped.
I'm working on a Backbone app that renders several instances of the same view. Each of the views has a fairly large node tree and I can see that the user agent can't render the views instantaneously.
The issue I'm running into is that when my render callbacks fire, the height is coming through as 0 because the user agent's rendering engine hasn't actually finished rendering the entire view. If I set a timeout, the correct final height comes through:
var ChildView = window.Backbone.View.extend({
render: function() {
var template = require('templates/ChildTemplate');
this.$el.html(template());
this.afterRender();
},
afterRender: function() {
console.log(this.$el.outerHeight()); // 0
var _this = this;
setTimeout(function(){
console.log(_this.$el.outerHeight()); // 51 (or some non-zero integer)
}, 100);
setTimeout(function(){
console.log(_this.$el.outerHeight()); // 240, full correct height
}, 300);
}
});
How can I account for this rendering engine delay?
Architecture:
jquery 1.7.2
backbone 0.9.9
If the template contains images then i suggest you use the standard jquery load function
_this.$el.children('img').load(function(){
//the element should exist now
});
Otherwise im not sure what the problem is. Im fairly certain that Underscore's template function is synchronous so i wouldnt have suspected any problems there. One thing that might work, and probably makes more sense from a design perspective, would be to make use of initialize
var ChildView = window.Backbone.View.extend({
initialize: function(){
var template = require('templates/ChildTemplate');
this.renderedTemplate = template();
},
render: function() {
this.$el.html(this.renderedTemplate);
this.afterRender();
},
});
If you dont want to use the name renderedTemplate then thats fine, just make sure not to name it template as that is already in use
I'm working on a application with Backbone.js.
When I'm switching of page, the global view of the current page is replaced with the view of the new page. Inside that view, I have a typical "render" function which replace the body html by the new template.
My problem is, after changing the html, so the DOM, how can I execute a function when the DOM is ready again ?
I have to use it because I need to scroll to an element and just executing my scrollTo() function after the render() don't work ($(myElement).offset().top always return me 0).
Here is my code :
module.exports = BaseView.extend({
initialize: function () {
this.render();
$('html, body').scrollTop(this.$("#toScroll").offset().top);
},
template: require('./templates/home'),
render: function () {
this.$el.html(this.template());
return this;
}
});
Thanks.
If your view's el is in the DOM and your #toScroll is inside the view's el, then #toScroll will be in the DOM as soon as you say this:
this.$el.html(this.template());
However, the HTML won't be rendered until after the browser gets control again (i.e. after your view has done all its work). The elements inside your el won't have any size or position information until the browser has rendered all of it (unless of course you're positioning and sizing everything by hand). The sequence of events goes like this:
v = new View.
initialize is called and that calls render.
render sets up everything in the DOM.
The view calls this.$('#toScroll').offset() and gets zeros.
The view tries to scrollTop to zero.
The browser gets control back.
The browser renders everything, this process computes the positions and offsets you want.
You just need to get 4 and 5 to happen after 7.
An easy way to do that is by putting your position/size dependent things inside a _.defer callback:
initialize: function () {
this.render();
var _this = this;
_(function() {
$('html, body').scrollTop(_this.$("#toScroll").offset().top);
}).defer();
}
You can also use a setTimeout with a delay of zero but _.defer makes your intent clearer.
Demo: http://jsfiddle.net/ambiguous/tWSBE/