I have an example application created by VS2017 Angular template that is a single page app with 3 routes defined in app.module.ts
RouterModule.forRoot([
{ path: '', component: HomeComponent, pathMatch: 'full' },
{ path: 'counter', component: CounterComponent },
{ path: 'fetch-data', component: FetchDataComponent },
])
and in app.component.html
<body>
<app-nav-menu></app-nav-menu>
<div class="container">
<router-outlet></router-outlet>
</div>
</body>
where navigation is defiend in nav-menu.component.html
<header>
<nav class='navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3'>
<div class="container">
<a class="navbar-brand" [routerLink]='["/"]'>my_new_app</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target=".navbar-collapse" aria-label="Toggle navigation"
[attr.aria-expanded]="isExpanded" (click)="toggle()">
<span class="navbar-toggler-icon"></span>
</button>
<div class="navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse" [ngClass]='{"show": isExpanded}'>
<ul class="navbar-nav flex-grow">
<li class="nav-item" [routerLinkActive]='["link-active"]' [routerLinkActiveOptions]='{ exact: true }'>
<a class="nav-link text-dark" [routerLink]='["/"]'>Home</a>
</li>
<li class="nav-item" [routerLinkActive]='["link-active"]'>
<a class="nav-link text-dark" [routerLink]='["/counter"]'>Counter</a>
</li>
<li class="nav-item" [routerLinkActive]='["link-active"]'>
<a class="nav-link text-dark" [routerLink]='["/fetch-data"]'>Fetch data</a>
</li>
</ul>
</div>
</div>
</nav>
</header>
The normal situation with Counter selected would look like this (if the navigation was on the side):
Home | Counter |
Counter (x) | |
Fetch | |
In some cases I would need to have 2 "main level" components visible so that instead of having area in router outlet for 1 compponent the area would be splitted in 2 and 2 routes would be active somehow.
Home | Counter | Fetch |
Counter (x) | | |
Fetch (x) | | |
Can or should this be done with angular routing? The normal use would still be that only 1 route is active and the router-outlet area is not splitted.
This can be done e.g using ngIf and have (toggle)buttons instead of routerlinks. However I'm developing a very large application and I'm interested in using routes if possible.
The linked "duplicate" is about second router outlet and has nothing to do with this. Here what I'm trying to achieve is main router outlet having 2 routes active at same time and content is splitted. This is probably not possible, but that's the idea, not some sidebar secondary navigation.
One option I tried is to have 3 split-areas where 1 of them has a router-outlet. The other 2 have counter and fetch-data components as their content. When used as single page app only the 1st split area is visible.
app.component.html
<body>
<app-nav-menu></app-nav-menu>
<div id="working" >
<as-split direction="horizontal">
<as-split-area>
<router-outlet></router-outlet>
</as-split-area>
<as-split-area *ngIf="secondSplitAreaVisible">
<app-counter-component></app-counter-component>
</as-split-area>
<as-split-area *ngIf="thirdSplitAreaVisible">
<app-fetch-data></app-fetch-data>
</as-split-area>
</as-split>
</div>
</body>
The other 2 can be set visible with a checkbox in the navigation component that looks like below. Note that in my case it must be controlled that the component can be visible only once in GUI. This is done by using auth guards for routes and disabling the checkboxes mentioned above to prevent showing a aplit area for a component that is already visible in router-outlet.
nav-menu.component.html:
<header>
<nav class='navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3'>
<div class="container">
<a class="navbar-brand" [routerLink]='["/"]'>my_new_app</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target=".navbar-collapse" aria-label="Toggle navigation"
[attr.aria-expanded]="isExpanded" (click)="toggle()">
<span class="navbar-toggler-icon"></span>
</button>
<div class="navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse" [ngClass]='{"show": isExpanded}'>
<ul class="navbar-nav flex-grow">
<li class="nav-item" [routerLinkActive]='["link-active"]'>
<a class="nav-link text-dark" [routerLink]='["/home"]'><mat-checkbox [(ngModel)]="firstChecked" (change)="toggleTab('home')" [disabled]="firstDisabled"></mat-checkbox>Home</a>
</li>
<li class="nav-item" [routerLinkActive]='["link-active"]' [ngStyle]="{'border-bottom' : secondChecked || secondActive ? '2px solid' : '0px' }">
<a class="nav-link text-dark" [routerLink]='["/counter"]'>
<mat-checkbox [(ngModel)]="secondChecked" (change)="toggleTab('counter', secondChecked)" [disabled]="secondActive"></mat-checkbox>Counter</a>
</li>
<li class="nav-item" [routerLinkActive]='["link-active"]' [ngStyle]="{'border-bottom' : thirdChecked || thirdActive ? '2px solid' : '0px' }">
<a class="nav-link text-dark" [routerLink]='["/fetch-data"]'><mat-checkbox [(ngModel)]="thirdChecked" (change)="toggleTab('fetch-data', thirdChecked)" [disabled]="thirdActive"></mat-checkbox>Fetch data</a>
</li>
</ul>
</div>
</div>
</nav>
</header>
app.module.ts route definitions
RouterModule.forRoot([
{ path: 'home', component: HomeComponent, canActivate: [AuthGuard]},
{ path: 'counter', component: CounterComponent, canActivate: [AuthGuard] },
{ path: 'fetch-data', component: FetchDataComponent, canActivate: [AuthGuard]},
{ path: '', redirectTo: '/home', pathMatch: 'full' }
and the auth guard:
@Injectable({
providedIn: 'root',
})
export class AuthGuard implements CanActivate {
subscription;
outletUrl: string;
secondSplitAreaVisible: boolean = false;
thirdSplitAreaVisible: boolean = false;
constructor(
private router: Router,
private ngRedux: NgRedux<IAppState>,
private actions: TabActions) {
this.subscription = ngRedux.select<string>('outletUrl')
.subscribe(newUrl => this.outletUrl = newUrl); // <- New
this.subscription = ngRedux.select<boolean>('secondOpen') // <- New
.subscribe(newSecondVisible => this.secondSplitAreaVisible = newSecondVisible); // <- New
this.subscription = ngRedux.select<boolean>('thirdOpen') // <- New
.subscribe(newThirdVisible => this.thirdSplitAreaVisible = newThirdVisible); // <- New
}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
if (state.url === '/counter' && this.secondSplitAreaVisible) {
return false;
}
if (state.url === '/fetch-data' && this.thirdSplitAreaVisible) {
return false;
}
return true;
}
}
Above uses redux to manage state changes. That part is also below just in case someone is interested:
nav-menu.component.ts
@Component({
selector: 'app-nav-menu',
templateUrl: './nav-menu.component.html',
styleUrls: ['./nav-menu.component.css']
})
export class NavMenuComponent {
firstChecked: boolean = false;
secondChecked: boolean = false;
thirdChecked: boolean = false;
firstDisabled: boolean = true;
secondActive: boolean = false;
thirdActive: boolean = false;
constructor(
private ngRedux: NgRedux<IAppState>,
private actions: TabActions,
private router: Router) {
router.events.subscribe((event) => {
if (event instanceof NavigationEnd) {
this.ngRedux.dispatch(this.actions.setOutletActiveRoute(event.url));
if (event.url.includes('counter')) {
this.secondActive = true;
this.thirdActive = false;
this.firstChecked = false;
}
else if (event.url.includes('fetch')) {
this.thirdActive = true;
this.secondActive = false;
this.firstChecked = false;
}
else {
// home
this.secondActive = false;
this.thirdActive = false;
this.firstChecked = true;
}
}
});
}
isExpanded = false;
collapse() {
this.isExpanded = false;
}
toggle() {
this.isExpanded = !this.isExpanded;
}
toggleTab(name: string, isChecked : boolean) {
this.ngRedux.dispatch(this.actions.toggleSplitArea({ splitArea : name, isVisible: isChecked}));
}
}
app.component.ts
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnDestroy {
title = 'app';
secondSplitAreaVisible: boolean = false;
thirdSplitAreaVisible: boolean = false;
subscription;
constructor(
private ngRedux: NgRedux<IAppState>,
private actions: TabActions) {
this.subscription = ngRedux.select<boolean>('secondOpen')
.subscribe(newSecondVisible => {
this.secondSplitAreaVisible = newSecondVisible;
});
this.subscription = ngRedux.select<boolean>('thirdOpen')
.subscribe(newThirdVisible => {
this.thirdSplitAreaVisible = newThirdVisible;
});
}
ngOnDestroy() {
this.subscription.unsubscribe();
}
}
app.actions.ts
@Injectable()
export class TabActions {
static TOGGLE_SPLIT_AREA = 'TOGGLE_SPLIT_AREA';
static SET_OUTLET_ACTIVE_ROUTE = 'SET_OUTLET_ACTIVE_ROUTE';
toggleSplitArea(splitAreaToggle: SplitAreaToggle): SplitAreaToggleAction {
return {
type: TabActions.TOGGLE_SPLIT_AREA,
splitAreaToggle
};
}
setOutletActiveRoute(url: string) : SetOutletActiveRouteAction {
return {
type: TabActions.SET_OUTLET_ACTIVE_ROUTE,
url
};
}
}
store.ts
export interface IAppState {
outletUrl : string;
secondOpen : boolean;
thirdOpen : boolean;
};
export const INITIAL_STATE: IAppState = {
outletUrl: 'home',
secondOpen : false,
thirdOpen : false
};
export function rootReducer(lastState: IAppState, action: Action): IAppState {
switch(action.type) {
case TabActions.SET_OUTLET_ACTIVE_ROUTE: {
const setRouteAction = action as SetOutletActiveRouteAction;
const newState: IAppState = {
...lastState,
outletUrl: setRouteAction.url
}
return newState;
}
case TabActions.TOGGLE_SPLIT_AREA: {
const splitToggleAction = action as SplitAreaToggleAction;
console.log('rootreducer splitareatoggle:' + splitToggleAction.splitAreaToggle.splitArea);
if (splitToggleAction.splitAreaToggle.splitArea === 'counter') {
const newState: IAppState = {
...lastState,
secondOpen: splitToggleAction.splitAreaToggle.isVisible
}
return newState;
}
else {
const newState: IAppState = {
...lastState,
thirdOpen: splitToggleAction.splitAreaToggle.isVisible
}
return newState;
}
}
default : {
return lastState;
}
}
}