New to ember.js. What I'm trying to do is: create a transitional route that has no path, that I can pass an AJAX promise to as the model when I transition to it, and then it makes a redirect decision once the promise completes. I want the LoadingRoute view to be invoked while this is happening. I've tried to accomplish that with the following route:
App.LoginUserRoute = Ember.Route.extend({
setupController: function(controller, model) {
//Model should be a promise
model.then(this.success.bind(this),this.failure.bind(this));
},
success: function (data) {
if (data.success) {
App.loggedInUser = App.User.create(data);
console.log(App.loggedInUser);
//Make redirection decision
}
else {
alert(data.error);
}
},
failure: function () {
//Failure code
}
});
However, when I try to pass the promise to the route like follows:
var request = $.post("api.php", {action: 'create_user', username: this.username, password: this.password});
this.transitionToRoute('loginUser',request);
I get the error "More context objects were passed than there are dynamic segments for the route: loginUser" because I'm trying to create a pathless route and Ember requires that the model be serialized into the path when passed using transitionToRoute().
The reason I need this is:
The login event can happen from multiple controllers (when the user registers, when they login using the login screen, or when the application first loads if the right cookie is detected) and I don't want to duplicate the login logic across all those controllers.
After the login completes, there's multiple different routes the user could then be directed to, depending on the nature of the returned user object.
I want the LoadingRoute to be invoked while the request is completing.
I assume the solution to my problem is not to use routing, but rather something else. However, I'm not sure what the "something else" would be.
You'll want to take advantage of a mixin, and hooking into the transition route.
The following SO answer will work for all of your needs:
Ember Client Side Authentication, route authentication
In the end I achieved what I was trying to do by adding the following code to ApplicationController
loginUser: function (request) {
this.transitionToRoute('loading');
request.then(this.loginRequestSuccess.bind(this),this.loginRequestFailure.bind(this));
},
loginRequestSuccess: function (data) {
if (data.success) {
App.loggedInUser = App.User.create(data.user);
console.log(App.loggedInUser);
//Route transition decision
}
else {
alert(data.error);
}
},
loginRequestFailure: function () {
//Failure code
}
And then calling the loginUser() function with the jqXHR object from wherever in the code a login request was made.
Related
I have loginForm service, which takes care of my login form behaviour. For example there is default function for what to do when the login was successful, but let's say that In deferent places I want the login form to behave differently.
I wrote it like this, but it's not the proper way I think. Should I write it differently use for example Angulars (extend) function?
angular.module('flapperNews').factory('loginForm', ['Auth', function(Auth) {
function defaultSuccess() {
alert('succes');
}
function defaultFail() {
alert('fail');
}
var service = {
fields: function() {
return fields;
},
submitForm: function(loginInfo, afterSuccess, afterFail) {
// Test if login ifnormation are right
Auth.login(loginInfo).then(function(user) {
// If new after success function is provided use it, if not use default
(afterSuccess > 0) ? afterSuccess() : defaultSuccess();
}, function(errorData) {
// If new after success function is provided use it, if not use default
(afterSuccess > 0) ? afterFail() : defaultFail();
});
},
};
return service;
}]);
I can come up with 3 possible solutions that would all make sense in a cetain scenario:
Callbacks (what you have done)
Broadcast events on $rootScope and let the controllers handle themselves
Pass in a argument that decideds what part of logic is supposed to be used / run and put the logic into the service.
In an login scenario id broadcast a event and let controllers handle the event.
EDIT: Here is the github repo. And you can test the site here.
On the homepage, just open the browser console and you will notice that WaitOn and data are being run twice. When there is no WaitOn, then the data just runs once.
I have setup my pages by extending RouteController and further extending these controllers. For example:
ProfileController = RouteController.extend({
layoutTemplate: 'UserProfileLayout',
yieldTemplates: {
'navBarMain': {to: 'navBarMain'},
'userNav': {to: 'topUserNav'},
'profileNav': {to: 'sideProfileNav'}
},
// Authentication
onBeforeAction: function() {
if(_.isNull(Meteor.user())){
Router.go(Router.path('login'));
} else {
this.next();
}
}
});
ProfileVerificationsController = ProfileController.extend({
waitOn: function() {
console.log("from controller waitOn");
return Meteor.subscribe('userProfileVerification');
},
data: function() {
// If current user has verified email
console.log("from controller data start");
var verifiedEmail = Meteor.user().emails && Meteor.user().emails[0].verified ? Meteor.user().emails[0].address : '';
var verifiedPhoneNumber = Meteor.user().customVerifications.phoneNumber && Meteor.user().customVerifications.phoneNumber.verified ? Meteor.user().customVerifications.phoneNumber.number : '';
var data = {
verifiedEmail: verifiedEmail,
verifiedPhoneNumber: verifiedPhoneNumber
};
console.log("from controller data end");
return data;
}
});
On observing the console in the client, it seems the hooks are being run 2-3 times. And I also get an error on one of the times because the data is not available. The following is the console on just requesting the page once:
from controller waitOn
profileController.js?966260fd6629d154e38c4d5ad2f98af425311b71:44 from controller data start
debug.js:41 Exception from Tracker recompute function: Cannot read property 'phoneNumber' of undefined
TypeError: Cannot read property 'phoneNumber' of undefined
at ProfileController.extend.data (http://localhost:3000/lib/router/profileController.js?966260fd6629d154e38c4d5ad2f98af425311b71:46:62)
at bindData [as _data] (http://localhost:3000/packages/iron_controller.js?b02790701804563eafedb2e68c602154983ade06:226:50)
at DynamicTemplate.data (http://localhost:3000/packages/iron_dynamic-template.js?d425554c9847e4a80567f8ca55719cd6ae3f2722:219:50)
at http://localhost:3000/packages/iron_dynamic-template.js?d425554c9847e4a80567f8ca55719cd6ae3f2722:252:25
at null.<anonymous> (http://localhost:3000/packages/blaze.js?efa68f65e67544b5a05509804bf97e2c91ce75eb:2445:26)
at http://localhost:3000/packages/blaze.js?efa68f65e67544b5a05509804bf97e2c91ce75eb:1808:16
at Object.Blaze._withCurrentView (http://localhost:3000/packages/blaze.js?efa68f65e67544b5a05509804bf97e2c91ce75eb:2043:12)
at viewAutorun (http://localhost:3000/packages/blaze.js?efa68f65e67544b5a05509804bf97e2c91ce75eb:1807:18)
at Tracker.Computation._compute (http://localhost:3000/packages/tracker.js?517c8fe8ed6408951a30941e64a5383a7174bcfa:296:36)
at Tracker.Computation._recompute (http://localhost:3000/packages/tracker.js?517c8fe8ed6408951a30941e64a5383a7174bcfa:310:14)
from controller data start
from controller data end
from controller waitOn
from controller data start
from controller data end
Have I not used the controllers properly?
Without being able to see the rest of the code that you have defined that uses these route controllers (such as templates or route definitions), I cannot accurately speak to the reason for the data function being called multiple times. I suspect that you may be using the ProfileVerificationsController with multiple routes, in which case the data definition for this controller would be executed multiple times, one for each route that uses the controller. Since the data definition is reactive, as you browse through your application and data changes, this might be resulting in the code defined to be rerun.
As for your controller definitions, I would suggest making a few modifications to make the code more robust and bulletproof. First, the ProfileController definition:
ProfileController = RouteController.extend({
layoutTemplate: 'UserProfileLayout',
yieldRegions: {
'navBarMain': {to: 'navBarMain'},
'userNav': {to: 'topUserNav'},
'profileNav': {to: 'sideProfileNav'}
},
onBeforeAction: function() {
if(!Meteor.user()) {
Router.go(Router.path('login'));
this.redirect('login'); // Could do this as well
this.render('login'); // And possibly this is necessary
} else {
this.next();
}
}
});
Notice the first thing that I changed, yieldTemplates to yieldRegions. This typo would prevent the regions from your templates using this route controller to be properly filled with the desired subtemplates. Second, in the onBeforeAction definition, I would suggest checking not only whether or not the Meteor.user() object is null using Underscore, but also checking for whether or not it is undefined as well. The modification that I made will allow you to check both states of the Meteor.user() object. Finally, not so much a typo correction as an alternative suggestion for directing the user to the login route, you could use the this.redirect() and this.render() functions instead of the Router.go() function. For additional information on all available options that can be defined for a route/route controller, check this out.
Now for the ProfileVerificationsController definition:
ProfileVerificationsController = ProfileController.extend({
waitOn: function() {
return Meteor.subscribe('userProfileVerification');
},
data: function() {
if(this.ready()) {
var verifiedEmail = Meteor.user().emails && Meteor.user().emails[0].verified ? Meteor.user().emails[0].address : '';
var verifiedPhoneNumber = Meteor.user().customVerifications.phoneNumber && Meteor.user().customVerifications.phoneNumber.verified ? Meteor.user().customVerifications.phoneNumber.number : '';
var data = {
verifiedEmail: verifiedEmail,
verifiedPhoneNumber: verifiedPhoneNumber
};
return data;
}
}
});
Notice the one thing that I changed, which is to wrap all of your code defined in the data option for your controller with a if(this.ready()){}. This is critical when using the waitOn option because the waitOn option adds one or more subscription handles to a wait list for the route and the this.ready() check returns true only when all of the handles in the wait list are ready. Making sure to use this check will prevent any cases of data unexpectedly not being loaded yet when you are building up your data context for the route. For additional information on defining subscriptions for your routes/route controllers, check this out.
As a final suggestion, for your onBeforeAction option definition in your ProfileController, I would suggest moving this out into its own global hook like so:
Router.onBeforeAction(function() {
if(!Meteor.user()) {
Router.go(Router.path('login'));
} else {
this.next();
}
});
Defining this check in the global hook ensures that you don't have to worry about adding your ProfileController to all of your routes just to make sure that this check is run for all of them. The check will be run for every route every time that one is accessed. Just a suggestion, though, as you may have reasons for not doing this. I just wanted to suggest it since I make sure to do it for every Meteor app that I develop for additional security.
I may be using a totally incorrect approach for my problem and if so please tell me
My Meteor app collects emails address and emails them a link to a download page with a token. This download page is a Iron Router route and the token is the ID of an item in a collection. The token is checked for prior use and then a download will be initiated [that part not written yet]. So I have this route:
this.route('download', {
path: '/download/:_id',
template: 'capture_download',
waitOn: function () {
return Meteor.subscribe('captures');
},
data: function() { return Captures.findOne(this.params._id); }
});
So I need to trigger a call to my server method that does the checking logic as soon as this route is loaded. And I need the ID value to make that call. So I have this:
Template.capture_download.rendered = function(template) {
Meteor.call('claimDownload', this.data._id, function(err, result) {
// callback logic here
});
}
What I don't understand is that this only sometimes works. Sometimes the call happens with the ID value correct. Other times I get:
Exception from Deps afterFlush function function: TypeError: Cannot read property '_id' of null
So I'm thinking that either my template event [rendered] is wrong [I can't find in the docs a list of template events anywhere], or that I need to do something to wait for a valid this value, or that my approach is totally wrong. How would I fix this occasional lack of data in the view when rendered?
Use onBeforeAction within your Iron Router route, rather than a rendered method in the template:
this.route('download', {
path: '/download/:_id',
template: 'capture_download',
waitOn: function () {
return Meteor.subscribe('captures');
},
data: function() { return Captures.findOne(this.params._id); },
onBeforeAction: function() {
Meteor.call('claimDownload', this.params._id, function(err, result) {
// callback logic here
});
}
});
See https://github.com/EventedMind/iron-router/blob/dev/DOCS.md#before-and-after-hooks. Your “checking for token prior use” sounds a lot like the “checking that the user is logged in” example in the docs, which is solved with onBeforeAction.
I'm trying to write a handler for all failed routes in my Ember application.
The documentation here seems to suggest that I can make a ErrorRoute on my App object which will automatically be transitioned to when routing on another route failed. I want to use this to redirect the user to a login page if the reason for the routing failure is due to an authentication problem (such as token timeout).
The problem I have is that inside the ErrorRoute I don't seem to have any way to access the error returned by the route that failed. I want to check for sure that it was an authentication error before redirecting them to the login screen and I'm not sure how to do that.
Here is what I wrote for testing:
App.ErrorRoute = Ember.Route.extend({
activate: function() {
console.log("MODEL A", this.modelFor('error'));
setInterval(function() {
console.log("MODEL B", this.modelFor('error'));
}.bind(this), 1000);
}
});
When the route is activated, the console logs MODEL A undefined as it tries to access the model for the App.ErrorController which isn't set yet for some reason. After a second the MODEL B console log fires and the error model has been set up.
If I can't access the error in the Route's activate method then where can I access it? Presumably I'm not supposed to wrap my logic in a timeout.
You could manage the error in any route of your current active hierarchy.
Normally, you setup your error handler at the application route to perform your app error logic.
App.AccountRoute = Ember.Route.extend({
afterModel: function() {
return new Em.RSVP.Promise(function(resolve, reject){
throw new AuthenticatedError('error message');
});
}
});
App.ApplicationRoute = Ember.Route.extend({
actions: {
error: function(error) {
if ( error instanceof AuthenticatedError ) {
this.transitionTo('login');
} else {
// if return true, the event will bubble and transition to error
return true;
}
}
}
});
http://emberjs.jsbin.com/cisur/1/edit
The error model is passed to the error route's renderTemplate(controller, model) method.
If you don't care about landing on a *.error route substate, if you're going to redirect anyway, #ppcano's answer is sufficient. But sometimes you want to encapsulate all error handling in a dedicated error route object. renderTemplate then is your place to handle things.
I agree though, it would be nice if its hooks had the correct modelFor(...) available.
I've run into an annoying issue when loading data asynchronously in an ember route's model callback. The issue seems to be that if the model method of my route returns a promise which is rejected then the route will never attempt to re-evaluate that route model. It just automatically returns the same rejected promise the next time it tries to go to that route without even trying to re-fetch the data!
I understand from this answer that an ember route will only call it's model method when trying to convert the url into a model. I'm guessing that in the case of routes with dynamic segments it may be called if it has never encountered that particular dynamic segment before.
Here is what I've got in my router setup.
window.App = Ember.Application.create({
LOG_TRANSITIONS: true,
LOG_TRANSITIONS_INTERNAL: true
});
App.Router.map(function() {
this.route('login');
this.resource('users', { path: '/users' }, function() {
this.resource('user', { path: '/:user_id' });
this.route('create', { path: '/create' });
});
});
And this is my route.
App.UserRoute = Ember.Route.extend({
model: function(params) {
// This returns a promise
return App.User.fetch(params.user_id);
}
});
I have some special handling for errors in my application route so that routes which fail due to authentication exceptions redirect the user to the login screen.
App.ApplicationRoute = Ember.Route.extend({
actions: {
sessionExpired: function() {
this.controllerFor('login').set("tokenExpired", true);
this.transitionTo('login');
},
error: function(err) {
if (err.type === "TokenException") {
this.send('sessionExpired');
}
}
}
});
The Problem
I navigate to the /users route
For some reason my token expires (inactivity, whatever...)
I navigate to the /users/1 route
The route's model method returns a promise which rejects and I am kicked out to the login screen
I log back in and try to navigate back to the /users/1 route
The route automatically just returns the same failed promise it did last time and I'm kicked out to the login screen. :(
I'm thinking that what I want is some way to clear all the evaluated route models after a user logs in. If this was a multi-user system and one user logs out and another user logs in on the same computer without refreshing the page then that new user shouldn't have routes automatically resolved from the previous user's session.
This seems to me like it would be a common problem yet I can't find any sort of app-wide invalidate cache method. How should I solve this?
I'm not sure where ember data stands on the cache clearing feature, but here is one way to do it
clearCache: function (type) {
var map = App.store.typeMapFor(type);
map.idToCid = {};
map.clientIds = [];
map.recordArrays = [];
map.findAllCache = null;
}
And here is an example as to how the ember firebase library handles a fail find using cache clearing.
delete store.typeMapFor(store.modelFor('user')).idToRecord[username];
Full example here:
https://github.com/firebase/emberFire/blob/master/examples/blog/js/app.js
For anyone else who finds this - I never found a way to reset the ember application and cause it to forget all resolved routes. I did find a few other work-arounds.
In the end, I opted to just window.reload() any time that a user logged out of the system or had their authentication token expire.
Authenticated URLs
Another reasonable approach would be to put a random unique id in the hash state. Essentially just do this.
Instead of a route like:
#/contacts/1
prefix every authenticated route with some kind of unique id
#/PyUE4E+JEdOaDAMF6CwzAQ/contacts/1
App.reset
I tried tried a number of things. One of the more promising things I tried was redirecting to the login screen and using the Application's reset method on my global App object. http://emberjs.com/api/classes/Ember.Application.html#method_reset
That didn't work though, it seems that even a reset Application remember's the models of any routes that it has resolved - weird.