full sample here
I have a very simple backbone js structure.
var Step1View = Backbone.View.extend({
el:'.page',
render:function () {
var template = _.template($('#step1-template').html());
this.$el.html(template);
}
});
var step1View = new Step1View();
var Router = Backbone.Router.extend({
routes:{
"":"home"
}
});
var router = new Router;
router.on('route:home', function () {
step1View.render();
})
Backbone.history.start();
This works well however i am unable to get this simple jquery function called.
$(document).ready(function() {
$('.tip').tooltip();
});
Update
School boy error here. Jquery onload functions need to be placed in the route. I'm very new to backbone so i'm not sure if this is best practice. But the following works.
render:function () {
var that = this;
var savings = new Savings();
savings.fetch({
success:function () {
var template = _.template($('#step3-template').html(), {savings:savings.models});
that.$el.html(template);
// put your jquery good ness here
$('.tip').tooltip();
$(".step3-form").validate();
}
})
}
Looks like you found your answer! Just wanted to also share that you could scope down your jQuery a bit by doing this instead.
savings.fetch({
success:function () {
var template = _.template($('#step3-template').html(), {savings:savings.models});
that.$el.html(template);
that.$el.find('.tip').tooltip();
that.$el.find(".step3-form").validate();
}
What you have in your example works but it's also scanning the whole document every time for HTML with the class tip where you could use the element you just created to scan downward only for the tip you just created inside it. Slight optimization.
Hope this is helpful!
Looks like you found your answer! Just wanted to also share that you could scope down your jQuery a bit by doing this instead.
savings.fetch({
success:function () {
var template = _.template($('#step3-template').html(), {savings:savings.models});
that.$el.html(template);
that.$el.find('.tip').tooltip();
that.$el.find(".step3-form").validate();
}
What you have in your example works but it's also scanning the whole document every time for HTML with the class tip where you could use the element you just created to scan downward only for the tip you just created inside it. Slight optimization.
Hope this is helpful!
Related
I'm evaluating using Casper.js to do functional/acceptance testing for my app. The biggest problem I've seen so far is that my app is an SPA that uses handlebars templates (which are compiled to JS) The pages of my app are nothing more than a shell with an empty div where the markup will be injected via JS.
I've messed around with Casper a little and tried using its waitFor functions. All I can seem to get from it are my main empty page before any of the markup is injected. I've tried waitForSelector but it just times out after 5 seconds. Should I try increasing the timeout? The page typically loads in a browser very quickly, so it seems like there may be another issue.
I'm using Yadda along with Casper for step definitions:
module.exports.init = function() {
var dictionary = new Dictionary()
.define('LOCALE', /(fr|es|ie)/)
.define('NUM', /(\d+)/);
var tiles;
function getTiles() {
return document.querySelectorAll('.m-product-tile');
}
function getFirstTile(collection) {
return Array.prototype.slice.call(collection)[0];
}
var library = English.library(dictionary)
.given('product tiles', function() {
casper.open('http://www.example.com/#/search?keywords=ipods&resultIndex=1&resultsPerPage=24');
casper.then(function() {
// casper.capture('test.png');
casper.echo(casper.getHTML());
casper.waitForSelector('.m-product-tile', function() {
tiles = getTiles();
});
});
})
.when('I tap a tile', function() {
casper.then(function() {
casper.echo(tiles); //nodelist
var tile = Array.prototype.slice.call(tiles)[0];
casper.echo(tile); //undefined!
var pid = tile.getAttribute('data-pid');
})
})
.then('I am taken to a product page', function() {
});
return library;
};
Any Angular, Backbone, Ember folks running into issues like this?
I am new to backbonejs. What I am trying to do is, render a template on page load and pass model as data parameter in _.template function. Here is my bacbone code:
var Trip = Backbone.Model.extend({
url: '/trips/' + trip_id + '/show'
});
var InviteTraveller = Backbone.View.extend({
el: '.page',
render: function () {
var that = this;
var trip = new Trip();
trip.fetch({
success: function(){
console.log(trip); //logs trip object correctly
var template = _.template($('#invite-traveller-template').html(), {trip: trip});
that.$el.html(template);
}
});
}
});
var Router = Backbone.Router.extend({
routes: {
'': 'fetchTrip'
}
});
var inviteTraveller = new InviteTraveller();
var router = new Router();
router.on('route:fetchTrip',function () {
inviteTraveller.render();
});
Backbone.history.start();
And here is my sample template:
<script type="text/template" id="invite-traveller-template">
<h3>Trip</h3>
<h3><%= trip.get('name') %></h3>
</script>
On running, I am getting the this in browser window and console shows:
trip is not defined
I am facing this issue since yesterday but could not figure out the solution yet. Not understanding what is going wrong, code also seems to be right. Any help would be greatly appreciated.
Update:
I removed
inviteTravellers.render();
from router.on() and then reloaded the page in browser. I still got same error which means that <script></script> (template) is being compiled before calling render() of InviteTraveller view. What can be the possible reason for this?
I had the same issue (underscore v1.8.2). My fix:
var template = _.template($('#invite-traveller-template').html());
var compiled = template({trip: trip});
that.$el.html(compiled);
You're passing the whole model to the template. Typically you would call model.toJSON and then pass its result to the template. Additionally using <%= in your template to render the attribute, which is meant for interpolating variables from that JSON object you're passing.
You can pass a whole model to the template and use <% ... %> to execute pure Javascript code and use print to get the attribute but it's probably overkill.
Have a look at this fiddle.
You code work perfectfly, here's it
I think that your problem came from another code, not the one you have posted, because there's no way for your view to render if you remove :
inviteTravellers.render();
Try to chaneg <h3><% trip.get('name'); %></h3> by <h3><%= trip.get('name') %></h3>
My code seems to be right but still my template was getting compiled on page load and I was getting trip is not defined error. I did not understand the reason of this behavior yet.
I solved this issue by using handlebarsjs instead of default underscore templates.
so i had the same famous problem of zombie views in my backbone app. I tried this to become a superhero :P
var Router=Backbone.Router.extend({
routes:{
"":"loadDashboard",
"home":"loadDashboard",
'post-leads':"loadPostLeads"
},
initialize:function(){
window.currentView=null;
},
loadPostLeads:function(){
require(['views/post-leads'],function(leads){
if(window.currentView!=null)
{window.currentView.remove();}
window.currentView=new leads();
window.currentView.render();
})
},
loadDashboard: function(){
require(['views/dashboard'],function(dashboard){
if(window.currentView!=null)
{window.currentView.remove();}
window.currentView=new dashboard();
window.currentView.render();
})
}
});
This doesn't work. I wanted something simple and don't want to use marionette or anything similar for that sake. Whats going wrong above? Is it a sensible approach?
In principle what you do should work, but there are some things that Backbone can't clean up, because it doesn't know of them.
First, you should make sure that you are using a recent version of Backbone (0.9.9 or newer). There have been some improvements to the event binding code, which makes it easier for the View.remove method to do all the necessary cleanup.
The common gotchas are:
Listening to model events:
//don't use other.on (Backbone doesn't know how to clean up)
this.model.on('event', this.method);
//use this.listenTo (Backbone cleans up events when View.remove is called)
//requires Backbone 0.9.9
this.listenTo(this.model, 'event', this.method);
Listening to DOM events outside your view's scope:
//if you listen to events for nodes that are outside View.el
$(document).on('event', this.method);
//you have to clean them up. A good way is to override the View.remove method
remove: function() {
$(document).off('event', this.method);
Backbone.View.prototype.remove.call(this);
}
Direct references:
//you may hold a direct reference to the view:
this.childView = otherView;
//or one of its methods
this.callback = otherView.render;
//or as a captured function scope variable:
this.on('event', function() {
otherView.render();
});
Closures:
//if you create a closure over your view, or any method of your view,
//someone else may still hold a reference to your view:
method: function(arg) {
var self = this;
return function() {
self.something(x);
}
}
Avoiding the following pitfalls should help your views to get cleaned up correctly.
Edit based on comment:
Ah, you didn't mention the full problem in your question. The problem with your approach is, as I gather, is that you're trying to render the two views into the same element:
var View1 = Backbone.View.extend({el:"#container" });
var View2 = Backbone.View.extend({el:"#container" });
And when you remove View1, the View2 does not correctly render.
Instead of specifying the view el, you should render the views into an element. On your page you should have a #container element, and append the view's element into the container.
loadPostLeads: function () {
var self = this;
require(['views/post-leads'], function (leads) {
self.renderView(new leads());
})
},
loadDashboard: function () {
var self = this;
require(['views/dashboard'], function (dashboard) {
self.renderView(new dashboard());
})
},
renderView: function(view) {
if(window.currentView) {
window.currentView.remove();
}
//the view itself does not specify el, so you need to append the view into the DOM
view.render();
$("#container").html(view.el);
window.currentView = view;
}
My rendered method is being called before _initializeLayout:
var _initializeLayout = function() {
console.log('initializeLayout...');
Controller.layout = new Layout();
Controller.layout.on("show", function() {
vent.trigger("layout:rendered");
});
vent.trigger('app:show', Controller.layout);
};
I use the layout in the on rendered method:
// controller attach a sub view/ search View
vent.on("layout:rendered", function() {
console.log('layout:rendered =>StartController');
// render views for the existing HTML in the template, and attach it to the layout (i.e. don't double render)
var inspectorStartView = new InspectorStartView();
Controller.layout.inspector.attachView(inspectorStartView);
var playerStartView = new PlayerStartView();
Controller.layout.player.attachView(playerStartView);
});
When I try it, my on rendered callback is called before _initializeLayout(). I have it calling _initializeLayout in the router/controller method:
Controller.go_inspector_control_center = function(term) {
_initializeLayout();
//vent.trigger("search:term", term);
};
I just ran it again and found that an event was being triggered from a different controller's _initializeLayout() method:
// private
var _initializeLayout = function() {
console.log('initialize Start Layout...');
Controller.layout = new Layout();
Controller.layout.on("show", function() {
**vent.trigger("layout:rendered"); // <--**
});
vent.trigger('app:show', Controller.layout);
};
It appears that the events need to have unique names. I'll try that. If anyone knows please chime in.
Andrew
You just can't take us Java guys anywhere. The answer (in Javascript) is that I do need to name my events uniquely. Makes sense when you think about it.
Although most event systems I've worked with X11/Java you don't name your event you use what they give you.
Here is a stackoverflow question on event naming.
I hope my public learning is helping others. ;-)
I've noticed that some web sites offer Ajax-ian search that refreshes the URL and displays the GET params used, for example:
someapp.com/search/Tokyo?price_min=80&price_max=300
As a result of an Ajax GET request.
I want to know how can I accomplish this by using Backbone.js, I understand that by using backbone's push state this may be possible, am I right?
How could I define a route like that (let's say the same case, scoped to /search) for a Place model for example?
Where would I do this? in a Router or in a Model?
I appreciate all the answers regarding this topic. And I apologize in advance for not providing any code, I usually do, but this exercise will be a proof of concept I'd like to make, and I hope backbone is the right tool for the job.
Thank you!
This is a working example of the solution - jsfiddle.net/avrelian/dGr8Y/, except that jsFiddle does not allow Backbone.history.navigate method to function properly.
Suppose, we have a button
<input class="fetch-button" type="button" value="Fetch" />
and a handler
$('.fetch-button').click(function() {
Backbone.history.navigate('posts/?author=martin', true);
});
This is our collection of posts
var Posts = Backbone.Collection.extend({
url: 'api/posts'
});
This is our Router with a custom parameter extractor
var Router = Backbone.Router.extend({
routes: {
'posts/?:filters': 'filterPosts'
},
filterPosts: function(filters){
posts.fetch({data: $.param(filters)});
},
_extractParameters: function(route, fragment) {
var result = route.exec(fragment).slice(1);
result.unshift(deparam(result[result.length-1]));
return result.slice(0,-1);
}
});
It is simplified $.deparam analog. You could use your own instead.
var deparam = function(paramString){
var result = {};
if( ! paramString){
return result;
}
$.each(paramString.split('&'), function(index, value){
if(value){
var param = value.split('=');
result[param[0]] = param[1];
}
});
return result;
};
And finally, instantiation of posts collection and router object
var posts = new Posts;
var router = new Router;
Backbone.history.start();
When a user clicks on the button address bar changes to myapp.com/s/#posts?author=martin. Please, note the sign #. We use a hash query string. Of course, you can use push state, but it is not widespread yet.