Hey all I am pretty new to Backbone though I have put several days into trying to get familiar with this framework, and it seems everytime I start feeling comfortable I run across a new problem.
I am wondering how to reference a view that is rendered from within my main appview. I know this is a really simple issue but I just can't seem to figure it out.
So for instance I have a simple view
var SubView = Backbone.View.extend({
//something here including render function
});
Then I render that view from within the main app view
var myApp = Backbone.View.extend({
render: function{
var mysubView = new SubView();
mysubView.render();
},
editSomething: function{
mysubView.remove();
}
});
When I try and reference that view from a function (editSomething:) in the main app view, I get a reference error.
What I am trying to achieve is that I have two views that include forms. I want to swtich between the two forms as an edit function is called and when an add function is called. But I can't seem to access the views that have already been rendered.
I don't want to initialize and render a new view before removing the existing view because from what I understand, I will start to get a bunch of views floating in memory.
Reference it using this:
var myApp = Backbone.View.extend({
render: function{
this.subView = new SubView();
this.subView.render();
},
editSomething: function{
this.subView.remove();
}
});
Related
I cant seem to access my Views from my main app file. I'm new to backbone and marionette and I'v read through the documents but cannot find why its not working. Any help or advise?
app.js
window.App = new Marionette.Application();
App.addRegions({
gameCriteria: "#game-criteria"
});
App.myRegion.show(myView);
});
myView.js
App.module("Views", function(Views, App, Backbone, Marionette, $, _) {
var GameCriteriaTab = Backbone.Marionette.ItemView.extend({
template: JST["game-criteria-tab"],
regions: {
rulesRegion: "#rules-region"
},
onShow: function() {
this.rulesRegion.show(RulesView);
}
});
});
did you instatiate your view before calling show ? i am waiting for a code similar to that :
App.myRegion.show(new GameCriteriaTab ({
model: new GameCriteriaModel()
}));
can you provide us with the error in chrome console ?
Not sure what you are trying to do here. I'm assuming that you want to create an app called 'windowApp', and you are trying to display a view called 'GameCriteriaTab' inside a region called 'gameCriteria'. I'm refactoring your code to the following:-
windowApp = new Backbone.Marionette.Application();
windowApp.addRegions({
gameCriteria: "#game-criteria"
});
windowApp.GameCriteriaTab = Marionette.ItemView.extend({ //Defining the view
template: " <include your template Id or className here> "
});
windowApp.on("start",function(){
var myView = new windowApp.GameCriteriaTab(); //You need to create an instance of the view if you want to render it in the page
windowApp.gameCriteria.show(myView); //Showing the view in the specified region
});
windowApp.start(); //This will run the marionette application
You cannot define any regions inside ItemView. Are you trying to create a nested view? In that case, a LayoutView would suit your needs, and you can add regions inside it. Just change the ItemView to a LayoutView then.
I have a view:
App.Views.Rebill = App.Views.baseView.extend({
view: 'requests._rebill_line',
tag: 'div',
className: 'row row-spaced',
render: function() {
var self = this;
App.get_view(self.view).done(function(data){
var view = Mustache.render(data, {
text: self.model.get('text'),
cost: self.model.get('cost')
});
self.$el.append(view);
var $selectVat = self.$el.find('select[name="vat"]');
var vatPicker = new App.Views.VatPicker({
model: self.model,
el: $selectVat
});
vatPicker.render();
});
return self;
}
});
this view is created in the parent:
addRebillLine: function(rebillModel){
var self = this;
var rebillView = new App.Views.Rebill({
model: rebillModel
});
self.$el.after(rebillView.render().$el);
},
I have linked this to a button, which creates an empty model, and calls the addRebillLine function. This works fine, the new Rebill view appears in the DOM when I click the button.
However, on rendering the parent view, I run through a json array, create a model with each line, and call the addRebillLine function with that model. This runs, but the Rebill views are not added to the DOM.
Further up, the parent view is itself a child view, and is attached to its parent like so:
this.$el.find('[data-role="repair-items"]').append(itemView.render().$el);
The parent and grandparent views are rendered synchronously, and the child view asynchronously (App.get_view() is basically a call to $.ajax, hence the .done())
The wierd thing is that I do the same thing with the App.Views.VatPicker view, in several other places, and that works just fine. The only difference is that I pass an element to attach to into the VatPicker view. But if I pass the parent $el element in, and run this.$parent.after(self.$el) in my done() callback, that doesn't work either.
When you call this line:
this.$el.find('[data-role="repair-items"]').append(itemView.render().$el);
it assumes your render code is synchronous (as it should be) , but you render code is not.
When render return the $el is not set yet, this is a problem in your design.
You should solve this design issue by making your templates available when the view needs them. Don't invent the wheel here, take a look at the TodoMVC backbone example.
If you want your templates to be loaded aysnc, use requirejs.
We are currently building a Marionette based application.
Basically, we have a Marionette Application that has multiple regions defined on it.
Each region will act as a container for different Modules to display their views. I want each Module to have full control of what is being displayed in it's container, but I want the Application to allocate these regions. For simplicity, let's say that each module just has a simple ItemView.
I'm considering 2 approaches to populating those regions with the module views.
The first approach says that when each module is initialized, it will create its view and it will call the application to display its view in the specified region, for example:
var app = new Marionette.Application();
app.addRegions({
regionA: "#regionA",
regionB: "#regionB"
});
app.module("moduleA", function(moduleA, app, ...){
moduleA.on("start", function(){
var viewA = new MyViewA();
app.regionA.show(viewA);
}
});
app.module("moduleB", function(moduleB, app, ...){
moduleB.on("start", function(){
var viewB = new MyViewB();
app.regionB.show(viewB);
}
});
The second approach says that each module should expose some function that returns its view. The Application will call that function when ready and it will stick the view in the designated region.
I'm not sure which approach is better and would be happy to hear opinions.
I would definitely go with the second approach, after having gone with the first approach in the past I am now hitting the limitations of this approach and moving to the second approach. I wrote a blog post about it here
It depends which approach you take, both are fine, we choose the second option because we use require.js to load our modules dynamically.
var dashboardPage = Backbone.Marionette.Layout.extend({
template: Handlebars.compile(tmpl),
regions: {
graphWidget : "#graphWidget",
datePickerWidget: "#datePickerWidget",
searchWidget : "#searchWidget"
},
widget: {
graphWidget: null,
datePickerWidget: null,
searchWidget: null,
},
initialize: function(options){
this.someId= options.someId;
//if have someId ID - fetch model;
if(this.someId){
//fetch model if campaignId not null
this.modelAjax = this.model.fetch();
}
onShow: function() {
var that = this;
if(this.modelAjax){
this.modelAjax.done(function(){
that.widget.graphWidget= new graphWidget(graphWidgetOptions);
that.listenTo(that.widget.graphWidget, 'graphWidget', that.getGraphWidgetData, that);
....
that.graphWidget.show(that.widget.graphWidget);
that.datePickerWidget.show(that.widget.datePickerWidget);
In my simple project I have 2 views - a line item view (Brand) and App. I have attached function that allows selecting multiple items:
var BrandView = Backbone.View.extend({
...some code...
toggle_select: function() {
this.model.selected = !this.model.selected;
if(this.model.selected) $(this.el).addClass('selected');
else $(this.el).removeClass('selected');
return this;
}
});
var AppView = Backbone.View.extend({
...some code...
delete_selected: function() {
_.each(Brands.selected(), function(model){
model.delete_selected();
});
return false;
},
});
Thing is, I want to know how many items are selected. In this setup selecting is NOT affecting the model and thus not firing any events. And from MVC concept I understand that views should not be directly talking to other views. So how can AppView know that something is being selected in BrandViews?
And more specifically, I AppView to know how many items were selected, so if more than 1 is selected, I show a menu for multiple selection.
You might want to have a read of this discussion of Backbone pub/sub events:
http://lostechies.com/derickbailey/2011/07/19/references-routing-and-the-event-aggregator-coordinating-views-in-backbone-js/
I like to add it in as a global event mechanism:
Backbone.pubSub = _.extend({}, Backbone.Events);
Then in one view you can trigger an event:
Backbone.pubSub.trigger('my-event', payload);
And in another you can listen:
Backbone.pubSub.on('my-event', this.onMyEvent, this);
I use what Addy Osmani calls the mediator pattern http://addyosmani.com/largescalejavascript/#mediatorpattern. The whole article is well worth a read.
Basically it is an event manager that allows you to subscribe to and publish events. So your AppView would subscript to an event, i.e. 'selected'. Then the BrandView would publish the 'selected' event.
The reason I like this is it allows you to send events between views, without the views being directly bound together.
For Example
var mediator = new Mediator(); //LOOK AT THE LINK FOR IMPLEMENTATION
var BrandView = Backbone.View.extend({
toggle_select: function() {
...
mediator.publish('selected', any, data, you, want);
return this;
}
});
var AppView = Backbone.View.extend({
initialize: function() {
mediator.subscribe('selected', this.delete_selected)
},
delete_selected: function(any, data, you, want) {
... do something ...
},
});
This way your app view doesn't care if it is a BrandView or FooView that publishes the 'selected' event, only that the event occured. As a result, I find it a maintainable way to manage events between parts of you application, not just views.
If you read further about the 'Facade', you can create a nice permissions structure. This would allow you to say only an 'AppView' can subscribe to my 'selected' event. I find this helpful as it makes it very clear where the events are being used.
Ignoring the problems with this that you already mention in your post, you can bind and trigger events to/from the global Backbone.Event object, which will allow anything to talk to anything else. Definitely not the best solution, and if you have views chatting with one another then you should consider refactoring that. But there ya go! Hope this helps.
Here is my case with a similar need: Backbone listenTo seemed like a solution to redirect to login page for timed out or not authenticated requests.
I added event handler to my router and made it listen to the global event such as:
Backbone.Router.extend({
onNotAuthenticated:function(errMsg){
var redirectView = new LoginView();
redirectView.displayMessage(errMsg);
this.loadView(redirectView);
},
initialize:function(){
this.listenTo(Backbone,'auth:not-authenticated',this.onNotAuthenticated);
},
.....
});
and in my jquery ajax error handler:
$(document).ajaxError(
function(event, jqxhr, settings, thrownError){
.......
if(httpErrorHeaderValue==="some-value"){
Backbone.trigger("auth:not-authenticated",errMsg);
}
});
You can use Backbone object as the event bus.
This approach is somewhat cleaner but still relies on Global Backbone object though
var view1 = Backbone.View.extend({
_onEvent : function(){
Backbone.trigger('customEvent');
}
});
var view2 = Backbone.View.extend({
initialize : function(){
Backbone.on('customEvent', this._onCustomEvent, this);
},
_onCustomEvent : function(){
// react to document edit.
}
});
Use the same model objects. AppView could be initialized with a collection, and BrandView initialized with one model from that collection. When attributes of a branch object change, any other code that has a reference to that model can read it.
So lets so you have some brands that you fetch via a collection:
var brands = new Brands([]);
brands.fetch();
Now you make an AppView, and an array of BrandView's for each model.
var appView = new AppView({brands: brands});
var brandViews = brands.map(function(brand) {
return new BrandView({brand: brand});
});
The appView and the brandViews now both have access to the same model objects, so when you change one:
brands.get(0).selected = true;
Then it changes when accessed by the views that reference it as well.
console.log(appView.brands.get(0).selected); // true
console.log(brandViews[0].brand.selected) // true
Same as John has suggested above, the Mediator Pattern works really good in this scenario, as Addy Osmani summing this issue up again in Backbone fundamentals.
Wound up using the Backbone.Mediator plugin which is simple and great, and makes my AMD View modules working together seamlessly =)
I'm new to backbone.js and trying to understand how routes, views etc works and now I have a problem with events building up for the same view. here is a clip that will show you exactly what I mean. http://screencast.com/t/QIGNpeT2OUWu
This is how my backbone router looks like
var Router = Backbone.Router.extend({
routes: {
"pages": "pages",
}
pages: function () {
var page_view = new PageView();
}
});
So when I click the Pages link I create a new PageView and this is the code I'm using
PageView = Backbone.View.extend({
el: $("#content"),
initialize: function () {
$.ajax({
url: '/pages',
success: function (data) {
$("#content").html(data);
}
});
},
events: {
"click td input[type=checkbox]": "updatePublishedStatus"
},
updatePublishedStatus: function (event) {
console.log('update publish status');
}
});
pretty basic I guess but as you can see in the clip each time I navigate to /pages I get another event registered to the checkbox.
There are a few things going wrong here.
Your video indicates pages being a collection well, Pages. Pages being a Backbone.Model with attributes such as Page name, slug, published etc... You lack that and it's going to hurt. You shouldn't just load some html and push it to your DOM, this defies the whole purpose of using Backbone in the first place.
If you do create a Model for a Page it will have a View. Then your /pages route will show the view of the Collection Pages etc.
You will fetch your data not inside a view's initialize but rather by doing pages.fetch(); where pages is an instance of the Pages collection. This can happen before you even initialize your router.
When changing attributes through your view, the individual models will be updated.
As a sidepoint: Fetching data on initialize is not great. You can call render() before you actually get the data and that's no fun.
Also instead of doing $('#content') you can use the view's $el. As in this.$el.html(...);
Move var page_view = new PageView() to be outside of Router.pages().
Have the PageView.initialize() success callback save data to a variable. Either in PageView or in a model.
Add a render function to PageView that sets $("#content").html(data);.
Call page_view.render() within Router.pages().