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();
});
Related
I am new to Ember and trying to figure out how the data routing works. I have a 'page-notices' component and template thats included in my application.hbs file. It handles showing error or other notifications to users. I cannot figure out how to set the data inside the component from the application controller.
When a user triggers a logout action in application controller, I send it to the server via ajax json request and then need to update the page-notices component if there was an error returned. What does the page-notices controller need to look like to get this done? Am I asking the wrong question and shouldn't be using the controller for this?
//app/templates/application.hbs
{{app-header}}
<div id="pagecontent">
{{page-notices}}
<div id="wrapper">
{{outlet}}
<div class="push"></div>
</div>
</div>
{{app-footer}}
//app/controllers/application.js
import Ember from 'ember';
import ENV from '/config/environment';
var $ = Ember.$;
export default Ember.Controller.extend({
session: Ember.inject.service('session'),
pagenotices: Ember.inject.controller("page-notices")
actions: {
logout: function() {
var self = this;
$.ajax({
dataType: "json",
method: 'GET',
url: ENV.APP.apiHost,
data: {p: 'logout'},
success: function( response ){
if( response.success || (response.loggedin == false) ){
self.get('session').invalidate();
self.transitionToLoginRoute();
} else {
self.get('pagenotices').set('pageerrors', response.error);
self.get('pagenotices').set('pageerrorsview', '');
}
}
});
},
},
transitionToLoginRoute: function() {
this.transitionToRoute('login');
},
});
//app/templates/components/page-notices.js
<div id="pagenotices" class="{{pagenoticeview}}">
<div id="pageerrors" class="error centered {{pageerrorsview}}">{{pageerrors}}</div>
<div id="pagemessages" class="notice centered {{pagemessagesview}}">{{pagemessages}}</div>
</div>
//app/components/page-notices.js
import Ember from 'ember';
import ENV from '/config/environment';
const { inject: { service }, Component } = Ember;
export default Component.extend({
pagenoticeview: 'hide',
pageerrors: '',
pageerrorsview: 'hide',
pagemessages: '',
pagemessagesview: 'hide',
});
I did not quite understand why you injected page-notices controller to application controller; because you have already put page-notices component directly to application.hbs. I might be wrong but I got the sense that you are confusing a controller and a component.
Anyway,the following should work.
Remove pagenotices: Ember.inject.controller("page-notices") this; since we have no work with pagenotices controller as I explained above.
Change the else part in ajax handler of logout action within application.js as follows:
self.set('pageerrors', response.error);
self.set('pageerrorsview', '');
So that the corresponding attributes are directly saved to application controller itself.
Pass the corresponding attributes to page-notices component from within application.hbs with
{{page-notices pageerrors=pageerrors pageerrorsview=pageerrorsview}}
Declare the initial values of pageerrors and pageerorsview within application.js and remove them from page-notices component if you want. I mean declarations of pageerrors: '', pageerrorsview: 'hide'
This should work if I got what you are asking right, best regards.
I have been wracking my brain and the internet and have not found an answer to this issue. Setup a site using Hot towelette, upgraded it to durandal 2.0 and all the associated packages. Followed the guide for upgrading from 1.0 to 2.0 on Durandal's site and have everything working except the routes. The default route works and the site loads the default page as expected. However clicking on the navs on the top of the page, the url changes but nothing happens. The url changes to http://host/#view. I saw the missing slash after the hash and added the hash to the route to fix that, but still does not change the view. I've tried many different samples etc. but haven't found what the problem is.
main.js
require.config({
paths: {
"text": "../Scripts/text",
"durandal": "../Scripts/durandal",
"plugins": "../Scripts/durandal/plugins",
"transitions": "../Scripts/durandal/transitions",
}
});
define('jquery', [], function() { return jQuery; });
define('knockout', [], function () { return ko; });
define(['durandal/system', 'durandal/app', 'durandal/viewLocator', 'plugins/router', 'services/logger'],
function (system, app, viewLocator, router, logger) {
app.title = "Remedy Approvals";
app.configurePlugins({
router: true
})
// Enable debug message to show in the console
system.debug(true);
app.start().then(function () {
toastr.options.positionClass = 'toast-bottom-right';
toastr.options.backgroundpositionClass = 'toast-bottom-right';
router.handleInvalidRoute = function (route, params) {
logger.logError('No Route Found', route, 'main', true);
};
// When finding a viewmodel module, replace the viewmodel string
// with view to find it partner view.
//router.makeRelative({ moduleId: 'viewmodels' });
viewLocator.useConvention();
// Adapt to touch devices
//app.adaptToDevice();
//Show the app by setting the root view model for our application.
app.setRoot('viewmodels/shell', 'entrance');
});
});
shell.js:
define(['durandal/system', 'plugins/router', 'services/logger'],
function (system, router, logger) {
var shell = {
router: router,
activate: activate
};
return shell;
function activate() {
var routes = [
{ route: ['approvals',''], moduleId: 'approvals', title: 'My Approvals', nav: true, hash: '#/approvals' },
{ route: 'alternate', moduleId: 'alternate', title: 'Alternate Approvals', nav: true, hash: '#/alternate' }
];
return router.makeRelative({ moduleId: 'viewmodels' })
.map(routes)
.buildNavigationModel()
.mapUnknownRoutes('approvals', 'not-found')
.activate();
}
// function log(msg, data, showToast) {
// logger.log(msg, data, system.getModuleId(shell), showToast);
// }
}
)
shell.html
<div>
<header>
<!--ko compose: {view: 'nav'} --><!--/ko-->
</header>
<section id="content" class="main container-fluid">
<!--ko compose: {model: router.activeItem,
afterCompose: router.afterCompose,
transition: 'entrance'} -->
<!--/ko-->
</section>
<footer>
<!--ko compose: {view: 'footer'} --><!--/ko-->
</footer>
</div>
nav.html
<div>
<ul class="nav nav-pills" data-bind="foreach: router.navigationModel">
<li role="presentation" data-bind="css: {active: isActive}"><a data-bind="attr: {href: hash}, html: title"></a></li>
</ul>
<label class="pull-right">username</label>
</div>
You're using the compose binding on the activeItem property of the router. I don't know if that's "wrong" as such, but I'm using the "router" binding for mine:
<div class="container page-host" data-bind="router"></div>
Excerpt from http://durandaljs.com/get-started.html:
At the bottom of the view is a special router binding which connects
to our router in the module. This serves as a placeholder where the
current "page" will be displayed. The transition property indicates
that whenever the page changes, Durandal should use the "entrance"
transition animation.
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.
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.
When you want to use classes you created in Em.Application.create() in your router you need to specify the router outside of the application.create. But because the application is automatically initialized the router doesn't route to the / route.
You used to be able to defer the initialization by adding autoinit: false to the application.create. Now you are supposed to use App.deferReadiness() and App.advanceReadiness(). However this doesn't appear to work.
And I can't seem to escape the feeling that you are "supposed" to do it differently.
I added the minimal code to show the problem below. There is also a jsfiddle here
EDIT:
Apparently there is a new router in ember I kinda sorta overlooked that. I've changed the code to the new router, but guess what it still doesn't work :P
window.App = App = Em.Application.create({
ApplicationController: Em.Controller.extend({}),
ApplicationView: Em.View.extend({
template: Em.Handlebars.compile('{{outlet}}'),
}),
ExtendedPatientController: Em.ObjectController.extend({}),
ExtendedPatientView: Em.View.extend({
classNames: ['patient-view', 'extended'],
template: Em.Handlebars.compile('{{name}}')
}),
Patient: Em.Object.extend({
name: undefined,
}),
});
App.Router.map(function (match) {
match('/').to('application', function (match) {
match('/').to('extendedPatient');
})
});
App.deferReadiness();
App.ExtendedPatientRoute = Em.Route.extend({
setupController: function (controller) {
controller.set('', App.Patient.create({
name: "Bert"
}));
},
renderTemplates: function () {
this.render('extendedPatient', {
into: 'application'
});
}
});
App.advanceReadiness();
You're actually doing a lot more work than you need to here.
Here's all the code that you need to make your example work.
Template:
<script type="text/x-handlebars" data-template-name="index">
<div class="patient-view extended">
<p>Name: {{name}}</p>
</div>
</script>
App:
window.App = Em.Application.create();
App.Patient = Em.Object.extend({
name: null
});
App.IndexRoute = Em.Route.extend({
model: function() {
return App.Patient.create({
name: "Bert"
});
}
});
The working fiddle is at: http://jsfiddle.net/NXA2S/23/
Let me explain it a bit:
When you go to /, you are entering the automatic index route. All you need to do to show something on the screen for that route is to implement an index template. The easiest way to do that when you're getting up and running is to put your template in your index.html. Later, you will probably want to use build tools (see my answer here for more information).
You can control what model is displayed in a route's template by overriding the model hook in its route handler. In the case of index, the route handler is App.IndexRoute. In this case, the model is a brand new App.Patient.
You will probably want to implement controllers and events. You can learn more about the router on the Ember.js website
So the new router does solve this problem and does feel a bit shinier.
I finaly found out how to do this basic example this is what happens in the router:
App.Router.map(function (match) {
match('/').to('extendedPatient');
});
This what needs to happen in the views:
ExtendedPatientView: Em.View.extend({
classNames: ['patient-view', 'extended'],
//You need to specify the defaultTemplate because you extend the view class
//instead on initializing it.
defaultTemplate: Em.Handlebars.compile('{{name}}')
}),
You do not have to defer the readiness in the app the new router fixes that.
And in the route you do not need to specify the renderTemplates so the router now looks like:
App.ExtendedPatientRoute = Em.Route.extend({
setupController: function (controller) {
controller.set('content', App.Patient.create({
name: "Bert"
}));
},
});
http://jsfiddle.net/NXA2S/28/