Aurelia load routes dynamically / from fetch - aurelia

I want to load menu options dynamically. so I'm wondering the best approach
I am able to use the code below to add routes after the page is loaded. This works for normal navigation, but does not work during a refresh.
Can configure router return a promise / how do I load menu items into the route?
#inject(HttpClient)
export class DocumentMenu {
router: Router;
documents : IDocument[];
heading = 'Document Router';
constructor(public http: HttpClient) {}
activate(): void {
this.http.fetch('http://localhost:17853/Document/GetDocuments?folderID=13244')
.then<IDocument[]>(response => response.json())
.then<IDocument[]>(docs => {
if ( docs ){
for( var doc of docs){
this.router.addRoute( { route : doc.DocumentID.toString(), name : doc.Name, moduleId: './documents/document', nav:true, title: doc.Name });
}
this.router.refreshNavigation();
}
return docs;
});
}
configureRouter(config: RouterConfiguration, router: Router) {
var routes = new Array();
routes.push(
{ route: 'index', name: 'index-name', moduleId: './documents/index', nav: false, title: 'Documents' } );
routes.push( { route: '', redirect: 'index' } );
config.map( routes );
this.router = router;
}
}

This does not answer your question, but I think it may be helpful to you and others with a similar issue.
The Dynamic Route Anti-Pattern
Your application has a number of different routes, all of which vary based on the state of the application. Therefore, you must first fetch the data, and then build the routes, and then register them with the router.
The reason this is an anti-pattern is because you will continuously need to update the router based on the state of the application, when Aurelia itself is built with static ways of describing dynamic content.
Dynamically Routing Homogeneous Data
Let's say you are building Google Drive, and you have a number of various files that could change as the user adds and removes them. For this case you have two categories of routes: Folders and Documents. Therefore, you make one route for each.
configureRouter(config) {
config.map([
{ route: 'folder/:id', moduleId: 'folder' }
{ route: 'document/:id', moduleId: 'document' }
}
}
class FolderViewModel {
activate({ id }) {
// get your specific folder data and load it into your folder view model
this.fetch('getDocuments?folderId=${id}')
}
}
class DocumentViewModel {
activate({ id }) {
// get your specific document and load it into your document view model
this.fetch('getDocuments?documentId=${id}')
}
}
Dynamically Routing Hetergeneous Data
Let's say instead you want to build YouTube. When user mjd10d logs in, he is welcome to watch videos to his heart's content, but he is not a premium content creator, and doesn't have access to the content creation portion of the site. The best way to handle this is to leave all possible routes in your application, and filter them based on the user's credentials in an AuthorizeStep.
configureRouter(config, router) {
config.addPipelineStep('authorize', AuthorizeStep);
}
#inject(UserSession)
class AuthorizeStep {
constructor(UserSession) {
this.user = UserSession;
}
run(navigationInstruction, next) {
var instructions = navigationInstruction.getAllInstructions()
if (!this.authorized(instructions.config)) {
return Redirect('404');
}
return next();
}
authorized(routeConfig) {
// something smart that returns false if unauthorized
return this.user.permissionLevel > routeConfig.requiredPermission;
}
}
Though not all cases will be authorization related, you can always register your own pipeline step using the addPipelineStep API

You can add routes dynamically (at startup or anytime for that matter) by having a single fixed (static) route in the "configureRouter" method (in app.ts), to which you then add all the other routes dynamically, when your fetch completes, like so:
configureRouter(config, router) {
config.title = 'SM';
//configure one static route:
config.map([
{ route: ['', 'welcome'], name: 'welcome', moduleId: 'welcome/welcome', title: 'Welcome' }
]);
routeMaps(this.navRepo) //your repo/service doing the async HTTP fetch, returning a Promise<Array<any>> (i.e., the routes)
.then(r => {
r.forEach(route => this.router.addRoute(route));
//once all dynamic routes are added, refresh navigation:
this.router.refreshNavigation();
});
this.router = router;
}
The "routeMaps" function is just a wrapper around the repo call and a mapping of the result to the Array of route items.

You can return a promise in activate. if activate() returns a promise, configureRouter() doesnt fire until the promise returned in activate() is resolved.
I ended up preparing the routes in activate like below:
activate(){
return this.http.fetch('url')
.then(response => response.json())
.then(docs => {
this.routerMapped = docs;
});
}
configureRouter(config, router) {
//build the routes from this.routermapped if necessary
config.map( this.routerMapped );
this.router = router;
}

To make this work, I created the routes in the constructor with a synchronous request
export class DocumentMenu {
...
routes : RouteConfig[];
constructor(http: HttpClient) {
this.http = http;
var folderID = window.location.hash.split('/')[2]
this.routes = new Array<RouteConfig>();
this.routes.push ( { route: 'index', name: 'index-name', moduleId: './documents/index', nav: false, title: 'Documents' });
this.routes.push ( { route: '', redirect: 'index' } );
for( var route of this.getRoutes( folderID )){
this.routes.push( route );
}
}
getRoutes(folderID: string) : RouteConfig[]
{
var routes = new Array<RouteConfig>();
var docsURL = 'http://localhost:17853/Document/GetDocuments?folderID=' + folderID;
// synchronous request
var docsResp = $.ajax({
type: "GET",
url: docsURL,
async: false,
cache:false
}).responseText;
var docs = JSON.parse( docsResp );
for( var doc of docs ){
routes.push( { route : doc.DocumentID.toString(), name : doc.Name, moduleId: './documents/document', nav:true, title: doc.Name });
}
return routes;
}
configureRouter(config: RouterConfiguration, router: Router) {
config.map( this.routes );
this.router = router;
}
...

Related

Accessing Vue.$router inside an external "service" class

I have a service which extends EventEmitter.
services/service/service.js (relative to main.js)
import { EventEmitter } from "events";
class Service extends EventEmitter {}
Inside the Service class, I have the following method which returns a Promise:
/**
* Method to fetch the Active Survey by "surveySlug":
*/
fetchActiveSurvey(serviceSlug) {
return new Promise((resolve, reject) => {
axios.get(`${this.baseURL}/service/${serviceSlug}`, { headers: { Authorization: AuthStr } }).then(response => {
resolve(response.data);
}).catch(error => {
if (error.response && error.response.data.status != 200) {
Vue.$router.push({ name: 'home'});
}
});
});
}
I also have the usual routes.js in the same directory as main.js, which is working fine.
Services are then established as plugins:
import Service from "../services/service/service"; // <= Refrences `service.js`
export default {
install(Vue) {
Vue.prototype.$service = Service;
}
};
I have tried the following:
Vue.$router.push({ name: 'home'});
Vue.prototype.$router.push({ name: 'home'});
However, I seem to find that $router is not defined. What would be the best way to define the router push routes from inside this service?
You need to use the this keyword to access the router instance. Try:
this.$router.push({ name: 'home'});

How to access current route meta fields in common javascript module - VueJs

I have following route in my application:
const router = new Router({
mode: 'history',
scrollBehavior: () => ({ y: 0 }),
routes: [
{ path: '/', component: HomePage, meta: { pageType: 'home'} },
],
});
and have on common js module:
const trackEvent = {
getFields: () => {
//here need to access meta fields(pageType) of current route. how is it possible ?
}
}
export default trackEvent;
i want to access meta field in common module. how is it possible ?
The meta property is accessible via this.$route.meta on a Vue instance. Just pass that to the getFields method.
export default {
created() {
let meta = getFields(this.$route.meta);
}
}
getFields: (meta) => {
console.log(meta);
return meta.fields.pageType; // not sure what you're trying to return exactly
}
If you can't pass in the current route, you'll need to import the router object and get the current route from that:
import router from 'path/to/your/router'
const trackEvent = {
getFields: () => {
let meta = router.currentRoute.meta;
console.log(meta);
return meta.fields.pageType; // not sure what you're trying to return exactly
}
}
export default trackEvent;

Render different view dynamically in Aurelia

Is there any way in aurelia I can render different view dynamically.
async Activate(booking) {
//booking: is the route param
const hasRecord = await this.service.RecordExists(booking);
if (hasRecord) {
map(booking,form);
}
return {
//Render different template
}
}
You should try to tackle this issue in another way. Why would you want to navigate to a ViewModel and trigger its creation, just in order to not use it and load another ViewModel? Seems inefficient at best right?
Aurelia exposes pipelines on the router, you should do this check there and redirect accordingly. Look at the PreActivate step here, you could write something like this (pseudo code):
configureRouter(config, router) {
function step() {
return step.run;
}
step.run = async (navigationInstruction, next) => {
if(await this.service.RecordExists(navigationInstruction.queryParams...)
{
return next()
} else {
next.cancel(new Redirect('your other page'))
}
};
config.addPreActivateStep(step)
config.map([
{ route: ['', 'home'], name: 'home', moduleId: 'home/index' },
{ route: 'users', name: 'users', moduleId: 'users/index', nav: true },
{ route: 'users/:id/detail', name: 'userDetail', moduleId: 'users/detail' },
{ route: 'files/*path', name: 'files', moduleId: 'files/index', href:'#files', nav: true }
]);
}
EDIT
You can have cases where you don't want a redirect, for example you have users wanting to bookmark baseurl/businessobject/id, and the url is navigatable before the object actually exists
Then you can use the getViewStrategy() function on your ViewModel:
getViewStrategy(){
if(this.businessObj){
return 'existingObjectView.html';
} else {
return 'nonExisting.html';
}
}

How to return to view after authentication in Aurelia

I have a view that can be accessed by a direct link from an email.
Ex.
http://myServer:7747/#/pics/ClientId/YYYY-MM-DD
So this is set up using a route:
{ route: ['/pics', '/pics/:clientId/:sessionDate', 'pics'],
name: 'pics', moduleId: './views/pics', nav: false, title: 'Pictures',
auth: true, activationStrategy: activationStrategy.invokeLifecycle
},
So if a client clicks on this link and is not logged in, I want the view to redirect to a login screen (I am using aurelia-authentication plugin) and then when it succeeds, I want it to return to this page using the same urlParams.
I have the redirect to the login page working, but getting back to this view is proving difficult. If I just try to use history.back() the problem is that the authentication plugin has pushed another navigationInstruction (loginRedirect) onto the history before I can do anything. If I just try to hard-code a 'go back twice' navigation I run into a problem when a user simply tries to log in fresh from the main page and there is no history.
Seems like this should be easier than it is, what am I doing wrong?
I haven't used the aurelia-authentication plugin, but I can help with a basic technique you can use that makes this very easy. In your main.js file, set the root of your app to a "login" component. Within the login component, when the user has successfully authenticated, set the root of your app to a "shell" component (or any component you choose) that has a router view and configure the router in its view-model. Once this happens, the router will take the user to the proper component based on the url. If the user logs out, just set the app root back to the "login" component.
Here's some cursory code to attempt to convey the idea. I assume you're using the SpoonX plugin, but that's not really necessary. Just as long as you reset the root of your app when the user authenticates, it will work.
In main.js
.....
aurelia.start().then(() => aurelia.setRoot('login'));
.....
In login.js
import {AuthService} from 'aurelia-authentication';
import {Aurelia, inject} from 'aurelia-framework';
#inject(AuthService, Aurelia)
export class Login {
constructor(authService, aurelia) {
this.authService = authService;
this.aurelia = aurelia;
}
login(credentialsObject) {
return this.authService.login(credentialsObject)
.then(() => {
this.authenticated = this.authService.authenticated;
if (this.authenticated) {
this.aurelia.setRoot('shell');
}
});
}
.....
}
In shell.html
.....
<router-view></router-view>
.....
In shell.js
.....
configureRouter(config, router) {
this.router = router;
config.map(YOUR ROUTES HERE);
}
.....
I got this to work by replacing the plugin's authenticateStep with my own:
import { inject } from 'aurelia-dependency-injection';
import { Redirect } from 'aurelia-router';
import { AuthService } from "aurelia-authentication";
import { StateStore } from "./StateStore";
#inject(AuthService, StateStore)
export class SaveNavStep {
authService: AuthService;
commonState: StateStore;
constructor(authService: AuthService, commonState: StateStore) {
this.authService = authService;
this.commonState = commonState;
}
run(routingContext, next) {
const isLoggedIn = this.authService.authenticated;
const loginRoute = this.authService.config.loginRoute;
if (routingContext.getAllInstructions().some(route => route.config.auth === true)) {
if (!isLoggedIn) {
this.commonState.postLoginNavInstr = routingContext;
return next.cancel(new Redirect(loginRoute));
}
} else if (isLoggedIn && routingContext.getAllInstructions().some(route => route.fragment === loginRoute)) {
return next.cancel(new Redirect(this.authService.config.loginRedirect));
}
return next();
}
}
The only difference between mine and the stock one is that I inject a 'StateStore' object where I save the NavigationInstruction that requires authentication.
Then in my login viewModel, I inject this same StateStore (singleton) object and do something like this to log in:
login() {
var redirectUri = '#/defaultRedirectUri';
if (this.commonState.postLoginNavInstr) {
redirectUri = this.routing.router.generate(this.commonState.postLoginNavInstr.config.name,
this.commonState.postLoginNavInstr.params,
{ replace: true });
}
var credentials = {
username: this.userName,
password: this.password,
grant_type: "password"
};
this.routing.auth.login(credentials,
{ headers: { 'Content-Type': 'application/x-www-form-urlencoded' } },
redirectUri
).catch(e => {
this.dialogService.open({
viewModel: InfoDialog,
model: ExceptionHelpers.exceptionToString(e)
});
});
};
Hope this helps someone!

Multiple start pages in Durandal

I have included my main.js and shell.js below for reference. As you can see my default route in the shell.js is the viewmodels/search and it has a second route to viewmodels/application with can take an option parameter, which is the IDKey for a particular application. Most of the time this is how I want users to enter the system by starting with the search screen where they can search for a particular application or have the option to click a button to start a new application. However I would like to be able to publish url links that could skip the search page and start the application with the viewmodels/application page with the appropriate IDKey.
I just cannot seem to figure out how to implement this behaviour. Can anybody get me pointed in the right direction of how to implement this.
MAIN.JS
define('jquery', [], function () { return jQuery; });
define('knockout', [], function () { return ko; });
define(['durandal/system', 'durandal/app', 'durandal/viewLocator'], function (system, app, viewLocator) {
app.title = 'My App';
//specify which plugins to install and their configuration
app.configurePlugins({
router: true,
dialog: true,
widget: {
kinds: ['expander']
}
});
app.start().then(function () {
toastr.options.positionClass = 'toast-bottom-right';
toastr.options.backgroundpositionClass = 'toast-bottom-right';
viewLocator.useConvention();
app.setRoot('viewmodels/shell', 'entrance');
});
});
SHELL.JS
define(['plugins/router'], function (router) {
return {
router: router,
activate: function () {
return router.map([
{ route: '', moduleId: 'viewmodels/search', title: 'Permit Application Search', nav: true },
{ route: 'application(/:id)', moduleId: 'viewmodels/application', title: 'Permit Application', nav: true }
]).buildNavigationModel()
.activate();
}
};
});
Following your routes as shown in code, you should simply be able to publish a link like http://yourdomain.com#application/12