How to defer routes definition in Angular.js? - dynamic

I have configured some basic routes that are available for all users before they log in:
App.config(function ($routeProvider) {
$routeProvider.
when('/login', { templateUrl: 'views/login.html', controller: PageStartCtrl.Controller }).
otherwise({ redirectTo: '/login' });
});
So the only thing user can do is to log in. After the user logs in, I would like to register additional routes like this:
$http
.post('api/Users/Login', { User: userName, Password: userPassword })
.success(function (response : any) {
App.config(function ($routeProvider) {
$routeProvider
.when('/dashboard',
{ templateUrl: 'part/dashboard.html',
controller: DashboardCtrl.Controller });
});
However, I suppose I should call .config method only once, because the $routeProvider is brand new instance that knows nothing about /login route. Further debugging showed me that the first instance of $resourceProvider is used when resolving view change.
Q: Is there a way how to register routes later?
Solution from Add routes and templates dynamically to $routeProvider might work, but is quite ugly (involved global variable nastyGlobalReferenceToRouteProvider).

Since routes are defined on a provider level, normally new routes can only be defined in the configuration block. The trouble is that in the configuration block all the vital services are still undefined (most notably $http). So, on the surface it looks like w can't define routes dynamically.
Now, it turns out that in practice it is quite easy to add / remove routes at any point of the application life-cycle! Looking at the $route source code we can see that all the routes definition are simply kept in the $route.routes hash which can be modified at any point in time like so (simplified example):
myApp.controller('MyCtrl', function($scope, $route) {
$scope.defineRoute = function() {
$route.routes['/dynamic'] = {templateUrl: 'dynamic.tpl.html'};
};
});
Here is the jsFiddle that demonstrates this in action: http://jsfiddle.net/4zwdf/6/
In reality, if we want to be close to what AngularJS is doing the route definition logic should be a bit more complex as AngularJS is also defining a redirect route to correctly handle routes with / at the end (make it effectively optional).
So, while the above technique will work, we need to note the following:
This technique depends on the internal implementation and might break if the AngularJS team decides to change the way routes are defined / matched.
It is also possible to define the otherwise route using the $route.routes as the default route is stored in the same hash under the null key

I found that the answer by #pkozlowski.opensource works only in angularjs 1.0.1. However, after angular-route.js becomes an independent file in the later version, directly set the $route doesn't work.
After reviewing the code, I find the key of $route.routes is no longer used to match location but $route.route[key].RegExp is used instead. After I copy the origin when and pathRegExp function, route works. See jsfiddle:
http://jsfiddle.net/5FUQa/1/
function addRoute(path, route) {
//slightly modified 'when' function in angular-route.js
}
addRoute('/dynamic', {
templateUrl: 'dynamic.tpl.html'
});

Related

Custom PageLayoutComponent

To have specific layout for some pages at our project we create few custom PageLayoutComponent's. Some contfiguration example:
{
// #ts-ignore
path: null,
canActivate: [CmsPageGuard],
component: CartPageLayoutComponent,
data: {
cxRoute: 'cart',
cxContext: {
[ORDER_ENTRIES_CONTEXT]: ActiveCartOrderEntriesContextToken,
},
},
},
All work fine with storefront until you will not try to select specific page at smartedit. As result it not use our custom CartPageLayoutComponent, but will use PageLayoutComponent for rendering.
Probably this is because it's not a normal route navigation. Can somebody from spartacus team suggest how this bug can be fixed?
Probably this is because it's not a normal route navigation
I believe your Route should be recognized normally, there is nothing special in adding a custom Angular Route.
So I guess there is something special about the page or URL of Spartacus Storefront that runs in your SmartEdit.
It's hard to tell the reason of your problem without more debugging.
You said your app works as expected when run differently (locally?), but when used in SmartEdit, then there is a problem. Please identify factors that makes the run SmartEdit different from your (local?) run. And try to isolate them. Guesses from top of my head:
production vs dev mode of the build?
exact URL of the cart page?
any difference in configuration between a local version and deployed one to be used in SmartEdit?
I would also add some debugging code to better know which routes configs are available and which one is used for the current route. For debugging purposes please add the following constructor logic in your AppModule:
export class AppModule {
// on every page change, log:
// - active url
// - the Route object that was matched with the active URL
// - all the Route objects provided to Angular Router
constructor(router: Router) {
router.events.subscribe((event) => {
if (event instanceof NavigationEnd) {
console.log({
activeUrl: router.url,
activeRouteConfig:
router.routerState.snapshot.root.firstChild.routeConfig,
allRoutesConfigs: router.config,
});
}
});
}
}
The pages opened in SmartEdit have the same route of cx-preview (e.g. to open faq page in smartedit, request is https://localhost:4200/electronics-spa/en/USD/cx-preview?cmsTicketId=xxxxx. backend can get page id from cmsTicketId). If you want to change the page layout, you can consider use PageLayoutHandler. Spartacus has some PageLayoutHandlers, e.g.
{
provide: PAGE_LAYOUT_HANDLER,
useExisting: CartPageLayoutHandler,
multi: true,
},

Reevaluate Nuxt.js middleware without a route change

I'm wondering if it's possible to essentially "reevaluate" the middleware conditions without actually changing the current route.
The middleware's purpose is to prevent non-logged-in users from accessing the "dashboard".
My issue is, a user could become logged in or logged out without necessarily changing route but they wouldn't be redirected until they try and change pages.
I have a VueX action that triggers when the user's auth state changes but this (from what I can see), can't access the redirect or route variables.
// /mixins/auth.js
const reevaluateAuthStatus = (store, redirect, route) => {
console.log(route)
const redirectPolicy = route.meta.map((meta) => {
if (meta.auth && typeof meta.auth.redirectPolicy !== 'undefined') { return meta.auth.redirectPolicy[0] }
return []
})
const user = store.getters['auth/getUser']
if (redirectPolicy.includes('LOGGEDOUT')) {
if (user) {
return redirect('/dashboard')
}
} else if (redirectPolicy.includes('LOGGEDIN')) {
if (!user) {
return redirect('/login')
}
}
}
module.exports = {
reevaluateAuthStatus
}
// /middleware/auth.js
import { reevaluateAuthStatus } from '../mixins/auth'
export default function ({ store, redirect, route }) {
reevaluateAuthStatus(store, redirect, route)
}
Appreciate any help on this :)
You cannot re-evaluate a middleware AFAIK because it's mainly this (as stated in the documentation)
middlewares will be called [...] on the client-side when navigating to further routes
2 clean ways you can still achieve this IMO:
use some websockets, either with socket.io or something similar like Apollo Subscriptions, to have your UI taking into account the new changes
export your middleware logic to some kind of call, that you could trigger again by calling the $fetch hook again or any other data-related fetching hook in Nuxt
Some more ugly solutions would probably be:
making an internal setInterval and check if the actual state is still valid every 5s or so
move to the same page you are actually on with something like this.$router.go(0) as somehow explained in the Vue router documentation
Still, most of the cases I don't think that this one may be a big issue if the user is logged out, because he will just be redirected once he tries something.
As if the user becomes logged-in, I'm not even sure on which case this one can happen if he is not doing something pro-active on your SPA.
I don't know if it's relevant or not, but I solved a similar problem this way:
I have a global middleware to check the auth status. It's a function that receives Context as a parameter.
I have a plugin that injects itself into context (e.g. $middleware).
The middleware function is imported here.
In this plugin I define a method that calls this middleware passing the context (since the Plugin has Context as parameter as well): ctx.$middleware.triggerMiddleware = () => middleware(ctx);
Now the middleware triggers on every route change as intended, but I can also call this.$middleware.triggerMiddleware() everywhere I want.

VueJS router alias to particular param

I have a lot of articles in my app, and the URL are written like this in Vue Router: /article/:id.
I have particular articles I want to "pin" and have easier URLs. For example: /pinned-article, which should point to /article/3274 and /other-pinned-article, pointing to /article/68173.
I though about adding this to my routes, but it doesn't work:
{ path: '/article/3274', component: Article, alias: '/pinned-article' }
I thought about something else, involving another component:
{ path: '/pinned-article/:id', component: PinnedArticle }
The component PinnedArticle silently aliasing the correct article with a command like router.alias in the <script> section, but it apparently doesn't exist.
Is there a way to solve this problem? I thought I could use some answers I read here in Stackvoverflow (for examples when it comes to redirect /me to /user/:id, but it doesn't apply.
Thanks in advance :)
addRoute
You can achieve this with Dynamic Routing, which is not the same as dynamic route matching, i.e. route params.
(This solution works in both Vue 3 and Vue 2 with Vue Router >= 3.5.0)
By using the addRoute method of Vue router, you can create routes at runtime. You can either use a redirect or not, depending on whether you want the url bar to read /article/3274 or /pinned.
Redirect
If you want the url to change from /pinned to /article/3274, use redirect:
methods: {
pinRoute() {
this.$router.addRoute({
path: '/pinned',
name: 'pinned',
redirect: { name: 'article', params: { id: 3274 }}
})
}
}
Access the route like:
this.$router.push('/pinned')
The above example assumes you give your Article route a name: 'article' property so you can redirect to it
Alias
You can keep the URL as /pinned using alias. Normally the alias would go on the existing Article route definition, but that doesn't work well with route params. You can use a "reverse alias" with a new route:
methods: {
pinRoute() {
this.$router.addRoute({
path: '/params/3274',
name: 'pinned',
alias: '/pinned',
component: () => import('#/views/Article.vue') // Article component path
})
}
}
Access the route like:
this.$router.push('/pinned')
Notes:
You'll probably want to pass an id argument to the pinRoute methods rather than hardcode them like in the examples above.
A nice thing about addRoute with either method above is if the route already exists, say, from the last time you called the method, it gets overwritten. So you can use the method as many times as you like to keep changing the destination of /pinned. (The docs in both Vue 2 and Vue 3 say the route definition will get overwritten, though Vue 2 router throws a duplicate route warning.)
Of course the pinned route won't automatically persist between app refreshes, so you'll need to save/load the pinned id (i.e. using localStorage, etc.) and run one of these methods on app load if you want that

Is there a more comprehensive way to handle authentication in Angular2 rc3 than guards?

I have an existing Angular2 app where login/authentication was handled by creating an AuthRouterOutletDirective that extended the RouterOutlet. This made it easy to use componentInstruction in the activate function to check if a user was logged in, if not redirect them to our login portal (for various reasons I have no control over this is through a separate app, which has it's own login screen and then sends back a token that we would save in the Angular2 app) before navigating to the next component.
I just upgraded to the router 3.0.0-alpha.8 and I see that this is no longer an option and it has been replaced by creating an authGuard and and using the canActivate method to handle authentication (I'm referring to this section of the documentation). The problem I'm struggling with is it seems like this is designed for apps where only a small number of routes are protected, and you can just add canActivate: [AuthGuard] to each route that requires authentication in the RouterConfig.
The problem I'm having is that every single route needs to be protected by authentication. There also needs to be continuous checking (unless there is a better fix) because we also (again due in part to the external login service we have to use) will log out the users and clear their token and the next time you navigate to a new route (regardless of what the route is) it should redirect you to login again. I understand that I could just add canActivate: [AuthGuard] to every single route but that seems like an insane and tedious fix, especially for apps with a large amount of routes, all of which require a user to be authenticated to view.
I've been searching for a fix but it seems like, perhaps in part because the upgrade is fairly new, that all the resources are for how to implement these AuthGuards on just one or two routes. I've been digging through the source code (specifically here) to see if there is another method I can extend that's more comprehensive than canActivate or a more universal way to have every route include a particular guard but I can't seem to find anything. Any advice on how best to implement this would be very much appreciated! Thanks.
Referring to my comment, you can add extra guard like that:
import {provideRouter, RouterConfig, Route, CanActivate, ActivatedRouteSnapshot} from '#angular/router';
import {Injectable} from "#angular/core";
#Injectable()
class UniversalGuard implements CanActivate {
canActivate(route: ActivatedRouteSnapshot) {
console.log('applying can activate');
return true;
}
}
let routes: RouterConfig = [
{path: 'path1', component: Component1} as Route,
{path: 'path2', component: Component2} as Route,
{path: 'path3', component: Component3} as Route
];
const routeAugumenter = route => {
let guards = route.canActivate || [];
guards.push(UniversalGuard);
route.canActivate = guards;
if (route.children) {
route.children = route.children.map(routeAugumenter);
}
return route;
};
let augumentedRoutes = routes.map(routeAugumenter);
console.log(augumentedRoutes);
export const APP_ROUTER_PROVIDERS = [
provideRouter(augumentedRoutes), UniversalGuard
];
I have not tested it with child routes, but should work as well.
Edit: updated information how to inject services into UniversalGuard.
If your guard needs some services injected, you inject them in the constructor, as usually. You have to provide these services (and the services they depend on, etc.) in Angular bootstrap call rather than in the components.

Durandal: Conditional Start Module When Activating the Router

My Durandal application's startup logic in shell.js requires sending the user to one of two possible views depending on some conditional logic. Basically, if certain options have not previously been selected I'll send them to a Setup page, otherwise I'll send them to the normal start page.
Previously, in Durandal 1.x I would just pass a string indicating the starting module when calling activate. But in Durandal 2.x that has been deprecated.
So first, I'm wondering what is the recommended way to do this from the standpoint of the routes array? Should I just register both routes as if neither is the start module (like below) then conditionally add a another route to the array with the default route of ''?
{ route: "setup", moduleId: "setup", title: "Setup", nav: false },
{ route: "Students", moduleId: "students", title: "Students", nav: false }
The second part of my question involves how to handle the need to make a call to a web service as part of my conditional logic for determining which module is the start module. My startup logic involves checking the local storage of the browser, but sometimes I'll also need to make an ajax request to the server to get a bit of information.
My understanding is that router.activate() is a promise. Can I actually just create my own promise and resolve it by calling router.activate() after the ajax call has completed? Or is there another way I'll need to handle that? Sample code for how I was thinking I might handle this:
var deferred = $.Deferred();
//Setup and conditional logic here
var routes = setupRoutes();
$.get('/api/myStartupEndpoint')
.done(function(results) {
//check results and maybe alter routes
deferred.resolve(router.map(routes).activate());
});
return deferred.promise();
Does that make sense? I'm still converting my app over to Durandal 2.0.1 so I haven't been able to try this yet, but regardless of whether it does or not I want to find out what the recommended approach would be in this scenario.
The way I'd do it is this - actually I think it's similar to how you're already thinking, so I hope it makes sense:
In your main.js, set your application root to the same module, no matter the circumstances. Since one of the main features of D2 is child routers, I'd suggest using the module name "root" for your application root, as it makes it easier to distinguish from "shell" modules (which I use for setting up child routers):
app.start().then(function () {
app.setRoot("root", "entrance");
});
In your root module, setup the routes as you have described:
{ route: "setup", moduleId: "setup", title: "Setup", nav: false },
{ route: "students", moduleId: "students", title: "Students", nav: false }
When your root module activates, check if the user has been setup or not. Use the result of that check to work out if you want the user to be sent to the setup page, or the students page. Note however that you must activate your router before redirecting; you can use $.when to help you here:
var checkUserSetup = function () {
var deferred = $.Deferred();
// check local storage, fire off ajax request, etc.
// resolve the deferred once you know if the user is setup or not
deferred.resolve(false);
return deferred.promise();
};
root.activate = function() {
mapRoutes();
var deferred = $.Deferred();
$.when(checkUserSetup(), router.activate()).done(function(isUserSetup) {
if (isUserSetup) {
router.navigate("students");
} else {
router.navigate("setup");
}
deferred.resolve();
});
return deferred.promise();
}
Hopefully this also answers the second part of your question; but just in case - yes, you can return a promise from the activate method, and durandal will "block" until you've resolved that promise. However, note that the router's activate method also returns a promise - so your code won't quite work. You're resolving your deferred with another deferred; you'd need to do something more like this:
root.activate = function() {
var deferred = $.Deferred();
//Setup and conditional logic here
var routes = setupRoutes();
$.get('/api/myStartupEndpoint')
.done(function(results) {
//check results and maybe alter routes
//don't resolve the deferred until the router has completed activation
router.map(routes).activate().done(deferred.resolve);
});
return deferred.promise();
}
Hope that helps.