I have a very simple app showing a login screen and a dashboard. When the login is successful the app navigates to the dashboard.
In this state, a back navigation (android) should not go back to the login screen.
This all works fine using the IonicHistory
this.$ionicHistory.nextViewOptions({
disableBack: true
});
return this.$state.go("dashboard");
This is all done in my LoginCtrl when the SessionService (which does the login call to the service and the Session management...) resolves the returned promise.
Now I want to write a Unit-Test to check this behaviour and make sure, never every removes this nextViewOptions-call.
My idea is something like this:
beforeEach(() => {
angular.mock.module("ionic");
angular.mock.module("myapp");
inject(function ($q: IQService, $rootScope: IScope, $ionicHistory: IonicHistoryService): void {
qService = $q;
scope = $rootScope;
ionicHistory = $ionicHistory;
});
});
describe("login", () => {
// Stub my sessionservice to immediatly resolve instead of doing a request
sinon.stub(sessionService, "login", () => qService.when());
let loginCtrl: LoginCtrl = new LoginCtrl(sessionService, stateService, ionicHistory);
loginCtrl.executeLogin();
scope.$apply();
// Here I want to validate, that the history (and therefore the back-stack) is correct.
// But the ionicHistory.viewHistory() does not change at all.
// Independent whether I add the nextViewOptions stuff or not...
}
Here the output from ionichHistory.viewHistory().histories:
Object{root: Object{historyId: 'root', parentHistoryId: null, stack: [], cursor: -1}}
I use the karma test runner to execute the tests.
I'm using:
Ionic 1.3.0
Karama Test Runner
TypeScript
Is this actually possible at all? Do I miss something completely?
Related
Suppose there is a size several link. Every link click is handled by controller. Consider the situation:
User visit some page. Let say that it is /search where user inputs keywords and press search button.
A background process started (waitint for search response in our case)
user goes to another link
after some time user goes back to fisrt page (/search)
At the point 4 angulajs load page as user goes to it at first time. How to make angulajs remeber not state but process? E.g. if process is not finished it shows progress bar, but if it finished it give data from process result and render new page. How to implement that?
Notes
I have found this but this is about just state without process saving.
I have found that but this is about run some process at background without managing results or process state (runing or finished)
You can use angularjs service to remember this "Process" of making an api call and getting the data from it .
here is a simple implementation.
the whole idea here is to create a angular service which will make an api call,
store the data aswell as the state of the data, so that it can be accessed from other modules of angularjs. note that since angularjs services are singleton that means all of their state will be preserved.
app.service('searchService', function() {
this.searchState={
loading: false,
data: null,
error: null
}
this.fetchSearchResults = function(key){
// call api methods to get response
// can be via callbacks or promise.
this.searchState.loading=true;
someMethodThatCallsApi(key)
.then(function(success){
this.searchState.loading=false;
this.searchState.data=success;
this.searchState.error=null
})
.catch(function(error){
this.searchState.loading=false;
this.searchState.data=null;
this.searchState.error=error
});
}
this.getState = function(){
return this.searchState
}
});
// in your controller
app.controller('searchController',function(searchService){
// in your initialization function call the service method.
var searchState = searchService.getState();
// search state has your loading variables. you can easily check
// and procede with the logic.
searchState.loading // will have loading state
searchState.data // will have data
searchState.error // will have error if occured.
});
Even if you navigate from pages. the angular service will preserve the state and you can get the same data from anywhere in the application. you simply have to inject the service and call the getter method.
Based on the question, (a little bit more context or code would help answers be more targeted), when considering async operations within angularJS, its always advisable to use getters and setters within service to avoid multiple REST calls.
Please note - Services are singletons, controller is not.
for eg:
angular.module('app', [])
.controller('ctrlname', ['$scope', 'myService', function($scope, myService){
myService.updateVisitCount();
$scope.valueFromRestCall = myService.myGetterFunctionAssociated(param1, param2);
//Use $scope.valueFromRestCall to your convinience.
}]
.service('myService', ['$http', function($http){
var self = this;
self.numberOfVisits = 0;
self.cachedResponse = null;
self.updateVisitCount = function(){
self.numberOfVisits+=1;
}
self.myGetterFunctionAssociated = function(param1, param2){
if self.cachedResponse === null || self.numberOfVisits === 0 {
return $http.get(url).then(function(response){
self.cachedResponse = response;
return response;
});
}
else {
return self.cachedResponse;
}
}
return {
updateVisitCount: function(){
self.udpateVisitCount();
},
myGetterFunctionAssociated : function(param1, param2){
return self.myGetterFunctionAssociated(param1, param2);
}
}
}]
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.
Iam having problems with angular $timeout. It works ok If I wont hit F5/reload page. When I reload the page the timeout stops and never calls the function I provided in timeout. I feel like the code is good but Iam missing something. Why the timeout is never hit when I refresh the page?
export class AccountController
{
constructor(private $scope: ControllerScope, private $location: ng.ILocationService, private AuthService: AuthService, private $timeout: ng.ITimeoutService)
{
}
public Login(model: LoginModel)
{
this.AuthService.login(model).then((response: any) =>
{
//when token will expire -> logout
this.$timeout(() =>
{
this.Logout();
}, <number>response.TokenExpireTime).then((d) =>
{
alert('Czas trwania sesji wygasł');
});
this.$location.path('/');
},
(err) =>
{
alert(err.error_description);
});
}
public Logout()
{
this.AuthService.logOut();
this.$location.path("/login");
}
}
Well, assuming that you call LOGIN only once user clicks login button, here's the explanation.
When you register $timeout, it will be removed once you refresh your browser. This is standard behaviour and just the way it all works in browser environment.
That is - on a new page refresh, when user is already logged in, you no longer call login method. It means that you don't register your $timeout once again and after refresh, browser has no idea that you wanted to use it.
One solution might be to change that code to register $timeout on every page load but taking further look at your code, you can set TokenExpireTime as a variable inside e.g localStorage and use it in somewhere else.
Example assuming localStorage['expires'] is set.
if (localStorage['expires']) {
$timeout(function() {
// do your thing, if you are not going to update model, keep 3rd parameter false,
// otherwise skip it to prevent $digest cycle from running
}, localStorage['expires'], false);
}
This is similar to bank sessions that logs you out after 15 minutes no matter what are you doing.
If you want to reset timeout when user goes to a different page or just interacts with your app, you need to make that logic slightly more advanced by adding e.g. focus events.
$(window).on('focusin', function() {
// clear timeout, user is back in your app
});
$(window).on('focusout', function() {
// register timeout, user is away
});
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.
I'm using AngularFire in a multiplayer game and it sure looks like AngularFire is deleting my objects after the first load of a controller.
I'm using browserify to concatenate my JS files together so all my modules look like CommonJS modules.
My controllers are all loaded via ngViews and defined routes. I'm trying to keep knowledge of what user objects in Firebase look like confined to a user service; hence, all AngularFire invocations for the user object live in the service.
Here's my HomeController:
module.exports = function(ngModule) {
ngModule.controller('HomeController',
function($scope, user) {
if (!$scope.user) {
return;
}
user.getInvitations($scope);
user.getGames($scope);
});
};
I'm using AngularFire's auth from an auth service I defined. When the user object comes in, it is stored on the $rootScope. Here's a snippet of my auth service:
ngModule.run(function(angularFireAuth, warbase, $rootScope) {
// Here's where Firebase does all the work.
angularFireAuth.initialize(warbase, {
scope: $rootScope,
name: 'user',
path: '/login'
});
});
My user service uses a gotUser promise that is resolved when the user is present on the $rootScope. Here's a snippet of my user service:
$rootScope.$watch('user', function() {
if (!$rootScope.user) {
return;
}
userRef = warUsers.child(getPlayerId());
userInvitationsRef = userRef.child('invitations');
userGamesRef = userRef.child('games');
gotUser.resolve();
});
// ...
getInvitations: function(scope) {
scope.invitations = [];
gotUser.promise.then(function() {
angularFire(userInvitationsRef, scope, 'invitations');
});
},
getGames: function(scope) {
scope.games = [];
gotUser.promise.then(function() {
angularFire(userGamesRef, scope, 'games');
});
}
When my HomeController is loaded by a user with no games and no invitations everything works as expected. The user creates a game and it shows up on the home screen.
If I then reload the page the user's game objects (from path /users/:userId/games/) are cleared. There is no code to remove these values in my application at the moment.
I thought there might be a reference to AngularFire hanging around and syncing the blank value I set in the user service, so I removed the scope.games = []; line. If I don't set the initial value on the scope in my service I get this error in the console: Uncaught Error: Firebase.set failed: First argument contains undefined.
I'm guessing this has something to do with my unfamiliarity with controller lifecycles under ngView, my mis-use of services to DRY up my Firebase/AngularFire references, and general AngularJS newbishness all rolled into a galloping ball of fail, but I'd appreciate any pointers anyone can provide.