I'm brand new to backbone and just learning the basics. I am building an image gallery with backbone. I am displaying a large version of an image. The routing is working properly. When a url is passed with an id the appropriate JSON is loaded into the model and the html is injected into the dom. Everything displays as expected.
However, I tried entering a url for the JSON for an image that didn't exist and noticed that the view still rendered but with the previously rendered view's properties (image url) still present. How do I ensure that the view is refreshed - all empty properties? Or is it the model that needs to be refreshed?
Note: I am re-using the view to avoid the overhead of creating and dystroying the view itself.
Here is the view in question:
var ImageView = Backbone.View.extend({
template: Handlebars.compile(
'<div class="galleryImageSingle">'+
'<h2>{{title}}</h2>' +
'<img id="image" src="{{imageUrl}}" class="img-polaroid" />' +
'<div class="fb-share share-btn small"><img src="img/fb-share-btn- small.png"/></div>'+
'</div>' +
'<div class="black-overlay"></div>'
),
initialize: function () {
this.listenTo(this.model, "change", this.render);
//this.model.on('change',this.render,this);
},
fbSharePhoto: function () {
console.log('share to fb ' + this.model.attributes.shareUrl)
},
close: function () {
//this.undelegateEvents();
this.remove();
},
render: function () {
this.$el.html(this.template(this.model.attributes));
this.delegateEvents({
'click .fb-share' : 'fbSharePhoto',
'click .black-overlay' : 'close'
});
return this;
}
})
Here is the router:
var AppRouter = Backbone.Router.extend({
routes: {
"" : "dashboard",
"image/:iId" : "showImage",
},
initialize: function () {
// this.galleriesCollection = new GalleriesCollection(); //A collection of galleries
// this.galleriesCollection.fetch();
this.imageModel = new Image();
this.imageView = new ImageView ({ model: this.imageModel });
},
dashboard: function () {
console.log('#AppRouter show dashboard - hide everything else');
//$('#app').html(this.menuView.render().el);
},
showImage: function (iId) {
console.log('#AppRouter showPhoto() ' + iId);
this.imageModel.set('id', iId);
this.imageModel.fetch();
$('#imageViewer').html(this.imageView.render().el);
}
});
Is is it that the model still has the old info or the view, or both?
For extra credit, how could I detect a failure to fetch and respond to it by not triggering the corresponding view? Or I am I coming at it wrongly?
Thanks in advance for any advice.
////////////////////////////////////////////////////////////////////////////
Looks like I found something that works. I think just the process of framing the question properly helps to answer it. (I'm not allowed to answer the question so I'll post what I found here)
It appears that its the model that needs refreshing in this case. In the app router when I call the showImage function I clear the model and reset its values to default before calling fetch and this did the trick. Ironically the trick here is showing a broken image tag.
showImage: function (iId) {
console.log('#AppRouter showPhoto() ' + iId);
this.imageModel.clear().set(this.imageModel.defaults);
this.imageModel.set('id', iId);
this.imageModel.fetch();
$('#imageViewer').html(this.imageView.render().el);
}
For my own extra credit offer: In the event of an error (if needed fetch() accepts success and error callbacks in the options hash). Still definitely open to hearing about a way of doing this thats baked in to the framework.
You can just update the model like this:
ImageView.model.set(attributes)
Related
In the following code, I am trying to trigger an event using dynamic require. For some reason I am not able to access app object in the eventRouter method. I am getting "TypeError: app is undefined" error. I have implemented listener on show event in respective controller files.
My question is similar to this post except my listeners are in different controller files and I am not able to access app object as suggested in the post.
Help appreciated !!!!
define(["app",
"tpl!templates/nav/nav.tpl",
"tpl!templates/nav/navMenuItem.tpl",
"entities/navEntity"
],
function(app, navTpl, navMenuItem, navEntity){
navMenuItem = Backbone.Marionette.ItemView.extend({
template: navMenuItem,
events: {
"click a": "eventRouter"
},
eventRouter:function(ev)
{
var that = this;
var moduleName = $(ev.currentTarget).text().toLowerCase();
require(['controllers/' + moduleName + 'Controller'], function(controller){
app.trigger(moduleName + ':show');
});
}
});
navMenu = Backbone.Marionette.CollectionView.extend({
tagName: 'ul',
itemView: navMenuItem,
collection: navEntity.navItems,
});
return {
navMenu: navMenu,
navMenuItem: navMenuItem
}
});
To overcome Circular dependencies you can check the Following :
https://stackoverflow.com/a/4881496/2303999
Manage your modules accordingly and avoid dependencies. Make common js file for functions you use use now and then. You can even use Marionette Vent object to pass events and do according on that event.
I am exploring the BBCloneMail demo application for MarionetteJS, but I am not seeing how the events are triggering the rendering actions. I saw some global 'show' event here:
https://github.com/marionettejs/bbclonemail/blob/master/public/javascripts/bbclonemail/components/appController.js#L25
show: function(){
this._showAppSelector("mail");
Marionette.triggerMethod.call(this, "show");
},
But I don't see, where/how the Marionette.triggerMethod results into rendering the Mail component. I was trying to call the triggerMethod for my case, but I get a 'cannot call apply for undefined'. Why is the call above working for the BBcloneMail application.
The Application controller for my case:
MA.AppController = Marionette.Controller.extend({
initialize: function(){
_.bindAll(this, "_showGenres");
},
show: function() {
if (MA.currentUser) {
MA.navbar.show(new MA.Views.Items.LogoutNavbar({model: MA.currentUser}));
}
else
{
MA.navbar.show(new MA.Views.Items.LoginNavbar());
}
this._showGenres();
},
_showGenres: function() {
var categoryNav = new MA.Navigation.Filter({
region: MA.filter
});
this.listenTo(categoryNav, "genre:selected", this._categorySelected);
categoryNav.show();
MA.main.show(MA.composites.movies);
},
showMovieByGenre: function(genre){
var movies = new MA.Controllers.MoviesLib();
that = this;
$.when(movies.getByCategory(genre)).then(that._showMovieList);
Backbone.history.navigate("#movies/genres/" + genre);
},
_showMovieList: function(movieList){
var moviesLib = new MA.Controllers.MoviesLib({
region: MA.main,
movies: movieList
});
Marionette.triggerMethod.call(this, "show");
}
});
I init the application controller in a init.js with:
app = new MA.AppController();
Looking at the source for triggerMethod, this is a way of both triggering an event (the string being passed in), and additionally (if it exists) running a method on the object that has an 'on' prefix.
In your case the error relates to line 560, specifically that there is no method apply on undefined. Based on the code its (in your case) trying to call the equivilent of this.trigger('show') - but AppController doesn't have a method called trigger.
In which case I'm guessing that in the BBCloneMail example this (being bassed into triggerMethod.call) is not actually the controller, but instead the view that is to be shown.
I'm using the Backbone Layout Manager Boilerplate. Unfortunately, a quite frustrating bug occurred. I like render a list of items as subviews inserted by insertView function. At the first load everthing works fine. But after a reload the the click events doesn't work anymore :(. I already tried to call delegateEvents() on the TableItem View manually but nothing changed. I hope anyone can give me a clue.
App.Views.Item = Backbone.View.extend({
template: "templates/item",
tagName: "li",
events: {
"click .applyButton" : "apply",
"click .viewDetailsButton" : "showDetail"
},
serialize: function() {
return { table : this.model.toJSON() };
},
apply: function(ev) {
ev.preventDefault();
alert("apply button clicked");
},
showDetail: function(ev) {
ev.preventDefault();
var id = this.model.get("_id");
app.router.navigate("#events/"+ id, {trigger : true})
}
});
/*
* List View
*/
App.Views.List = Backbone.View.extend({
template: "templates/list",
tagNam: "ul",
className: "tableList",
beforeRender: function() {
var events = this.model.get("userEvents").get("hosting");
events.each(function(model) {
this.insertView(new App.Views.Item({ model : model }));
}, this);
},
serialize: function() {
return {};
}
});
I think you might want to add a cleanup function on your Item view to undelegate the events when layoutmanager removes the view. I don't know if this will fix your problem, but it seems like good practise.
When you say after a reload, do you mean reloading the page with the browser reload button? if so, how do you get it to work in the first place?
It would help if you could provide a jsfiddle of your setup, or point us to a repo so we can test it on our machines. Make sure you include the router so that we can have a look at how the view and the layout that contains it are initialised.
I am developing a backbone application which is using require.js.
I want a user to enter in the 'id' for a model and then either be redirected to a view for that model if it exists, or display an error message if it does not. This sounds extremely simple, but I am having trouble figuring out the roles of each component.
In the application below, the user will come to an index page with an input (with id 'modelId') and a button (with class attribute 'lookup').
The following piece of code is the router.
define(['views/index', 'views/myModelView', 'models/myModel'],
function(IndexView, MyModelView, myModel) {
var MyRouter = Backbone.Router.extend({
currentView: null,
routes: {
"index": "index",
"view/:id": "view"
},
changeView: function(view) {
if(null != this.currentView) {
this.currentView.undelegateEvents();
}
this.currentView = view;
this.currentView.render();
},
index: function() {
this.changeView(new IndexView());
},
view: function(id) {
//OBTAIN MODEL HERE?
//var model
roter.changeView(new MyModelView(model))
}
});
return new MyRouter();
});
The following piece of code is the index view
define(['text!templates/index.html', 'models/myModel'],
function( indexTemplate, MyModel) {
var indexView = Backbone.View.extend({
el: $('#content'),
events: {
"click .lookup": "lookup"
},
render: function() {
this.$el.html(indexTemplate);
$("#error").hide();
},
lookup: function(){
var modelId = $("#modelId").val()
var model = new MyModel({id:modelId});
model.fetch({
success: function(){
window.location.hash = 'view/'+model.id;
},
error: function(){
$("#error").text('Cannot view model');
$("#error").slideDown();
}
});
},
});
return indexView
});
What I can't figure out is that it seems like the better option is for the index view to look up the model (so it can display an error message if the user asks for a model that doesn't exist, and also to keep the router cleaner). But the problem is that the router now has no reference to the model when the view/:id router is triggered. How is it supposed to get a hold of the model in the view() function?
I guess it could do another fetch, but that seems redundant and wrong. Or maybe there is supposed to be some global object that both the router and the view share (that the index view could put the model in), but that seems like tight coupling.
You can do something like this. You could do something similar with a collection instead of a model, but it seems like you don't want to fetch/show the whole collection?
With this type of solution (I think similar to what #mpm was suggesting), your app will handle browser refreshes, back/forward navigation properly. You basically have a MainView, which really acts more like a app controller. It handles events triggered either by the router, or by user interaction (clicking lookup or a back-to-index button on the item view).
Credit to Derick Bailey for a lot of these ideas.
In the Router. These are now only triggered if the user navigates by changing a URL or back/forward.
index: function() {
Backbone.trigger('show-lookup-view');
},
view: function(id) {
var model = new MyModel({id: id});
model.fetch({
success: function(){
Backbone.trigger('show-item-view', model);
},
error: function(){
// user could have typed in an invalid URL, do something here,
// or just make the ItemView handle an invalid model and show that view...
}
});
}
In new MainView, which you would create on app startup, not in router:
el: 'body',
initialize: function (options) {
this.router = options.router;
// listen for events, either from the router or some view.
this.listenTo(Backbone, 'show-lookup-view', this.showLookup);
this.listenTo(Backbone, 'show-item-view', this.showItem);
},
changeView: function(view) {
if(null != this.currentView) {
// remove() instead of undelegateEvents() here
this.currentView.remove();
}
this.currentView = view;
this.$el.html(view.render().el);
},
showLookup: function(){
var view = new IndexView();
this.changeView(view);
// note this does not trigger the route, only changes hash.
// this ensures your URL is right, and if it was already #index because
// this was triggered by the router, it has no effect.
this.router.navigate('index');
},
showItem: function(model){
var view = new ItemView({model: model});
this.changeView(view);
this.router.navigate('items/' + model.id);
}
Then in IndexView, you trigger the 'show-item-view' event with the already fetched model.
lookup: function(){
var modelId = $("#modelId").val()
var model = new MyModel({id:modelId});
model.fetch({
success: function(){
Backbone.trigger('show-item-view', model);
},
error: function(){
$("#error").text('Cannot view model');
$("#error").slideDown();
}
});
},
I don't think this is exactly perfect, but I hope it could point you in a good direction.
I'm quite new in the world of Backbone and I decided to use Marionette for my first serious project with it.
With some difficulties I managed to set up my app's basic options and routing and I was pretty happy with it, but now I'm facing a blocking problem with a CompositeView that represent a Table.
This View is rendered inside a region of a specific layout, called "grid". This layout has 3 region: the top_controls, table_view and bottom_controls. Since I needed to bind some action on some of the elements of the layout I decided to use it as a View, and to include the "master" collection inside it, so I can just rendered a filtered version of the collection inside the CompositeView, without touching the main one.
From my router I call it in this way:
App.grid = new Grid({collection: Clt});
App.page.show(App.grid);
The structure of the layout is this (I'm using requireJS):
var Grid = Backbone.Marionette.Layout.extend({
className: "container-fluid",
template: gridLayout,
regions: {
top_controls: "#top_controls",
table_view: "#table_view",
bottom_controls: "#bottom_controls",
},
initialize: function(){
this.renderTable(this.collection, true);
},
renderTable: function(collection, fetch){
if(fetch){
collection.fetch({success:function(){
var vista = new CompView({collection: collection});
App.grid.table_view.show(vista);
}});
} else {
var vista = new CompView({collection: collection});
App.grid.table_view.show(vista);
}
},
events: {
"keyup input":"filter_grid"
},
filter_grid: function(e){
var $el = e.currentTarget;
var to_filter = $($el).val();
if(to_filter==""){
this.renderTable(this.collection, false);
} else {
var filtered = this.collection.filter(function(item){
return item.get("link_scheda").toLowerCase() == to_filter;
});
if(filtered.length>0){
var filtro = new AssocCollection();
filtro.reset(filtered);
this.renderTable(filtro, false);
}
}
}
});
return Grid;
The Layout template looks like this:
<div class="row-fluid" id="top_controls"><input type="text" id="filter" class="input"/></div>
<div class="row-fluid" id="table_view"></div>
<div class="row-fluid" id="bottom_controls"><button class='add btn btn-primary'>Add</button></div>
My CompositeView is structured like that:
var AssocView = Backbone.Marionette.CompositeView.extend({
tagName: 'table',
className: 'table table-bordered table-striped',
id: 'tableAssoc',
template: assocTemplate,
itemView: assocRow,
appendHtml: function(collectionView, itemView, index){
collectionView.$("tbody").append(itemView.el);
},
events: {
"click .sort_link":"sort_for_link",
},
sort_for_link: function(){
this.collection.comparator = function(model){
return model.get("link_value");
}
this.collection.sort();
},
onRender: function(){
console.log("render table!");
}
});
return AssocView;
The first display of the table is done right, and the filtering too. The problem occur when
I click the table header with the class "sort_link": the entire Table is wiped away from the HTML while the collection stay the same (I suppode the entire layout is re-rendered). If for example I render the CompositeView in another place, like the app main region, it all works as intended. So I guess to problem it's located inside my Layout declaration.
Any help will be much appreciated!
In your Grid, you need to override the initialEvents method and don't do anything in it.
Grid = Backbone.Marionette.Layout.extend({
initialEvents: function(){},
// ... everything you already have
});
Layout extends from ItemView, and ItemView provides the initialEvents implementation. This method checks to see if it was given a collection, and if it does, it wires up the collection "reset" event to the "render" method of the view. In your case, you are passing the collection through and don't want this behavior. So, overriding the initialEvents method will correct it.
Update: I thought I had removed that initialEvents a long time ago. If you're keeping up to date w/ Marionette versions, grab v0.9.10 (or whatever the latest is) and this problem is gone now.