How can I create a new Backbone view from within that view? For example; my view ModalDialog1 needs to (re)instantiate itself when ModalDialog2 is closed.
define('ModalDialog1.View',
[
'modal_dialog1.tpl'
, 'ModalDialog2.View'
, 'Backbone'
, 'underscore'
],
function(
modal_dialog1_tpl
, ModalDialog2View
, Backbone
, _
)
{
'use strict';
return Backbone.View.extend({
template: modal_dialog1_tpl
, events: {
'click a[data-modal-id="why-need-info"]': 'openModalDialog2'
}
, openModalDialog2: function() {
var self = this;
var closeCallback = function() {
// How to reinstantiate this view/self??
var modalDialog1 = new self();
modalDialog1 .showInModal();
}
var view = new ModalDialog2View({closeCallback: closeCallback})
.showInModal();
// On calling showInModal the current modal view (this) is destroyed
}
, getContext: function()
{
return {
}
}
})
});
You could use the constructor of the view in question.
var modalDialog1 = new self.constructor();
modalDialog1.showInModal();
The second option would be is to have a method which initializes modalDialog1 when modalDialog2 closes.
I have a Backbone Marionette application whose layout's regions are not working properly. My app is structured using Require modules and some of these modules' regions are failing to close themselves when the module has been returned to a second time. The first time through the regions are closing as expected but upon return the layout object no longer contains the region objects it did during the first visit: I am using the browser debugger to ascertain this difference.
Here is my Module code:-
define(["marionette", "shell/shellapp", "interaction/videoreveal/model", "interaction/videoreveal/controller", "utils/loadingview", "utils/imagepreloader"], function(Marionette, shellApp, Model, Controller, LoadingView, imagePreloader){
var currentModuleModel = shellApp.model.get("currentInteractionModel"); // get module name from menu model
var Module = shellApp.module(currentModuleModel.get("moduleName")); // set application module name from menu model
Module.init = function() { // init function called by shell
//trace("Module.init()");
Module.model = new Model(shellApp.interactionModuleData); // pass in loaded data
Module.controller = new Controller({model: Module.model, mainRegion:shellApp.mainRegion}); // pass in loaded data and region for layout
Module.initMain();
};
Module.initMain = function() {
trace("Module.initMain()");
shellApp.mainRegion.show(new LoadingView());
// do some preloading
var imageURLs = this.model.get('imagesToLoad');
imagePreloader.preload(imageURLs, this.show, this);
};
Module.show = function() {
Module.controller.show();
};
Module.addInitializer(function(){
//trace("Module.addInitializer()");
});
Module.addFinalizer(function(){
//trace("Module.addFinalizer()");
});
return Module;
});
Here is the Controller class which is handling the Layout and Views:-
define(["marionette", "shell/vent", "shell/shellapp", "interaction/videoreveal/layout", "interaction/videoreveal/views/mainview", "ui/feedbackview", "ui/videoview"], function(Marionette, vent, shellApp, Layout, MainView, FeedbackView, VideoView){
return Marionette.Controller.extend({
initialize: function(options){
trace("controller.initialize()");
// store a region that will be used to show the stuff rendered by this component
this.mainRegion = options.mainRegion;
this.model = options.model;
this.model.on("model:updated", this.onModelUpdate, this);
this.layout = new Layout();
this.layout.render();
this.mainView = new MainView({model:this.model, controller:this});
this.feedbackView = new FeedbackView({feedbackBoxID:"vrFeedbackBox"});
this.videoView = new VideoView({videoContainerID:"vrVideoPlayer"});
vent.on("feedbackview:buttonclicked", this.onFeedbackClick, this);
vent.on("videoview:buttonclicked", this.onVideoClick, this);
},
// call the "show" method to get this thing on screen
show: function(){
// get the layout and show it
this.mainRegion.show(this.layout);
this.model.initInteraction();
},
initFeedback: function (index) {
this.model.set("currentItem", this.model.itemCollection.models[index]);
this.model.set("itemIndex", index);
this.model.initFeedback();
},
initVideo: function (index) {
this.model.set("currentItem", this.model.itemCollection.models[index]);
this.model.set("itemIndex", index);
this.model.initVideo();
},
finalizer: function() {
this.layout.close();
},
// events
onFeedbackClick: function(e) {
this.layout.overlayRegion.close();
},
onVideoClick: function(e) {
this.layout.overlayRegion.close();
},
onFinishClick: function() {
this.model.endInteraction();
},
onFeedbackClosed: function() {
this.layout.overlayRegion.off("close", this.onFeedbackClosed, this);
if (this.model.get("currentItem").get("correct") === true) {
this.model.initThumb();
}
},
onModelUpdate: function() {
trace("controller onModelUpdate()");
switch (this.model.get("mode")) {
case "initInteraction":
this.layout.mainRegion.show(this.mainView);
break;
case "initFeedback":
this.layout.overlayRegion.on("close", this.onFeedbackClosed, this);
this.feedbackView = new FeedbackView({feedbackBoxID:"vrFeedbackBox"})
this.feedbackView.setContent(this.model.get("currentItem").get("feedback"));
this.layout.overlayRegion.show(this.feedbackView );
break;
case "initVideo":
this.layout.overlayRegion.show(new VideoView({videoContainerID:"vrVideoPlayer"}));
break;
case "interactionComplete":
vent.trigger('interactionmodule:completed', this);
vent.trigger('interactionmodule:ended', this);
break;
}
}
});
});
Here is the FeedbackView class:-
define(['marionette', 'tweenmax', 'text!templates/ui/feedbackWithScrim.html', 'shell/vent'], function (Marionette, TweenMax, text, vent) {
return Marionette.ItemView.extend({
template: text,
initialize: function (options) {
this.model = options.model;
this.content = options.content; // content to add to box
this.feedbackBoxID = options.feedbackBoxID; // id to add to feedback box
this.hideScrim = options.hideScrim; // option to fully hide scrim
},
ui: {
feedbackBox: '.feedbackBox',
scrimBackground: '.scrimBackground'
},
events : {
'click button': 'onButtonClick' // any button events within scope will be caught and then relayed out using the vent
},
setContent: function(content) {
this.content = content;
},
// events
onRender: function () {
this.ui.feedbackBox.attr("id", this.feedbackBoxID);
this.ui.feedbackBox.html(this.content);
if (this.hideScrim) this.ui.scrimBackground.css("display", "none");
this.$el.css('visibility', 'hidden');
var tween;
tween = new TweenMax.to(this.$el,0.5,{autoAlpha:1});
},
onButtonClick: function(e) {
trace("onButtonClick(): "+ e.target.id);
vent.trigger("feedbackview:buttonclicked", e.target.id) // listen to this to catch any button events you want
},
onShow : function(evt) {
this.delegateEvents(); // when rerendering an existing view the events get lost in this instance. This fixes it.
}
});
});
Any idea why the region is not being retained in the layout when the module is restarted or what I can do to correct this?
Much thanks,
Sam
Okay.... I got there in the end after much debugging. I wouldn't have got there at all if it wasn't for the generous help of the others on this thread so THANKYOU!
Chris Camaratta's solutions definitely pushed me in the right direction. I was getting a Zombie layout view in my Controller class. I decided to switch a lot of my on listeners to listenTo listeners to make their decoupling and unbinding simpler and hopefully more effective. The key change though was to fire the Controller class's close method. I should have had this happening all along but honestly it's my first time getting into this mess and it had always worked before without needing to do this. in any case, lesson hopefully learned. Marionette does a great job of closing, unbinding and handling all of that stuff for you BUT it doesn't do everything, the rest is your responsibility. Here is the key modification to the Module class:-
Module.addFinalizer(function(){
trace("Module.addFinalizer()");
Module.controller.close();
});
And here is my updated Controller class:-
define(["marionette", "shell/vent", "shell/shellapp", "interaction/videoreveal/layout", "interaction/videoreveal/views/mainview", "ui/feedbackview", "ui/videoview"], function(Marionette, vent, shellApp, Layout, MainView, FeedbackView, VideoView){
return Marionette.Controller.extend({
initialize: function(options){
trace("controller.initialize()");
// store a region that will be used to show the stuff rendered by this component
this.mainRegion = options.mainRegion;
this.model = options.model;
this.listenTo(this.model, "model:updated", this.onModelUpdate);
this.listenTo(vent, "feedbackview:buttonclicked", this.onFeedbackClick);
this.listenTo(vent, "videoview:buttonclicked", this.onVideoClick);
},
// call the "show" method to get this thing on screen
show: function(){
// get the layout and show it
// defensive measure - ensure we have a layout before axing it
if (this.layout) {
this.layout.close();
}
this.layout = new Layout();
this.mainRegion.show(this.layout);
this.model.initInteraction();
},
initFeedback: function (index) {
this.model.set("currentItem", this.model.itemCollection.models[index]);
this.model.set("itemIndex", index);
this.model.initFeedback();
},
initVideo: function (index) {
this.model.set("currentItem", this.model.itemCollection.models[index]);
this.model.set("itemIndex", index);
this.model.initVideo();
},
onClose: function() {
trace("controller onClose()");
if (this.layout) {
this.layout.close();
}
},
// events
onFeedbackClick: function(e) {
this.layout.overlayRegion.close();
},
onVideoClick: function(e) {
this.layout.overlayRegion.close();
},
onFinishClick: function() {
this.model.endInteraction();
},
onFeedbackClosed: function() {
if (this.model.get("currentItem").get("correct") === true) {
this.model.initThumb();
}
},
onModelUpdate: function() {
trace("controller onModelUpdate()");
switch (this.model.get("mode")) {
case "initInteraction":
this.layout.mainRegion.show(new MainView({model:this.model, controller:this}));
break;
case "initFeedback":
var feedbackView = new FeedbackView({feedbackBoxID:"vrFeedbackBox", controller:this});
feedbackView.setContent(this.model.get("currentItem").get("feedback"));
this.layout.overlayRegion.show(feedbackView);
this.listenTo(this.layout.overlayRegion, "close", this.onFeedbackClosed);
break;
case "initVideo":
this.layout.overlayRegion.show(new VideoView({videoContainerID:"vrVideoPlayer"}));
break;
case "interactionComplete":
vent.trigger('interactionmodule:completed', this);
vent.trigger('interactionmodule:ended', this);
break;
}
}
});
});
If I understand your question correctly your views do not work well after they are closed and re-opened.
It looks like you are using your layout/views after they are closed, and keeping them for future use with these references:
this.feedbackView = new FeedbackView();
Marionette does not like this, once you close a view, it should not be used again. Check out these issues:
https://github.com/marionettejs/backbone.marionette/pull/654
https://github.com/marionettejs/backbone.marionette/issues/622
I would advise you not to store these views and just recreate them when you show them
layout.overlayRegion.show(new FeedbackView());
#ekeren's answer is essentially right; I'm just expanding on it. Here's some specific recommendations that I believe will resolve your issue.
Since you're utilizing regions you probably don't need to create your views ahead of time:
initialize: function(options) {
this.mainRegion = options.mainRegion;
this.model = options.model;
this.model.on("model:updated", this.onModelUpdate, this);
vent.on("feedbackview:buttonclicked", this.onFeedbackClick, this);
vent.on("videoview:buttonclicked", this.onVideoClick, this);
},
Instead, just create them dynamically as needed:
onModelUpdate: function() {
switch (this.model.get("mode")) {
case "initInteraction":
this.layout.mainRegion.show(new MainView({model:this.model, controller:this}));
break;
case "initFeedback":
var feedbackView = new FeedbackView({feedbackBoxID:"vrFeedbackBox"})
feedbackView.setContent(this.model.get("currentItem").get("feedback"));
this.layout.overlayRegion.show(feedbackView);
this.layout.overlayRegion.on("close", this.onFeedbackClosed, this);
break;
case "initVideo":
this.layout.overlayRegion.show(new VideoView({videoContainerID:"vrVideoPlayer"}));
break;
case "interactionComplete":
vent.trigger('interactionmodule:completed', this);
vent.trigger('interactionmodule:ended', this);
break;
}
}
The layout is a bit of a special case since it can be closed in several places, but the principle applies:
show: function(){
// defensive measure - ensure we have a layout before axing it
if (this.layout) {
this.layout.close();
}
this.layout = new Layout();
this.mainRegion.show(this.layout);
this.model.initInteraction();
},
Conditionally cleanup the layout:
finalizer: function() {
if (this.layout) {
this.layout.close();
}
},
I have a backbone view that is initialized via route, but i when i navigate to another route and return to the previous one again via link, the events in the view get fired twice
Heres my router
define(['underscore','backbone','views/projects/view_project',
'views/projects/project_tasks','views/projects/project_milestones',
'views/projects/project_tasklists','views/projects/project_documents'
],
function( _,Backbone,ProjectTasks,ProjectMilestones,
ProjectTasklists,ProjectDocuments) {
var ProjectRouter = Backbone.Router.extend({
initialize: function(projects) {
if(projects) {
this.projects = projects;
}
},
//url routes mapped to methods
routes: {
"project/:id":"get_project",
"project/:id/milestones":"get_project_milestones",
"project/:id/tasks":"get_project_tasks",
"project/:id/tasklists":"get_project_tasklists",
"project/:id/documents":"get_project_documents"
},
get_project: function(id) {
UberERP.UI.loadpage("#project-view");
var project_view = new ProjectView(this.projects,id);
},
get_project_tasks: function(id) {
UberERP.UI.loadpage("#project-tasks-view");
var project_tasks_view = new ProjectTasks(id,this.projects);
},
get_project_tasklists: function(id) {
UberERP.UI.loadpage("#project-tasklist-view");
var project_tasks_view = new ProjectTasklists(id,this.projects);
},
get_project_milestones: function(id) {
UberERP.UI.loadpage("#project-milestones-view");
var project_milestones_view = new ProjectMilestones(id,this.projects);
},
get_project_documents: function(id) {
UberERP.UI.loadpage("#project-documents-view");
var project_documents_view = new ProjectDocuments(id,this.projects);
}
});
return ProjectRouter;
});
and a snipper from the view
events: {
"click input[name=task]":"select_task",
"click a.remove-icon":"remove_task",
"click td.view-task":"view_task",
"click #project-tasks-table .sort-by-status":"sort_by_status",
"click #project-tasks-table .group-filter-btn":"sort_by_task_list"
},
select_task: function( event ) {
var el = $(event.currentTarget);
row = el.parent('td').parent('tr');
console.log(el.val());
if(row.hasClass('active')) {
row.removeClass('active');
}
else {
row.addClass('active');
}
}
I have a line in the select_task method that logs the value of the clicked input element.
When the view is initially called it works properly and logs to the console. But after navigating to another route and returning back, the value of the input element is logged twice when clicked. What could be wrong?
I think you just find your self in the middle of a Backbone ghost View issue.
Try to remove and unbind all your Views when you are moving from one route to another.
I'm trying out backbonejs but got stuck on how to bind the model to the view.
yepnope({
load : ["/static/js/lib/jquery-1.6.2.min.js", "/static/js/lib/underscore-min.js", "/static/js/lib/backbone-min.js"],
complete: nameList
});
function nameList() {
var PageItem = Backbone.Model.extend({
defaults: {name: "default name" }
});
var Page = Backbone.Collection.extend({
model: PageItem
});
var page = new Page;
var AppView = Backbone.View.extend({
el: $("#names"),
$artistList: $('#names_list'),
$inputField: $('input#new_name'),
events: {
"keypress input": "processKeyPress"
},
processKeyPress: function(event){
if(event.charCode == 13) {
event.preventDefault();
this.addName();
}
},
addName: function(event) {
var newName = this.$inputField.val();
this.$artistList.prepend('<li>' + newName + '</li>');
page.push(new PageItem({name: newName}));
// I've also tried page.push({name: newName});
} });
var app = new AppView;}
When I press enter on the input field, it runs processKeyPress which calls addName, the new name is added the the html list but not pushed onto the model. I keep getting:
Uncaught TypeError: Object function (a){return new l(a)} has no method 'isObject'
ok, please test this out for yourself, but it appears to work with the Github version of underscore so maybe there was a bug which has been fixed.. http://jsfiddle.net/yjZVd/6/
I've got a timer that I'm updating dynamically.
------------------update --------------------------
when I first posted the question, I didn't think it mattered that the timer was being called from within a backbone view, but I believe as a result of that, I can't use a global variable (or at least a global variable isn't working). I'll be calling multiple timers, so setting only one global variable and deleting it won't work. I need to be able to clear a single timer without clearing the others.
What I start the timer,
function countDown(end_time, divid){
var tdiv = document.getElementById(divid),
to;
this.rewriteCounter = function(){
if (end_time >= MyApp.start_time)
{
tdiv.innerHTML = Math.round(end_time - MyApp.start_time);
}
else {
alert('times up');
}
};
this.rewriteCounter();
to = setInterval(this.rewriteCounter,1000);
}
in my app, I initiate the timer in a backbone view with
MyApp.Views.Timer = Backbone.View.extend({
el: 'div#timer',
initialize: function(){
timer = this.model;
this.render();
},
events: {
"clicked div#add_time": "update_timer"
}
render: function(){
$(this.el).append(HandlebarsTemplates['timer'](timer);
this.start_timer();
},
start_timer: function(){
delete main_timer; // this doesn't work :(
clearTimtout(main_timer); //this doesn't work either :(
var main_timer = setTimeout(new countDown(timed.length, 'main_timer'),timed.length*1000);
},
update_timer: function(){
timed.length=timed.length+30
this.start_timer();
}
});
so what I'm trying to do is to update the timer, kill the old timer, and then restart it with the new values. I have different timers, so just calling the timed.length within the countdown function won't work.
var main_timer = setTimeout(new countDown(timed.length, 'main_timer'),timed_length*1000);
This statement creates a local variable main_timer. Instead you have to create a global variable and use that to clear the time out as shown below
clearTimtout(main_timer);
main_timer = setTimeout(new countDown(timed.length, 'main_timer'),timed_length*1000);
EDIT:
use a function as setTimeout handler as shown below
clearTimeout(main_timer);
main_timer = setTimeout(function(){
new countDown(timed.length, 'main_timer');
},timed_length*1000);
note: hope timed.length and timed_length are correct.
EDIT:
modify countdown like given below.
function countDown(end_time, divid){
var tdiv = document.getElementById(divid),
to;
this.rewriteCounter = function(){
if (end_time >= MyApp.start_time)
{
tdiv.innerHTML = Math.round(end_time - MyApp.start_time);
}
else {
alert('times up');
}
};
this.clearRewriteCounter = function(){
clearInterval(to);
}
this.rewriteCounter();
to = setInterval(this.rewriteCounter,1000);
return this;
}
and in MyApp.Views.Timer
MyApp.Views.Timer = Backbone.View.extend({
el: 'div#timer',
initialize: function(){
timer = this.model;
this.render();
},
events: {
"clicked div#add_time": "update_timer"
}
render: function(){
$(this.el).append(HandlebarsTemplates['timer'](timer);
this.start_timer();
},
start_timer: function(){
clearTimeout(this.main_timer);
this.main_timer = setTimeout(function(){
if(this.countDownInstance){
this.countDownInstance.clearRewriteCounter();
}
this.countDownInstance = new countDown(timed.length, 'main_timer');
},timed_length*1000);
},
update_timer: function(){
timed.length=timed.length+30
this.start_timer();
}
});