durandal child router with parameter from parent - durandal

I am trying to initialize a child router to build sub navigation for the customer section of my application.
The url i am trying to configure is:
#customer/1/orders/1
I defined a route to get to the customer view in my shell.js
define(['plugins/router', 'durandal/app'], function (router, app) {
return {
router: router,
activate: function () {
router.map([
{ route: 'customer/:id*splat', moduleId: 'customer/customer' }
]).buildNavigationModel();
return router.activate();
}
};
});
I created a customer view that contains sub navigation for the customer section. The navigation will use the customer id that was in the route. This view doesnt really do anything except show customer sub-navigation. I created a child router in this view.
define(['plugins/router', 'knockout'], function (router, ko) {
var childRouter = router.createChildRouter()
.makeRelative({
moduleId: 'customer',
fromParent: true
}).map([
{ route: 'orders/:orderId', moduleId: 'orders' }
]).buildNavigationModel();
var activate = function(id) {
};
return {
router: childRouter,
activate: activate
};
});
My problem is that I can't seem to get the routing to work when I have a parameter in my parent router. The customer view gets routed to but the orders view doesn't. I will end up having more sub views under the customer section.

I managed to get this working just fine with my parameterized route. My childRouter is second level and I'm also using {pushState: true} so have no #(hashes) in my hashes :) so you'll need to add some if you're not using pushState. It looks like this:
Folder structure looks like this:
app
|
|--users
|
|--profile
| |
| |--activity.html
| |--activity.js
| |--articles.html
| |--articles.js
| |--tags.html
| |--tags.js
|
|--index.html
|--indexjs
|--profile.html
|--profile.js
Splat route in the top level router:
{ route: 'users/:userId/:slug*details', title: 'User', moduleId: 'users/profile', nav: false }
profile.js looks like this:
define(['plugins/router', 'plugins/http', 'durandal/app', 'jquery', 'knockout', 'timeago', 'bootstrap'], function (router, http, app, $, ko) {
var childRouter = router.createChildRouter();
var userProfile = function(user) {
$.extend(this, user);
};
var userProfileViewModel = function () {
var self = this;
self.userProfile = ko.observable();
self.router = childRouter;
self.activate = function () {
var userId = router.activeInstruction().params[0];
var slug = router.activeInstruction().params[1];
var userRoute = '/users/' + userId + '/' + slug;
self.router.reset();
self.router
.makeRelative({
moduleId: 'users/profile',
route: 'users/:userId/:slug'
})
.map([
{ route: '', moduleId: 'articles', title: 'articles', nav: false, hash: userRoute + '/articles' },
{ route: 'articles', moduleId: 'articles', title: 'articles', nav: true, hash: userRoute + '/articles' },
{ route: 'tags', moduleId: 'tags', title: 'tags', nav: true, hash: userRoute + '/tags' },
{ route: 'activity', moduleId: 'activity', title: 'activity', nav: true, hash: userRoute + '/activity' }
]).buildNavigationModel();
return self.loadUserProfile(userId);
};
self.loadUserProfile = function(userId) {
var url = '/api/users/profile/' + userId;
return http.jsonp(url).then(function(response) {
self.userProfile(new userProfile(response));
});
};
};
return userProfileViewModel;
});
Note also that I'm returning a constructor function here, not an object since in my case I don't want a singleton for this view.

I'd keep it simple and start with some static routes at the top level
router.map([
{ route: 'customer/:id', moduleId: 'customer/customer' },
{ route: 'customer/:id/orders/:id', moduleId: 'customer/orders' },
{ route: 'customer/:id/xxx/:id', moduleId: 'customer/xxx' }
]).buildNavigationModel();
In order to have full life cycle control for each customer (order, xxx, etc.) instance return a constructor function instead of a singleton.
define(['knockout'], function (ko) {
var ctor = function(){
...
};
//this runs on every instance
ctor.prototype.activate = function(id) { // or function(orderId, xxxID)
};
return ctor;
});
More info singleton vs constructor: http://durandaljs.com/documentation/Creating-A-Module.html

Related

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';
}
}

Aurelia load routes dynamically / from fetch

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;
}
...

Durandal child router setup with relative moduleId

I am trying to configure Durandal child routing. I have a public section for witch the parent router is responsible:
//main router
return router.map(config.publicRoutes)
.buildNavigationModel()
.mapUnknownRoutes('account/login', '#login/')
.activate();
//public routes
[{ route: 'login', title: 'Login', moduleId: 'account/login', nav: false, hash: '#login/' },
{ route: 'register', title: 'Register', moduleId: 'account/register', nav: false, hash: '#register/' },
{ route: 'reset-password', title: 'Reset password', moduleId: 'account/reset-password', nav: false, hash: '#reset-password/' },
{ route: 'private*details', moduleId: 'private/private-shell', title: 'Application', nav: true, hash: '#private/' }
];
Then a child router should be responsible for the private section. I am mapping the routes for the child router after the user has logged in. Depending on the user type (admin, user) I am activating the child router with the appropriate routes:
//initializing the router from the login view
var promise = Q.all([private_shell.initRoutes(isAdmin || true)]);
return promise.then(navigate("#private/silos"));
// child router in private-shell
var privateRouter = router.createChildRouter();
var routes = [];
//method to initialize the proper routes after login
var initRoutes = function (isAdmin) {
privateRouter.reset().makeRelative({
moduleId: 'viewmodels/private/',
fromParent: true
});
console.log(privateRouter);
return privateRouter.map(isAdmin ? config.adminRoutes : config.userRoutes).buildNavigationModel();
};
The first time when the router is initialized all works fine but if I return to the main router(login view) and another login is performed the child router adds the relative moduleId twice.
After the first login the routes have the moduleId ´viewmodels/private/route´, which is the right one, but second time the login initializes the child router the routes have the moduleId ´viewmodels/private/viewmodels/private/route´.
GET http://localhost:7777/App/viewmodels/private/viewmodels/private/silos.js 404 (Not Found)
When it should be:
GET http://localhost:7777/App/viewmodels/private/silos.js
I wasn't able to identify what might cause this. Any help?
Can you try specifying the parent route in a route property on the makeRelative settings object?
Perhaps also try making the reset call explicit.
Like this:
privateRouter.reset();
privateRouter.makeRelative({
moduleId: 'viewmodels/private/',
fromParent: true,
route: 'viewmodels/private'
});

How to set a Splat route as Default route Durandal 2.0

My default route has child views, can I set a Splat route as Default route in Durandal 2.0 if yes how I tried something like below but it fails , basically I want to implement a childrouter in my default view how can I do this..
define(['plugins/router'], function (router) {
return {
router: router,
activate: function () {
return router.map([
{ route: 'knockout-samples*details', moduleId: 'ko/index',title: 'Knockout Samples', nav: true, hash: '#knockout-samples' }
]).buildNavigationModel()
.activate();
}
};
});
If I understand you correctly then yes - you can have a splat as your default route. You would do something like this in your root shell:
router.map({
moduleId: "child/shell",
route: "*details"
});
And then in your child's view model:
var childRouter = rootRouter
.createChildRouter()
.makeRelative({ moduleId: "child" });
// Uses "child/defaultPage" as the view model, and "#/" as the route
childRouter.map({
moduleId: "defaultPage",
route: ""
});
Hope that helps.

Combining parameters, splat routes and child routers

We are trying to use route parameters alongside splat routes. This is the scenario.
We have a navigation menu, and then as part of one of the options, we have a wizard. When we go to the wizard, we want to pass the identifier for the object we are working on. At the same, the wizard has steps and we want to handle those at the child level router, passing also the step as part of the route (*details).
The splat route looks like this:
{ route: 'offer/:offerId/*details', moduleId: 'offerbuilder/index', title: 'Offer Builder' }
The children routes look like this:
[
{ route: 'parameters', moduleId: 'parameters', title: 'Parameters', nav: true },
{ route: 'cart', moduleId: 'cart', title: 'Cart', nav: true },
{ route: 'workspaces', moduleId: 'workspaces', title: 'Workspaces', nav: true }
]
We've got the router and child router to resolve the routes appropriately, but the issue we are having is that the hashes for the children routes don't replace the passed parameter value, but use the original pattern. In other words, this do work:
http://oursite/main/#offer/123456789/parameters
but the hashes generated as part of the wizard steps are:
http://oursite/main/#offer/:offerId/parameters
My question is, do splat routes support the inclusion of parameters? If not, what would be the suggested workaround?
This is currently an issue with the child router, which is discussed here.
The solution is to pull the parentId and manually construct the hashes for the child router.
var childRouter = router.createChildRouter()
.makeRelative({
moduleId: 'employees/doctors',
route: 'doctors/:id',
});
return {
router: childRouter,
activate: function() {
var doctorID = router.activeInstruction().params;
childRouter.map([
{ route: '', moduleId: 'patients/index', hash: '#employees/doctors' + doctorID(), nav: true },
{ route: 'schedule', moduleId: 'schedule/index', hash: '#employees/doctors/' + doctorID() + '/schedule', nav: true }
]).buildNavigationModel();
}
};