Would like to check what is the issue caused to my <a href> links when after I introduced an interceptor to my angular app and it has cause the links to not reload when it is on the same page? below is how i introduce my interceptor to add jwt's Authentication token to my web service requests header.
app.config(['$httpProvider', function ($httpProvider) {
$httpProvider.interceptors.push(['$q', '$localStorage', '$location', function ($q, $localStorage, $location) {
return {
'request': function (config) {
config.headers = config.headers || {};
if ($localStorage.jwtToken) {
config.headers.Authorization = 'Bearer ' + $localStorage.jwtToken;
}
return config;
},
'responseError': function (response) {
if (response.status === 401 || response.status === 403) {
}
return $q.reject(response);
}
};
}]);
}]);
Noted that the presentation site and the business logic processing part are both independently separated and do not rely on each other. Which means that the presentation site is responsible to load the javascripts and HTML codes while the scripts are the one that is responsible to request data from the server. Authentication are done via JWT therefore I use the interceptor to inject the JWT related headers to every RESTful requests.
PHP => (renders HTML template) + (data from Angular) <= Angular => (send request to server get data)
Using the code above i was able to complete the JWT authentication but it causes all the <a href> links on the presentation page to not reload if it is in the same page. For example I have 3 items in my menu (Home, Page1, Page2). When I'm in Home and if I click on the Home link, it suppose to reload the page (like F5) but nothing happens. I would need to navigate away from the page then only i am able to click on the link.
What have i done wrong here?
Update 1: Question from #Sanjeev: How are you handling routing in you app, are you using ng-router module or custom ui-router module ? Can you add the routing code as well.
Noted that at this moment all routes are within the HTML itself using links. The javascripts do not handle any routes. Its responsibility is to GET and POST data.
Update 2: Added plunker link. Note that i would suggest you to try both commenting and uncommenting the entire interceptor section to see the difference when clicking the link. Follow these instruction below and you will recreate the scenario i mentioned.
Load and run the plunker file
On the top right corner, click on "Launch the preview in a separate window"
Copy the URL in the window and replace it in the section in line 25. The url should look something like run.plnkr.co/somerandomkeys+
Close the separate window and try clicking the link in the menubar.
When commenting the said section, notices that the page will load (acts as a refresh) but when you uncomment the section the link will not work anymore. Some sort of same page detection thing is blocking the action.
Solution :
Analysis: I ran your demo and understood the issue you were highlighting, actually the issue is not related to interceptors at all. Interceptors get called only when you make http requests using $http service.
In Angular apps the anchor tag behavior changes the moment you inject '$location' service in your app, you have injected '$location' service in your interceptor module (although i don't see it being used). So this solves the mystery why you start getting anchor issue when you add interceptor :)
In your example the anchor has same link as the current location so Angular is preventing the default behavior of anchor tag and clicking anchor does not reload your page.
You can solve it be multiple ways:
Don't inject '$location' service if you are not using it, if you can't remove it then go for solution 2 or 3.
Add attribute target="_self" or target="_blank" as per your case, this will solve your issue without requiring any Js code change. I tested this fix with your code and it worked for me.
Add a ng-click handler on anchors and change window.location in
it, or better create a directive for anchors and check if href is
same as current location then force page reload using location.reload()
If you decide to use angular routing which is great feature of Angular JS then use $route.reload() method
Related
I am working on an add-in and it's an Angular single page application. Currently it has only one controller ( one screen ). Based on the documentation:
If your add-in uses client-side routing, as single-page applications
typically do, you have the option to pass the URL of a route to the
displayDialogAsync method, instead of the URL of a complete and
separate HTML page.
HTML
<button type="button" class="ms-Button ms-Button--compound" id="addPlaceholder" ng-click="openPopup()">
<span class="ms-Button-label">Open Popup</span><span class="ms-Button-description">Open Popup</span>
</button>
Controller code
researchApppBuilderModule.controller('researchcontroller', ['$scope', '$location',
function ($scope, $location) {
var urlRoot = $("base").first().attr("href");
$scope.openPopup = function () {
var dialog;
Office.context.ui.displayDialogAsync(urlRoot + '/app/research', { height: 30, width: 20 },
function (asyncResult) {
dialog = asyncResult.value;
});
}
}]);
I have tested this route within the Add-in and it's working. So, I know the route is correct.
So, this code opens up a dialog but with an error:
ADD-IN ERROR:
Sorry, we cannot load the add-in. Please make sure you have network and/or Internet Connectivity. Click "Retry" once you're back online.
Error Screenshot
I tried searching in the documentation but couldn't find anything. What am I missing?
Although you "can" pass a route to the displayDialogAsync method, I don't recommend it for reasons given in the note just below the paragraph of documentation that you quoted: An entirely new instance of your add-in is launched in the dialog. This means that the app goes through its initialization code. This usually confuses the routing strategy and the dialog ends up trying to navigate to a URL that is an invalid combination of the default route and the route that you passed to the method, like https://example.com/app/#/app/research.
I recommend that create a research.html page and pass the URL of it to the dialog (as this sample does: Word-Add-in-AngularJS-Client-OAuth). This may seem to deviate from the purity of a single page app; but you're doing that anyway when you pass a route to the method, because you are really passing a second instance of the app's "single page" (because you're launching a second instance of the whole app).
Otherwise, you can troubleshoot, by having the callback to displayDialogAsync log the asyncResult.error.code and asyncResult.error.message properties to the console. I also recommend that you read Use the Dialog API | Handle errors and events. You could also use the Fiddler tool or the Charles tool to see what URL the dialog is trying to open.
Finally, your code doesn't show what the urlRoot is. Be sure it is an absolute URL including the HTTPS protocol.
In myproject I've an app with different modules, which is organized into a single page.
I used $stateProvider, $urlRouterProvider for routing through different pages by a side menu.
Problem Statement:
I use $http for the API request, for one condition I need to reload the whole page(which means both parent and child together)
I have tried the following and the issues are
1) $state.go('app');
$state.go('app.live_tracking',null,{reload:true});
Problem: This approach is get blocked saying cannot do for parent which abstract"
2) $state.go($state.current, {}, {reload: true});
ref:Clear History and Reload Page on Login/Logout Using Ionic Framework
Problem: reload the current route alone not the parent(menu is not loaded)
3) $route.reload():
ref:link
Problem: reload the current route alone not the parent(menu is not loaded)
4) $state.transitionTo('app.live_tracking', $state.$current.params, {reload: true});
Problem: reload the current route alone not the parent(menu is not loaded)
I'm here terribly stuck with this, someone kindly give me a hint to solve
Thanks in advance
I currently have a set-up based on the meanjs stack boilerplate where I can have users logged in this state of being 'logged-in' stays as I navigate the URLs of the site. This is due to holding the user object in a Service which becomes globally available.
However this only works if I navigate from my base root, i.e. from '/' and by navigation only within my app.
If I manually enter a URL such as '/page1' it loses the global user object, however if I go to my root homepage and navigate to '/page1' via the site. Then it's fine, it sees the global user object in the Service object.
So I guess this happens due to the full page refresh which loses the global value where is navigating via the site does not do a refresh so you keep all your variables.
Some things to note:
I have enabled HTML5Mode, using prefix of '!'.
I use UI-Router
I use a tag with '/'
I have a re-write rule on express that after loading all my routes, I have one last route that takes all '/*' to and sends back the root index.html file, as that is where the angularjs stuff is.
I'm just wondering what people generally do here? Do they revert the standard cookies and local storage solutions? I'm fairly new to angular so I am guessing there are libraries out there for this.
I just would like to know what the recommended way to deal with this or what the majority do, just so I am aligned in the right way and angular way I suppose.
Update:
If I manually navigate to another URL on my site via the address bar, I lose my user state, however if I manually go back to my root via the address bar, my user state is seen again, so it is not simply about loosing state on window refresh. So it seems it is related to code running on root URL.
I have an express re-write that manually entered URLs (due to HTML5 Location Mode) should return the index.html first as it contains the AngularJs files and then the UI-Route takes over and routes it properly.
So I would have expected that any code on the root would have executed anyway, so it should be similar to navigating via the site or typing in the address bar. I must be missing something about Angular that has this effect.
Update 2
Right so more investigation lead me to this:
<script type="text/javascript">
var user = {{ user | json | safe }};
</script>
Which is a server side code for index.html, I guess this is not run when refreshing the page to a new page via a manual URL.
Using the hash bang mode, it works, which is because with hash bang mode, even I type a URL in the browser, it does not cause a refresh, where as using HTML5 Mode, it does refresh. So right now the solution I can think of is using sessionStorage.
Unless there better alternatives?
Update 3:
It seems the best way to handle this when using HTML5Mode is that you just have to have a re-write on the express server and few other things.
I think you have it right, but you may want to look at all the routes that your app may need and just consider some basic structure (api, user, session, partials etc). It just seems like one of those issues where it's as complicated as you want to let it become.
As far as the best practice you can follow the angular-fullstack-generator or the meanio project.
What you are doing looks closest to the mean.io mostly because they also use the ui-router, although they seem to have kept the hashbang and it looks like of more of an SEO friendly with some independant SPA page(s) capability.
You can probably install it and find the code before I explained it here so -
npm install -g meanio
mean init name
cd [name] && npm install
The angular-fullstack looks like this which is a good example of a more typical routing:
// Server API Routes
app.route('/api/awesomeThings')
.get(api.awesomeThings);
app.route('/api/users')
.post(users.create)
.put(users.changePassword);
app.route('/api/users/me')
.get(users.me);
app.route('/api/users/:id')
.get(users.show);
app.route('/api/session')
.post(session.login)
.delete(session.logout);
// All undefined api routes should return a 404
app.route('/api/*')
.get(function(req, res) {
res.send(404);
});
// All other routes to use Angular routing in app/scripts/app.js
app.route('/partials/*')
.get(index.partials);
app.route('/*')
.get( middleware.setUserCookie, index.index);
The partials are then found with some regex for simplicity and delivered without rendering like:
var path = require('path');
exports.partials = function(req, res) {
var stripped = req.url.split('.')[0];
var requestedView = path.join('./', stripped);
res.render(requestedView, function(err, html) {
if(err) {
console.log("Error rendering partial '" + requestedView + "'\n", err);
res.status(404);
res.send(404);
} else {
res.send(html);
}
});
};
And the index is rendered:
exports.index = function(req, res) {
res.render('index');
};
In the end I did have quite a bit of trouble but managed to get it to work by doing few things that can be broken down in to steps, which apply to those who are using HTML5Mode.
1) After enabling HTML5Mode in Angular, set a re-write on your server so that it sends back your index.html that contains the Angular src js files. Note, this re-write should be at the end after your static files and normal server routes (e.g. after your REST API routes).
2) Make sure that angular routes are not the same as your server routes. So if you have a front-end state /user/account, then do not have a server route /user/account otherwise it will not get called, change your server-side route to something like /api/v1/server/route.
3) For all anchor tags in your front-end that are meant to trigger a direct call to the server without having to go through Angular state/route, make sure you add a 'target=_self'.
I have a completely separate (from backend) AngularJS app. It uses tokens (JWT) for authentication. I'm also using ui-router and Restangular.
My problem:
I have a sidebar/profile area that displays information from the currently logged in user on every "page". It is implemented as a directive+controller and is outside of the ui-view directive context. Inside of the controller for this sidebar I'm making a request to get the profile information and attach it to the $scope. Now, when a user first visits the app that sidebar and controller get invoked and the request is made to get the profile information. However, if and since the user is not logged in, the request fails and no data is bound with the $scope.
In the meantime (notice this all happens really fast), the event listener (listening for $stateChangeSuccess from ui-router) determines that the user is not logged in (essentially if they don't have a token) and does a $state.go('login') to forward the user to the login view. The user logs in, and a $state.go('dashboard') is invoked to bring them back to the dashboard. However, being that sidebar is outside of the context of the router the controller for it is not instantiated again to cause a new request for profile information. So the sidebar is just empty with no profile information.
I'm aware that there are going to be several possible solutions to this, but I'm trying to find a descent one.
How would you architect an angular app in order to solve or avoid the problem I'm having?
P.S. I think I may be having analysis paralysis over this.
It's hard for me to answer without seeing your code specifically. If I understand correctly your directive is firing prior to the user logging in, and since there is no user profile, the side bar doesn't initiate correctly. What I would suggest is possibly doing an ng-if on the tag that fires the directive something like:
<side-bar ng-if='userData' />
That way the tag isn't inserted into the DOM until the userData exists and therefore doesn't fire the directive on the login page.
Assuming that the sidebar is the highest angular controller in your application and the other controllers are nested inside it you should be able to put a function on it that will load the information that you need. Then you can call $rootScope.loadme() anywhere you inject $rootScope.
var mainMod = angular.module('mainMod',[]);
mainMod .controller('mainController', function($scope)
{
$scope.loadMe = function()
{
//load stuff here
};
$scope.loadMe();
});
mainMod .controller('secondController', function($rootScope, $scope)
{
$rootScope.loadMe();
});
SudoCode probably wont work with copy paste but the idea should be sound.
In the html view, images are displayed like this:
<img ng-src="{{element.image.url}}">
element.image.url points to an url like: /rest_api/img/12345678.
This is working fine, images are displayed.
Now, I add authentication:
Before the user is authenticated, every resources respond with an http error 401, images too. When authentication succeeds, a token is placed in a custom headers and sent with every $http requests, allowing access the resources:
$http.defaults.headers.common['Authorization'] = token;
This is working fine for Json files loaded with $resource. But the direct links to the images are still 401 after authentication.
How to call the images with custom headers?
Or any advice on how I should do this.
As said here you can use angular-img-http-src (bower install --save angular-img-http-src if you use Bower).
If you look at the code, it uses URL.createObjectURL and URL.revokeObjectURL which are still draft on 19 April 2016. So look if your browser supports it.
In order to use it, declare 'angular.img' as a dependency to your app module (angular.module('myapp', [..., 'angular.img'])), and then in your HTML you can use http-src attribute for <img> tag.
In your example it would be: <img http-src="{{element.image.url}}">
Of course, this implies that you have declared an interceptor using $httpProvider.interceptors.push to add your custom header or that you've set statically your header for every requests using $http.defaults.headers.common.MyHeader = 'some value';
There is a vary simple answer for that. You should use: angular-img-http-src.
Problem:
You used token based auth and you need to serve images from secured
routes.
Solution:
Use http-src instead of ng-src and it will fetch images using the
$http service - meaning Authorization headers added via interceptors
will be present - then build a Blob and set the src to an objectURL.
It works perfectly on my project.
I am facing the same problem.
The best solution I found is passing the Authorization token as a query parameter.
For example :
<img src="http://myhost.com/image/path?accessToken=123456789" >
This way you can secure those images only for your REST consumers.
Consider the URL be http://foo.com/bar.png
In your controller,
angular.module('foo')
.controller('fooCtrl', ['$sce',
function($sce) {
$scope.dataSrc = "http://foo.com/bar.png"
$scope.src = $sce.trustAsResourceUrl($scope.dataSrc)
}
])
And in your view,
<img ng-src="{{src}}" />
.. seems to do the trick.
As far as I know it's not possible to pass additional headers with asset requests (scripts, images, media, CSS files that the browser loads while rendering the page). That's all controlled by the browser. Only when making a XHR (AJAX) request can you modify headers.
I would suggest looking at your server side authentication and seeing if there's a solution there.