Marionette, layout within layout - javascript

I have a page that uses marionette layout to bind views to #content.
My landing page in my app is many views drawn together, so I planned to use another layout view instead of a composite view.
However currently with the code below I get:
TypeError: object is not a function
My controller.js calls views based on routes like this:
app.addRegions({
content: "#content"
});
define([], function() {
"use strict";
return {
landing: function() {
return require(["app/views/index"], function(View) {
return MyApp.content.show(new View()); ////LINE THROWING THE ERROR
});
}
};
});
The sub layout causing the issue
define([
"marionette",
'app/views/images/collection',
"tpl!app/templates/index.html"
],
function(Marionette, ImagesView, template) {
"use strict";
var AppLayout, layout;
AppLayout = void 0;
layout = void 0;
AppLayout = Backbone.Marionette.Layout.extend({
template: template(),
regions: {
collection1: "#images",
}
});
layout = new AppLayout();
layout.collection1.show(new ImagesView());
return layout;
})
Any help greatly appreciated

It looks like you're returning a new layout object from your index, and then trying to use it as a function in your controller.
In your index:
layout = new AppLayout();
...
return layout;
and then in your controller:
return MyApp.content.show(new View());
You probably just need to do
return MyApp.content.show(View);
UPDATE
After working on this some more, we found another issue. The ordering of rendering the views was wrong. The statement layout.collection1.show(new ImagesView()); won't actually render anything because layout itself hasn't been rendered yet. The solution to this was to move that statement into the onRender method of the AppLayout. This way, when MyApp.content.show(View) is called, it will automatically render the ImagesView at the correct time.

Related

Backbone sub views definition - main view vs router

Here is how my Backbone Router looks like
define([
"jquery",
"underscore",
"backbone"
], function ($, _, Backbone) {
return Backbone.Router.extend({
routes: {
"overview": "overview"
},
overview: function () {
require([
"views/overview",
"models/user-collection",
"grid",
"spreadsheet"
], function (OverviewView, TestCollection, GridView, SpreadSheetView) {
// Data
var collection = new TestCollection();
// Main view
var view = new OverviewView({
el: "#page",
collection: collection
});
// Sub view #1
var gridView = new GridView({
el: "#backgridWrapper"
});
// Sub View #2
var spreadsheetView = new SpreadSheetView({
el: "#handsontableWrapper"
});
// Flow
collection.fetch({
success: function () {
view.render();
gridView.render();
spreadsheetView.render();
}
});
});
}
});
});
As you can see there are several views:
Main view
Sub view #1
Sub view #2
I've did a lot of searching on how to organize the views and sub-views in Backbone, however all of them supposed to create a new sub-view instance directly within a view definition, so that router only knows about Main view...
So the question is - is it a good idea to handle sub-views at a router, instead of directly at view constructor?
The router should be just handling routes and initializing stuff.
Things like fetching data should go in the view that uses it - The view displays the data or error messages (in case of a failure), so I think it's wise to let the view fetch the data rather than some router who's only interested in the routes and have no interest in the data.
and I prefer initializing the sub views, inside their parent view, rather than somewhere else. The parent - child relationship itself justifies that, you better keep the children with their parents than a stranger so they will be under better control and you can easily find them later as well :)
Mostly it's a matter of opinion, but the thing is if you don't, all your code will soon get cluttered in the router rather than being well organized.
Below is how I'll structure the same thing.
Note that I'm initializing child views as part of parent views render method. It could be done when the parent view is initialized, but I see no point in doing so unless the parent view successfully fetches data and is proceeding to render itself.
define([
"jquery",
"underscore",
"backbone"
], function($, _, Backbone) {
return Backbone.Router.extend({
routes: {
"overview": "overview"
},
overview: function() {
require(["views/overview"], function(OverviewView) {
// initialize Main view
var view = new OverviewView({
el: "#page"
});
});
}
});
});
define([
"jquery",
"underscore",
"backbone",
"models/user-collection",
"grid",
"spreadsheet"
], function($, _, Backbone, TestCollection, GridView, SpreadSheetView) {
return Backbone.View.extend({
initialize: function(options) {
this.collection = new TestCollection();
this.fetchData();
},
events: {},
render: function() {
// rendering subviews is part of rendering their parent view.
//I prefer to do that here
// Sub view #1
this.gridView = new GridView({
el: "#backgridWrapper"
});
// Sub View #2
this.spreadsheetView = new SpreadSheetView({
el: "#handsontableWrapper"
});
//Below lines can be handled while initializing the respective view
// (In their initialize() method, or after fetching some data etc
// or can be chained with the above initialization if their render() method returns a reference to itself (`return this`)
this.gridView.render();
this.spreadsheetView.render();
},
fetchData: function() {
var view = this;
this.collection.fetch({
success: function() {
view.render();
}
});
}
});
});
side note : I strongly suggest not to put a collection under models folder.

Error while processing route: Cannot read property 'connectOutlet' in Ember.js

I'm getting the following error after upgrading Ember to the latest version:
Error while processing route: portfolio Cannot read property 'connectOutlet'
The error takes place whenever I navigate for example from:
http://localhost:8080/#/portfolio
to:
http://localhost:8080/#/publications
The weird thing is that if I refresh the pages many times sometimes it works and some times it's doesn't so feels like some file is loaded too late or maybe loaded twice.
aplication.hbs
The application view renders the header, footer and main container, which contains the application {{outlet}}.
<!-- ... -->
<div class="container" id="maincontainer">
<div class="maincontainer">
{{outlet}}
</div>
</div>
<!-- ... -->
index.hbs
My index view renders a couple of subviews:
<div class="jumbotron fadeInUp animated">
<div class="row">
<div id="summary_content">
{{view view.CvSummaryView}}
</div>
</div>
</div>
routes
In all my routes I'm only adding the model() function. I'm not overriding renderTemplate() or anything else.
define([
'Ember'
],
function (Ember) {
"use strict";
return Ember.Route.extend({
model: function()
{
var result = {};
$.ajax({
async: false,
dataType: "json",
url: './website/js/models/portfolio.json',
success: function(data){
result.portfolio = data;
}
});
return result;
}
});
}
);
I tried the following with no luck:
renderTemplate: function(){
this.render({
outlet: "main",
into: "application"
});
}
Do you have any ideas about what can be the root cause of this issue?
The entire app source code can be found at https://github.com/remojansen/remojansen.github.io/tree/master/website/js
UPDATE 1
I've been reading the Ember documentation and I added {{outlet "main"}} into my application template and tried with:
renderTemplate: function() {
this.render('blog', { // the template to render
into: 'application', // the template to render into
outlet: 'main' // the name of the outlet in that template
});
}
The I've been debugging the Ember code and I reached this function:
function appendView(route, view, options) {
if (options.into) {
var parentView = route.router._lookupActiveView(options.into);
var teardownOutletView = generateOutletTeardown(parentView, options.outlet);
if (!route.teardownOutletViews) { route.teardownOutletViews = []; }
replace(route.teardownOutletViews, 0, 0, [teardownOutletView]);
parentView.connectOutlet(options.outlet, view);
} else {
var rootElement = get(route.router, 'namespace.rootElement');
// tear down view if one is already rendered
if (route.teardownTopLevelView) {
route.teardownTopLevelView();
}
route.router._connectActiveView(options.name, view);
route.teardownTopLevelView = generateTopLevelTeardown(view);
view.appendTo(rootElement);
}
}
In the function above, in the line:
var parentView = route.router._lookupActiveView(options.into);
The variable parentView is null and options.into is "application". So the line below throws an exception:
parentView.connectOutlet(options.outlet, view);
I have defined the application template and view but not an application route I don't know if that could be the problem.
Thanks!
After some time debugging I noticed that the ember router._activeViews element didn't always contain the application view:
Works
Doesn't work
I tried to analyse why was this happening and because as I said in the question:
The weird thing is that if I refresh the pages many times sometimes it
works and some times it's doesn't so feels like some file is loaded
too late or maybe loaded twice.
I was almost sure that is was related with the usage of require.js and loading application components asynchronously.
The solution was use deferReadiness() and advanceReadiness(). Here is what I did in case it can help somebody in the future...
app.js
define(['Ember'], function (Ember) {
"use strict";
window.app = Ember.Application.create({
LOG_TRANSITIONS: false, // basic logging of successful transitions
LOG_TRANSITIONS_INTERNAL: false, // detailed logging of all routing steps
LOG_VIEW_LOOKUPS: false // detailed logging of view resolution
});
// Delay the app's initialization . We will invoke advanceReadiness()
// when are ready for the app to be initialized
window.app.deferReadiness();
return window.app;
});
main.js
require([
'website/js/app',
/* routes, views... */
], function (
app,
/* routes, views... */
){
"use strict";
// Configure Routes
app.Router.map(routes);
// Set Routes
app.IndexRoute = indexRoute;
// ...
// Set Views
app.IndexView = indexView;
// ...
// We're ready to launch the app!
app.advanceReadiness();
});

marionette region undefined following init

The error ONLY occurs under a special routing circumstance when the layout (see gist link) is called by the router following a login processed in a different controller and routed here by the global event mechanism.
Everything is fine as long as an existing session is reused and there is NO Logon processed.
main code in this gist ( error at line #113 "this.headerRegion" not defined
Code blocks coming to the gist module , from logon and from router...
loginSuccess: function (user) {
vent.trigger('user:login'); //notify all widgets we have logged in
app.router.navigate('home', { trigger: true });
}
...
return marionette.AppRouter.extend({
routes: {
'home' : 'home',
...
},
home: function() {
this._showPage('home');
},
...
_showPage: function (pageName, options) {
console.log("RouterShow " +pageName);
var that = this;
//make sure we are authenicated, if not go to login
if (!Parse.User.current())
pageName = 'login';
require(['./pages/' + pageName + '/Controller'], function (PageController) {
if (that.currentPageController) {
that.currentPageController.close();
that.currentPageController = null;
}
// line below loads the layout in the gist link
that.currentPageController = new PageController(options);
that.currentPageController.show(app.regionMain)
.fail(function () {
//display the not found page
that.navigate('/not-found', { trigger: true, replace: true });
});
});
}
...
define([
'marionette',
'./Layout',
'app'
], function (
Marionette,
Layout,
app
) {
'use strict';
return Marionette.Controller.extend({
show: function (region) {
var that = this, d = new Marionette.$.Deferred();
region.show(new Layout({ })); //this layout in gist link
return d.promise();
}
});
});
pageController.show() near the end of the above block calls the Layout region in the gist
To recreate the error that ONLY OCCURs following a logon, I do the following:
Show compoundView #1 at line #57 of the gist.
click in compound view #1 firing event at line #40 of the gist ('roleList:getuser',)
swap new views #2 into EXISTING regions used for the first views at lines #113, 114
ERROR at 113, "this.headerRegion" no longer exists in the layout!
Discussion - now IMO Layout extends Marionett.ItemView and from the source, it should always have the regions defined before calling init. The constructor checks for undef "this.headerRegion" at line #23 of the gist.
My code reimplements the superclass constructor in lines 18 - 23 of the gist and it looks like "headerRegion" and "mainRegion" attributes are always defined. But, the error is :
Uncaught TypeError: Cannot call method 'show' of undefined
Layout.js:113 Marionette.Layout.extend.getUserRelation
The region properties like 'headerRegion' in your case are only available after render(), not just initialize(). From the Marionette docs:
Once you've rendered the layout, you now have direct access to all of
the specified regions as region managers.
You must be triggering those events ('roleItem:getrole', etc.) before the layout is rendered in logic outside the gist. Instead you'll need to render first if you want to implement getUserRelation() in this way.

backbone.js, handlebars error : this._input.match is not a function

I'm new to backbone.js and handlebars and I'm having a problem getting my template to render out the data.
Here is my collection and model data from tagfeed.js module:
// Create a new module.
var Tagfeed = app.module();
// Default model.
Tagfeed.Model = Backbone.Model.extend({
defaults : {
name : '',
image : ''
}
});
// Default collection.
Tagfeed.Collection = Backbone.Collection.extend({
model : Tagfeed.Model,
url : Api_get('api/call')
});
Tagfeed.TagView = Backbone.LayoutView.extend({
template: "tagfeed/feed",
initialize: function() {
this.model.bind("change", this.render, this);
},
render: function(template, context) {
return Handlebars.compile(template)(context);
}
});
Then in my router I have:
define([
// Application.
"app",
// Attach some modules
"modules/tagfeed"
],
function(app, Tagfeed) {
// Defining the application router, you can attach sub routers here.
var Router = Backbone.Router.extend({
routes: {
"index.html": "index"
},
index: function() {
var collection = new Tagfeed.Collection();
app.useLayout('main', {
views: {
".feed": new Tagfeed.TagView({
collection: collection,
model: Tagfeed.Model,
render: function(template, context) {
return Handlebars.compile(template)(context);
}
})
}
});
}
});
return Router;
});
THis successfully makes a call to the api, makes a call to get my main template, and makes the call to get the feed template HTML. If I don't include that render(template, context) function, then it renders on the page as the straight up HTML that I have in the feed template with the {{ name }} still included. however when its included, I get the error
TypeError: this._input.match is not a function
[Break On This Error]
match = this._input.match(this.rules[rules[i]]);
and if I examine the variables that get passed into the appLayout views render function for feed, I see that the template var is a function, and the context var is undefined, then it throws that error.
Any ideas what I'm doing wrong? I know I have at least one problem here, probably more.
Since you're using requirejs, you can use the text module to externalise your templates or better still pre-compile them and include them in your view. Check out http://berzniz.com/post/24743062344/handling-handlebars-js-like-a-pro
E.g. using pre-compiled templates
// router.js
define(['views/tag_feed', 'templates/feed'], function(TagFeedView) {
var AppRouter = Backbone.Router.extend({
// ...
});
})
// tag_feed.js
define(['collections/tag_feed'], function() {
return Backbone.View.extend({
// ...
render: function() {
this.$el.html(
Handlebars.templates.feed({
name: '...'
})
);
}
});
})
For reference I've created simple boilerplate for a backbone/require/handlebars setup https://github.com/nec286/backbone-requirejs-handlebars

using Backbone JS boilerplate & code navigation

a newbe question:
I've downloaded the backbone boilerplate from https://github.com/david0178418/BackboneJS-AMD-Boilerplate it uses require.js and I wonder about the code navigation during development.
Here is my question:
let's say I have 2 views one extend the other like so:
View 1:
define([
'underscoreLoader',
'backboneLoader',
'text!templates/main.html'
],
function (_, Backbone, MainTemplate) {
"use strict";
return Backbone.View.extend({
template:_.template(MainTemplate),
initialize:function () {
this.render();
},
log:function(msg){
console.log(msg);
},
render:function () {
this.$el.append(this.template({}));
return this;
}
});
}
);
View 2:
define([
'underscoreLoader',
'backboneLoader',
'text!templates/other.html',
'views/main-view'
],
function (_, Backbone, MainTemplate,MainView) {
"use strict";
// how would you navigate to MainView (main-view.js)
return MainView.extend({
template:_.template(MainTemplate),
initialize:function () {
this.render();
},
render:function () {
this.log("my message");
this.$el.append(this.template({}));
return this;
}
});
}
);
Now when I develop (I use IntelliJ) I would like to middle click MainView on the extended view and navigate to the code without having to browse the project tree.
Is that possible using this boilerplate? is there a better approach or a better boilerplate?
I would really like Netbeans's navigator to show me all the methods:
var New_Controller = Backbone.View.extend({
el : null, ...
}
But I can't seem to get it to work. Google came up with something for #lends, but I can't even get Backbone.js to get loaded to the code hint cache.
I ended up installing WebStorm (I saw the IDE in all the egghead.io tutorials) to get the navigator to list all methods and properties.
FYI, Aptana Studio and Zend Studio showed nothing like Netbeans. And Eclipse IDE for JavaScript Web Developers only partially (impractical in real life) works; it flattens the entire hierarchy.
I found this to work fine for me:
the Backbone Objects are wrapped with my custom objects, which allows me to navigate code, extend objects and keep multiple files easily.
Here is how:
Object 1
function ItemModel() {
ItemModel.model = (
Backbone.Model.extend({
initialize:function () {
},
defaults:{
name:"item name"
},
log:function(){
console.log("inherited method");
}
})
);
return new ItemModel.model();
}
Object 2
function ItemModel2() {
ItemModel2.model = (
ItemModel.model.extend({
initialize:function () {
},
defaults:{
name:"item name2"
}
})
);
return new ItemModel2.model();
}
and in my app:
var App = {
item:new ItemModel(),
item2:new ItemModel2()
};

Categories

Resources