Durandal 2.0 - Child routers intended for nested menus? - durandal

I've building an app, and wanting to show a 2-tier menu, with both tiers always available. Durandal 2.0 introduced their new router, which supports 'child routers', which allow for easier deeplinking.
My question - Can I have my 'child' navigation routes permanently loaded (and a sub menu rendered when the parent is not active), or is the 'child router' design intended to lazy-load them to evaluate them once a deeplink is to be evaluated?
The Durandal examples show main navigation register a splat route, then when that view model is loaded/activated, the child router is registered.
E.g.: In the example provided with Durandal 2.0, the mian nev is registered in shell.js
router.map([
{ route: ['', 'home'], moduleId: 'hello/index', title: 'Validation test', nav: true },
{ route: 'knockout-samples*details', moduleId: 'ko/index', title: 'Knockout Samples', nav: true, hash: '#knockout-samples' }
]).buildNavigationModel()
.activate();
And the ko/index.js view model then registers the children (on activate())
var childRouter = router.createChildRouter()
.makeRelative({
moduleId:'ko',
fromParent:true
}).map([
{ route: '', moduleId: 'helloWorld/index', title: 'Hello World', type: 'intro' },
{ route: 'helloWorld', moduleId: 'helloWorld/index', title: 'Hello World', type: 'intro', nav: true},
{ route: 'clickCounter', moduleId: 'clickCounter/index', title: 'Click Counter', type: 'intro', nav: true}
]).buildNavigationModel();
Whereas I'm looking to define my routes in one place, e.g.:
var routes = [
{ route: ['', 'home'],
moduleId: 'hello/index',
title: 'Validation test',
nav: true },
{ route: 'knockout-samples*details',
moduleId: 'ko/index',
moduleRootId: 'ko', // Custom property to make child routes easier
title: 'Knockout Samples',
nav: true,
hash: '#knockout-samples',
childRoutes: [
{ route: '', moduleId: 'helloWorld/index', title: 'Hello World', type: 'intro' },
{ route: 'helloWorld', moduleId: 'helloWorld/index', title: 'Hello World', type: 'intro', nav: true},
{ route: 'clickCounter', moduleId: 'clickCounter/index', title: 'Click Counter', type: 'intro', nav: true}
]}
])
And the part I'm struggling with is registering the child routes, and have them evaulate correctly when navigating to them. (Rendering the menu would come later...)
// Load routes router.map(routes.navRoutes).buildNavigationModel().mapUnknownRoutes(function(instruction)
{
logger.logError('No Route Found', instruction.fragment, 'main', true);
});
// Try load child routes...
$.each(routes.navRoutes, function(index, parentRoute) {
if (!parentRoute.childRoutes) {
return;
}
var childRouter = router.createChildRouter()
.makeRelative({
moduleId: parentRoute.moduleRootId,
fromParent: true // also tried false + non-splat routes...
}).map(parentRoute.childRoutes).buildNavigationModel();
});
I've encountered various errors in getting this nav to render. Mostly internal router.js errors when trying to calculate the hash (either from a non-activated parent, or the child) - so all hashes have been defined.
Once I've got it to map the navigation, the child routes don't seem to be accessible - they just load the main splat page without error.
So I'm wondering if I'm going about this the right way at all? (Registering all child routes up front, with the goal of being about to render a 2-tier menu).
I'm thinking the fallback is using something like the Durandal 1.2 answer about subrouting, using a combination of flat routing registration, custom 'isSameItem' function & computed observables to render the 2-tier navigation.

I do hope I can help You a bit.
Can You have 'child' routers permanently loaded?
As far I know You can't ( or rather there is not simple way to achieve that). Child routers are designed to provide sub-routing capability for particular view - You can think about them as a Durandal navigation inside Durandal view. If we check the example code on Durandal page we can easily see that child routing life-time is connected with given view. What is more if we check the code of function creating child routes we will see that it creates new router and only store reference to parent router - the parent router ( in most cases main router) does not have references to its childs
router.createChildRouter = function() {
var childRouter = createRouter();
childRouter.parent = router;
return childRouter;
};
What can be done to achieve multilevel routing in main router?
I had similar problem in the past but with old version of Durandal. For this problem I had started from scratch You gave and modifed it a bit - I get rid of splat route as its intend to use with child routes and my solution will not use them.
var routes = [
{
route: ['', 'home'],
moduleId: 'viewmodels/home',
title: 'Validation test',
nav: true
},
{
route: 'knockout-samples',
moduleId: 'viewmodels/ko/index',
moduleRootId: 'viewmodels/ko', // Custom property to make child routes easier
title: 'Knockout Samples',
nav: true,
hash: '#knockout-samples',
childRoutes: [
{ route: 'simpleList', moduleId: 'simpleList', title: 'SimpleList', nav: true, hash : 'simpleList' },
{ route: 'clickCounter', moduleId: 'clickCounter', title: 'Click Counter', nav: true, hash : 'clickCounter' }
]
}
];
Next step is converting this user-friendly definition to route table which can be easily register in main router.
$.each(routes, function(index, route) {
if (route.childRoutes === undefined)
return
$.each(route.childRoutes, function(index, childRoute) {
childRoute.route = route.route + '/' + childRoute.route;
childRoute.moduleId = route.moduleRootId + '/' + childRoute.moduleId;
childRoute.title = route.title + '/' + childRoute.title;
childRoute.hash = route.hash + '/' + childRoute.hash;
childRoute.parent = route.moduleRootId;
});
routes = routes.concat(route.childRoutes);
});
Last step is standard registering router and activating it.
return router.map(routes)
.buildNavigationModel()
.activate();
How to render register routing so multilevel layout is preserved?
My code works with Bootstrap 2.3.0 and render multilevel menu as dropdown button. When route has no child routes ( so is just simple route) and has no parent ( its 1st level navigation) is rendered as simple button. If route has child routes its rendered as dropdown button and child routes are added to dropdown list.
<div class="btn-group" data-bind="foreach: router.navigationModel">
<!-- ko if: $data.childRoutes === undefined && $data.parent === undefined -->
<a data-bind="css: { active: isActive }, attr: { href: hash }, text: title" class="btn btn-info" href="#"/>
<!-- /ko -->
<!-- ko if: $data.childRoutes !== undefined -->
<div class="btn-group btn-info">
<a data-bind="css: { active: isActive }, attr: { href: hash }, text: title" class="btn btn-info" href="#"/>
<button class="btn btn-info dropdown-toggle" data-toggle="dropdown">
<span class="caret"/>
</button>
<ul class="dropdown-menu" data-bind="foreach: childRoutes">
<a data-bind="css: { active: isActive }, attr: { href: hash }, text: title" class="btn btn-info" href="#"/>
</ul>
</div>
<!-- /ko -->
</div>
The styles probably need to be bit polished but overall logic is done.

Related

Vue 3 Overview & Detail view same router-link should be active

This nav structure is given:
<router-link to="/">Dashboard</router-link>
<router-link to="/travels">Travels</router-link>
...
Travels contains a list with all Travel Locations like so:
<div v-for="travel of travels" :key="travel.id">
<router-link :to="{name: 'Travel.Detail', params:{id:id}}">{travel.label}</router-link>
</div>
Router is working too:
{
path: '/',
name: 'Dashboard',
component: Dashboard
},
{
path: '/travels',
name: 'Travels.Overview',
component: () => import('#/views/projects/TravelsOverview.vue'),
},
{
path: '/travels/:id',
name: 'Travels.Detail',
component: () => import('#/views/projects/TravelsDetail.vue'),
}
A requirement now is that when selecting a travel and showing the TravelsDetail view that the menu item for Travels stays active. As far as i understand children / nested routes like so:
{
path: '/travels',
name: 'Travels.Overview',
component: () => import('#/views/projects/TravelsOverview.vue'),
children:[
{
path: ':id',
name: 'Travels.Detail',
component: () => import('#/views/projects/TravelsDetail.vue'),
}
]
}
Are only working WITHIN the parent view inside another tag and cannot be used to "replace" the parent correct?
So how it is possible to have such a structure with overview & detail views (full views) where in overview and detail view the link item still has the "active" class?
Okey i've found a solution here:
https://forum.vuejs.org/t/router-active-class-on-submenu-parent/7027
<router-link to="/travels" custom v-slot="{ href, navigate, isActive, isExactActive }">
<li :class="{ 'uk-active active': isActive || isExactActive || subIsActive('/travels') }">
<a :href="href" #click="navigate"><span>Travels</span></a>
</li>
</router-link>
And in the method:
subIsActive(input) {
const paths = Array.isArray(input) ? input : [input]
return paths.some(path => {
return this.$route.path.indexOf(path) === 0 // current path starts with this path string
})
}
Now every route that starts with /travels adds the active classes to the router-link item.

trying to create aurelia routing in plunker

I am working on creating a page in plunker to demo aurelia routing. Here is the link. For some reason, I am unable to show the route in the page. I am able to run similar code in my local environment just fine. I think it is something in plunker that has to be done differently.
Here is the code:
app.html
<template>
<h1>Hello</h1>
<div class="row col-lg-6 col-lg-offset-3">
<div class="btn-group col-sm-offset-1" role="group" aria-label="...">
<a repeat.for="row of router.navigation" class="${row.isActive ? 'active btn btn-primary' : 'btn btn-default'}" href.bind="row.href">
${row.title}
</a>
</div>
<router-view></router-view>
</div>
</template>
app.ts
import { Aurelia, PLATFORM } from "aurelia-framework";
import { Router, RouterConfiguration } from "aurelia-router";
export class App {
router: Router;
// Configure Routing
configureRouter(config: RouterConfiguration, router: Router): void {
console.log("Aurelia routing");
config.title = "Aurelia Routing";
// config.options.root = "/";
config.map([
{
route: "",
redirect: "home",
settings: { icon: "home" }
},
{
route: "home",
moduleId: "./Home",
nav: true,
title: "Home",
settings: { icon: "home" }
},
{
route: "/support",
moduleId: "./Support",
nav: true,
title: "Support Request",
settings: { icon: "home" }
}
]);
this.router = router;
console.log(router);
}
// console.log(this.router);
}
Further details, such as bootstrapping, etc can be found in plunker link.
There's a typo/error in your main.ts:
aurelia.use.basicConfiguration()
Should be:
aurelia.use.standardConfiguration()
Changing this, I saw the console.log messages you put in the configuration, but I got another error, but the routing works now.

how to defime routing for multiple subviews

I am developing and application using durandal. I have two sections in my page with different navigation menu. I am using composition to keep make these subviews. But I am not sure how to define routes for these sections.
In your shell.js
{ route: 'work*details', moduleId: 'profile/index', title: 'Profil', nav: 1, hash: '#profil', authorize: ["User", "Administrator"] },
in the subfolder index.js
var childRouter = router.createChildRouter()
.makeRelative({
moduleId: 'viewmodels/marketplace',
fromParent: true
}).map(userRoute[
// put you sub nav here
]).buildNavigationModel();
return {
router: childRouter,
work: ko.computed(function () {
return ko.utils.arrayFilter(childRouter.navigationModel(), function (route) {
return route.type == 'intro';
});
})
};
the markup
<ul class="nav navbar-nav">
<!--ko foreach: work-->
<li data-bind="css: { active: isActive }">
<a data-bind="attr: { href: hash }, text: title"></a>
</li>
<!--/ko-->
</ul>
find out more here.

Durandal 2.0 routing parameterized routes (same route different parameter)

I am probably missing something basic, but when building navigation I am attempting to define multiple parameterized routes in the shell. The idea is that all of these routes will pass the user through to the same view/vm, but the parameter will determine content that is displayed after an ajax call). The routing itself works well, but the title is always passed through from the first route in the list.
activate: function () {
router.makeRelative({moduleId: 'viewmodels'}).map([
{
route: 'grid/:page',
title: 'Title 1',
moduleId: 'grid',
nav: 3,
hash: '#grid/param1'
},
{
route: 'grid/:page',
title: 'Title 2',
moduleId: 'grid',
nav: 2,
hash: '#grid/param2'
},
{
route: 'grid/:page',
title: 'Title 3',
moduleId: 'grid',
nav: 4,
hash: '#grid/param3'
},
{
route: 'grid/:page',
title: 'Title 4',
moduleId: 'grid',
nav: 5,
hash: '#grid/param4'
}
]).buildNavigationModel();
};
So, regardless of which of the generated links a user clicks on, the Title is always returned as 'Title 1'. Nav order does not matter. The first physical object in the list will supply the title for all links. If I hard code the links into shell.html and use a splat in the router I do not have this problem, however, hard coding the links is not feasible nor desirable for the app.
So, the question is, what am I doing wrong?
In the code above there's truly only one route 'grid/:page'. By defining a parameterized route the router maps everything the match grid/:page to the first route.
See more about that in the router documentation http://durandaljs.com/documentation/Using-The-Router/.
Instead of using the router.navigationModel() build your own small navigation model.
Top level down approach:
Step 1 Defining a grid route with an optional parameter (/:page).
router
.makeRelative({moduleId: 'viewmodels'})
.map([
{
route: 'grid(/:page)',
title: 'grid page',
moduleId: 'grid',
hash: '#grid'
}
])
.buildNavigationModel();
Step 2 Navigation model
define(['plugins/router', 'knockout', './navItem'], function( router, ko, NavItem ) {
var ctor = function(){
this.childRouter = router;
this.param = ko.observable('');
this.navigation = ko.observableArray([
new NavItem({title: 'Title 1', param: 'param1'}),
new NavItem({title: 'Title 2', param: 'param2'}),
new NavItem({title: 'Title 3', param: 'param3'}),
new NavItem({title: 'Title 4', param: 'param4'})
]);
};
ctor.prototype.activate = function(param){
this.param('Param: ' + (param || 'no param!'));
};
return ctor;
});
Step 3 Navigation item model
define(['plugins/router'], function( router ) {
var ctor = function(options){
this._options = options || {};
this.init(this._options)
};
ctor.prototype.init = function(options){
this.title = options.title;
this.param = options.param;
this.hash = '#extras/optional/' + this.param;
};
ctor.prototype.isActive = function(){
return router.activeInstruction().fragment === this.hash.replace('#', '');
};
return ctor;
});
Step 4 Navigation view
<div class="navbar">
<div class="navbar-inner">
<ul class="nav" data-bind="foreach: navigation">
<li data-bind="css: { active: isActive() }">
<a data-bind="attr: { href: hash }, html: title"></a>
</li>
</ul>
<div class="loader pull-right" data-bind="css: { active: childRouter.isNavigating }">
<i class="icon-spinner icon-2x icon-spin"></i>
</div>
</div>
<div>
<h3 data-bind="text: param"></h3>
</div>
</div>
Live version can be seen at: http://dfiddle.github.io/dFiddle-2.0/#extras/optional.
Feel free to fork and adjust to your liking.

How to use icon in routes using Durandal?

I was looking around the documentation of the structures of these route-info object, but I didn't find any. Since Durandal use other JS libs, I don't sure what is this belong to (maybe sammy?).
I am facing 2 problems:
Problem #1 I want to use an icon in the route information, and I found that I could use title or caption to accomplish that...
Ugly Option 1: using icon info in the caption
{ route: 'dashboard', title: 'Dashboard', moduleId: 'viewmodels/dashboard', nav: true, caption: 'icon-dashboard' },
and do some binding like this:
<i data-bind="attr: { 'class': caption}"></i>
<a data-bind="attr: { href: hash }, html: title"></a>
or Ugly Option2: using icon html code in the model
{ route: 'dashboard', title: 'Dashboard', moduleId: 'viewmodels/dashboard', nav: true, caption: '<i class="icon-plus-sign"></i> Dashboard' }
and the binding will be:
<a data-bind="attr: { href: hash }, html: caption"></a>
I personally like option 1, because the is separation of data and display. But the property (caption) is not the best place to put it... what other options are??? I saw people using setting, but again, what settings-options are?? can I create my own icon property?
Other Problem How to design sub-menu... if is there a property to reference a parent-route??
Update 8/23/2013 I found this info about Child Routers
You can add your own properties to the route object. This is the good thing about JavaScript!
So as you say, you could add a settings object to the route like this:
{ route: 'dashboard', title: 'Dashboard', moduleId: 'viewmodels/dashboard', nav: true, settings : { caption: 'icon-dashboard', another :'property'} }
And just do the binding the same way you were doing:
<i data-bind="attr: { 'class': settings.caption}"></i>
<a data-bind="attr: { href: hash }, text: title"></a>
Just make sure that all your objects that will be bound in the UI contain the settings.captionor add extra logic in the binding to manage route objects that which property isundefined.