How to call an Angular method when browser closes - angularjs

I'm relatively new in AngularJS and I have been asked to modify our application so that when the user closes the browser, we also log them out of Auth0 service.
I have the code below but I can't get it to fire. Kindly help.
$rootScope.$on("$destroy", function() {
lockService.logout();
});
lockService.logout() holds the functionality that successfully logs out the user when they click logout button, including the Auth0 signout function.
Somewhere I read that I can use the $on.$destroy of the main Controller but I am not sure how to do this. The above code is in fact inside the mainController function but it still doesn't work.
This is where I found my answer: https://stackoverflow.com/a/36444134/1168597

You can add a directive like this to your body element.
<body body-unload></body>
app.directive('bodyUnload', [
function () {
return {
restrict: 'A',
replace: false,
link: function (scope, element) {
function cleanupApp(){
// do cleanup here
}
element[0].onbeforeunload = function() {
cleanupApp();
};
}
};
}
]);

I have found a much cleaner and more effective approach.
$rootScope.$on('$locationChangeStart', function () {
if (!($window.performance.navigation.type === 1) && lockService.isAuthenticated()) {
$rootScope.logout();
}
});
So here if window.performance.navigation.type is 1, it means the page was refresh-ed. What I do then is if it is not refreshed AND my lockService.isAuthenticated() returns true, I logout the user.

Related

AngularJS directive using a service as its scope

I have a simple view problem that I may have made more difficult by trying to create my first directive in Angular. I have this service that returns an array of user roles for the current user. Many users are only going to have the user role. With that in mind I don't want to display anything on their profile page. However, some people like myself will have User and Admin roles. So on the profile page I would like to display a drop down to change the current role.
I thought I'd create a directive that did a check basically
if user.roles.length != 1 then show drop down else remove the element from the dom.
Like I said before I have never created a directive before and they seem like the right way to go but I am not getting the result I would like.
I have this:
app.directive('selector', function (SessionState, $compile) {
return {
priority: 100000,
scope: false,
compile: function (element, attr, linker) {
var data = SessionState.User.Data.roles;
var multiRoleView = '<p>Active Role:</p><select ng-model="State.User.activeRole" ng-options="role for role in data"></select>';
if (data.length != 1) {
element.html(multiRoleView).show();
$compile(element.contents());
} else {
element.children.remove();
element.remove();
}
return function linkFn() {
/* Optional */
}
}
}
});
This will render the html correctly depending on if that user should see it, but for admins it doesn't display any roles because I am assuming my data variable is never being used.
I hate to post a bad question but with some fiddling around and reading the documentation for $compile I found that by setting the scope variable to true. I could simply use the parents scope and that made everything very easy.
The completed and correct code:
Live link: https://app.baileysproject.com
Set scope to true, uses parents scope.
Removed the element.children.remove(), throws errors.
app.directive('selector', function (SessionState, $compile) {
return {
priority: 100000,
scope: true,
compile: function (element, attr, linker) {
var multiRoleView = '<p>Active Role:</p><select ng-model="State.User.activeRole" ng-options="role for role in State.User.Data.roles"></select>';
if (data.length != 1) {
element.html(multiRoleView).show();
$compile(element.contents());
} else {
element.remove();
}
return function linkFn() {
/* Optional */
}
}
}
});

Implementing component require property in Angular 1.5 components

I am having no joy with implementing require: {} property as part of an angular component. Allow me to demonstrate with an example I have.
This is the component/directive that supposed to fetch a list of judgements. Nothing very fancy, just a simple factory call.
// judgements.component.js
function JudgementsController(GetJudgements) {
var ctrl = this;
ctrl.Get = function () {
GetJudgements.get().$promise.then(
function (data) {
ctrl.Judgements = data.judgements;
}, function (error) {
// show error message
});
}
ctrl.$onInit = function () {
ctrl.Get();
};
}
angular
.module('App')
//.component('cJudgements', {
// controller: JudgementsController,
//});
.directive('cJudgements', function () {
return {
scope: true,
controller: 'JudgementsController',
//bindToController: true,
};
});
I am trying to implement component require property to give me access to ctrl.Judgements from the above component/directive as follows:
// list.component.js
function ListController(GetList, GetJudgements) {
var ctrl = this;
ctrl.list = [];
ctrl.Get = function () {
GetList.get().$promise.then(
function (data) {
ctrl.list = data.list;
}, function (error) {
// show error message
});
};
//ctrl.GetJudgements = function () {
// GetJudgements.get().$promise.then(
// function (data) {
// ctrl.Judgements = data.judgements;
// }, function (error) {
// // show error message
// });
//}
ctrl.$onInit = function () {
ctrl.Get();
//ctrl.GetJudgements();
};
}
angular
.module('App')
.component('cTheList', {
bindings: {
listid: '<',
},
controller: ListController,
controllerAs: 'ctrl',
require: {
jCtrl: 'cJudgements',
},
template: `
<c-list-item ng-repeat="item in ctrl.list"
item="item"
judgements="ctrl.Judgements"></c-list-item>
<!--
obviously the reference to judgements here needs to change
or even better to be moved into require of cListItem component
-->
`,
});
Nice and simple no magic involved. A keen reader probably noticed GetJudgement service call in the ListController. This is what I am trying to remove from TheList component using require property.
The reason? Is actually simple. I want to stop database being hammered by Judgement requests as much as possible. It's a static list and there is really no need to request it more than once per instance of the app.
So far I have only been successful with receiving the following error message:
Error: $compile:ctreq
Missing Required Controller
Controller 'cJudgements', required by directive 'cTheList', can't be found!
Can anyone see what I am doing wrong?
PS: I am using angular 1.5
PSS: I do not mind which way cJudgement is implemented (directive or component).
PSSS: If someone wonders I have tried using jCtrl: '^cJudgements'.
PSSSS: And multiple ^s for that matter just in case.
Edit
#Kindzoku posted a link to the article that I have read before posting the question. I hope this also helps someone in understanding $onInit and require in Angular 1.5+.
Plunker
Due to popular demand I made a plunker example.
You should use required components in this.$onInit = function(){}
Here is a good article https://toddmotto.com/on-init-require-object-syntax-angular-component/
The $onInit in your case should be written like this:
ctrl.$onInit = function () {
ctrl.jCtrl.Get();
};
#iiminov has the right answer. No parent HTML c-judgements was defined.
Working plunker.

Angular UI Router Reload Controller on Back Button Press

I have a route that can have numerous optional query parameters:
$stateProvider.state("directory.search", {
url: '/directory/search?name&email',
templateUrl: 'view.html',
controller: 'controller'
When the user fills the form to search the directory a function in the $scope changes the state causing the controller to reload:
$scope.searchDirectory = function () {
$state.go('directory.search', {
name: $scope.Model.Query.name,
email: $scope.Model.Query.email
}, { reload: true });
};
In the controller I have a conditional: if($state.params){return data} dictating whether or not my service will be queried.
This works great except if the user clicks the brower's forward and/or back buttons. In both these cases the state (route) changes the query parameters correctly but does not reload the controller.
From what I've read the controller will be reloaded only if the actual route changes. Is there anyway to make this example work only using query parameters or must I use a changing route?
You should listen to the event for succesful page changes, $locationChangeSuccess. Checkout the docs for it https://docs.angularjs.org/api/ng/service/$location.
There is also a similar question answered on so here How to detect browser back button click event using angular?.
When that event fires you could put whatever logic you run on pageload that you need to run when the controller initializes.
Something like:
$rootScope.$on('$locationChangeSuccess', function() {
$scope.searchDirectory()
});
Or better setup like:
var searchDirectory = function () {
$state.go('directory.search', {
name: $scope.Model.Query.name,
email: $scope.Model.Query.email
}, { reload: true });
$scope.searchDirectory = searchDirectory;
$rootScope.$on('$locationChangeSuccess', function() {
searchDirectory();
});
Using the above, I was able to come up with a solution to my issue:
controller (code snippet):
...var searchDirectory = function (searchParams) {
if (searchParams) {
$scope.Model.Query.name = searchParams.name;
$scope.Model.Query.email = searchParams.email;
}
$state.go('directory.search', {
name: $scope.Model.Query.name,
email: $scope.Model.Query.email,
}, { reload: true });
};...
$rootScope.$on('$locationChangeSuccess', function () {
//used $location.absUrl() to keep track of query string
//could have used $location.path() if just interested in the portion of the route before query string params
$rootScope.actualLocation = $location.absUrl();
});
$rootScope.$watch(function () { return $location.absUrl(); }, function (newLocation, oldLocation) {
//event fires too often?
//before complex conditional was used the state was being changed too many times causing a saturation of my service
if ($rootScope.actualLocation && $rootScope.actualLocation !== oldLocation && oldLocation !== newLocation) {
searchDirectory($location.search());
}
});
$scope.searchDirectory = searchDirectory;
if ($state.params && Object.keys($state.params).length !== 0)
{..call to service getting data...}
This solution feels more like a traditional framework such as .net web forms where the dev has to perform certain actions based on the state of the page. I think it's worth the compromise of having readable query params in the URL.

Why will my twitter widget not render if i change the view in angularjs?

Hi and thanks for reading.
I have a angular app im making and ive stumbled on a problem. set up as so
index.html-
<html ng-app="myApp">
...
<div ng-view></div>
<div ng-include="'footer.html'"></div>
...
</html>
I wont bother putting my routes its pretty simple /home is shows the /home/index.html and so on...
/home/index.html (default view when you come to the site)
<div class="responsive-block1">
<div class="tweet-me">
<h1> tweet me </h1>
</div>
<div class="twitter-box">
<twitter-timeline></twitter-timeline>
</div>
twitter timeline directive
directives.directive("twitterTimeline", function() {
return {
restrict: 'E',
template: '<a class="twitter-timeline" href="https://twitter.com/NAME" data-widget-id="XXXXXXXXXXXXXX">Tweets by #NAME</a>',
link: function(scope, element, attrs) {
function run(){
(!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0],p=/^http:/.test(d.location)?'http':'https';if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src=p+"://platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);}}(document,"script","twitter-wjs"));
console.log('run script');
};
run();
}
};
});
So I have just created a basic twitter directive using the tag from twitter. But when I change the view example to /blog then go back to /home the twitter widget no longer renders at all.
Im also using an $anchorScroll and if i jump to anyway on the page with this the widget also disappears. Any info would be great thanks.
See this post: https://dev.twitter.com/discussions/890
I think that you may be able to get the widget to re-render by calling
twttr.widgets.load().
If you find that this does not work, you will need to wrap this code into $timeout in your controller:
controller('MyCtrl1', ['$scope', '$timeout', function ($scope, $timeout) {
$timeout = twttr.widgets.load();
}])
To build on Sir l33tname's answer:
In services declaration:
angular.module('app.services', []).
service('tweetWidgets', function() {
this.loadAllWidgets = function() {
/* widgets loader code you get when
* declaring you widget with Twitter
* this code is the same for all widgets
* so calling it once will reference whatever
* widgets are active in the current ng-view */
!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0],p=/^http:/.test(d.location)?'http':'https';if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src=p+"://platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);}}(document,"script","twitter-wjs");
};
this.destroyAllWidgets = function() {
var $ = function (id) { return document.getElementById(id); };
var twitter = $('twitter-wjs');
if (twitter != null)
twitter.remove();
};
});
Then in controller declarations:
angular.module('app.controllers', []).
controller('view_1_Controller', tweetWidgets) {
// load them all
tweetWidgets.loadAllWidgets();
}).
controller('view_2_Controller', tweetWidgets) {
// now destroy them :>
tweetWidgets.destroyAllWidgets();
});
Now whenever you leave view #1 to go to view #2, your controller for view #2 will remove the widgets associated with view #1 and when you return to view #1 the widgets will be re-instatiated.
The problem is because when Angular switches views the script tag that was originally inserted is not removed from the document. I fixed this on my own website by removing the Twitter script element whenever my Twitter timeline directive is not in the view. See the code below with comments.
function (scope, el, attrs) {
el.bind('$destroy', function() {
var twitterScriptEl = angular.element('#twitter-wjs');
twitterScriptEl.remove();
});
// function provided by Twitter that's been formatted for easier reading
function (d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0], p = /^http:/.test(d.location) ? 'http' : 'https';
// If the Twitter script element is already on the document this will not get called. On a regular webpage that gets reloaded this isn't a problem. Angular views are loaded dynamically.
if (!d.getElementById(id)) {
js = d.createElement(s);
js.id = id;
js.src = p + "://platform.twitter.com/widgets.js";
js.parentNode.insertBefore(js, fjs);
}
}(document, "script", "twitter-wjs");
}
Basically it's what Loc Nguyen say.
So every time you recreate it you must remove it first.
var $ = function (id) { return document.getElementById(id); };
function loadTwitter() {!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0],p=/^http:/.test(d.location)?'http':'https';if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src=p+"://platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);}}(document,"script","twitter-wjs");}
var twitter = $('twitter-wjs');
twitter.remove();
loadTwitter();
Answer by #b1r3k works without problems :
put this in your controller:
$timeout(function () { twttr.widgets.load(); }, 500);
For those trying to load twttr.widgets.load() inside their controller, you will most likely get an error that twttr is not defined AT SOME POINT in your UX, because the async call to load the twitter script may not be completed by the time you controller instantiates and references twttr.
So I created this TwitterService
.factory('TwitterService', ['$timeout', function ($timeout) {
return {
load: function () {
if (typeof twttr === 'undefined') {
(function() {
!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0],p=/^http:/.test(d.location)?'http':'https';if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src=p+'://platform.twitter.com/widgets.js';fjs.parentNode.insertBefore(js,fjs);}}(document, 'script', 'twitter-wjs');
})();
} else {
$timeout = twttr.widgets.load();
};
}
}
}])
and then call TwitterService.load() inside the controllers that require your widgets. This worked pretty well. It basically just checks if the twttw object exists and if it does, just reload the script... otherwise just reload the script.
Not sure if this is the best implementation, but it seems like all other solutions have edge cases where it will throw an error. I have yet to find one with this alternative.

What is event.preventDefault preventing despite the event?

One goal of my main controller is to prevent users from going to urls of other users. That works perfectly fine with listening on $locationChangeStart and using its events preventDefault method. Unfortunately calling this method has the strange side effect of somehow "interrupting" the work of the function "handleNotification" which has the goal of notifying the user for 2 seconds that she or he has done something illegitimate. If I comment out event.preventDefault(), everything works as expected. So my question is: What is the 'scope' of the 'default' preventDefault prevents that I don't have on my mind and which keeps the handleNotification function from working properly?
$scope.$on('$locationChangeStart', function(event, newUrl, oldUrl) {
ifUserIs('loggedIn', function() {
if (newUrl.split('#/users/')[1] !== $scope.user.userId) {
handleNotification('alert', 'You are not allowed to go here.');
event.preventDefault();
}
});
});
function handleNotification (type, message) {
$scope.notice = {
content: message,
type: type
};
$timeout(function() {
delete $scope.notice;
return true;
}, 2000);
}
Update below
Ok. The problem is somewhere else. In the related jsfiddle everything works fine. After finding the source which is responsible for this strange behaviour I will let you know.
<html ng-app="mapApp">
<div ng-controller="mainCtrl">
<global-error message="{{notice.content}}"></global-error>
</div>
</html>
And the code.
var mapApp = {};
mapApp = angular.module('mapApp', []);
mapApp.controller('mainCtrl', function ($scope, $location, $timeout) {
$location.path('users/2')
$scope.$on('$locationChangeStart', function (event, newUrl, oldUrl) {
handleNotification('alert', 'You are not allowed to go here.');
event.preventDefault();
});
function handleNotification(type, message) {
$scope.notice = {
content: message,
type: type
};
$timeout(function () {
delete $scope.notice;
console.log('deleted');
return true;
}, 2000);
$scope.$digest();
}
});
mapApp.directive('globalError', function () {
return {
restrict: 'E',
scope: {
message: '#',
type: '#'
},
template: "<div class=\"alert-box {{type}}\">\
<p>\
{{message}}\
</p>\
</div>"
};
});
Update
Ok. One step further. And the problem is still there. Right now I know that changing the path in the browser is something different than changing the url by putting $location.path('users/2') inside the code (see above). While $location.path('users/2') works as expected, changing the path in the browsers address bar manually just makes the address jump back to the old address without displaying the notice. So event.preventDefault() works correctly but handleNotification('alert', 'You are not allowed to go here.') isn't. Strange.
Update 2
Adding $scope.$digest() to the end of the handleNotification function did the trick.

Resources