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