Angular 如果更改单页应用程序可以使用路由,以便 2 个路由在拆分视图中都可见吗?
Can Angular Routing be used if single page application is changed so that 2 routes can be both visible w splitted view?
我有一个由 VS2017 Angular template 创建的示例应用程序,它是一个单页应用程序,在 app.module.ts
中定义了 3 个路由
RouterModule.forRoot([
{ path: '', component: HomeComponent, pathMatch: 'full' },
{ path: 'counter', component: CounterComponent },
{ path: 'fetch-data', component: FetchDataComponent },
])
并在 app.component.html
<body>
<app-nav-menu></app-nav-menu>
<div class="container">
<router-outlet></router-outlet>
</div>
</body>
导航在 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>
选中计数器的正常情况如下所示(如果导航在侧面):
Home | Counter |
Counter (x) | |
Fetch | |
在某些情况下,我需要有 2 个 "main level" 组件可见,这样就不会在路由器出口中为 1 个组件设置区域,而是将区域分成 2 部分,并且 2 条路由会以某种方式处于活动状态。
Home | Counter | Fetch |
Counter (x) | | |
Fetch (x) | | |
可以或应该使用 angular 路由来完成吗?
正常使用还是只有1条route active,router-outlet区域不分。
这可以完成,例如使用 ngIf 并使用(切换)按钮而不是路由器链接。但是,我正在开发一个非常大的应用程序,如果可能的话,我有兴趣使用路由。
链接的"duplicate"是关于第二个路由器插座的,与此无关。在这里,我想要实现的是主路由器出口同时激活 2 条路由并且内容被拆分。这可能是不可能的,但这就是想法,而不是一些侧边栏辅助导航。
我尝试过的一个选择是有 3 个分割区域,其中 1 个有路由器插座。其他 2 个具有计数器和获取数据组件作为它们的内容。当用作单页应用程序时,只有第一个拆分区域可见。
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>
可以使用导航组件中的复选框将其他 2 个设置为可见,如下所示。请注意,在我的例子中,必须控制组件在 GUI 中只能显示一次。这是通过对路由使用 auth guards 并禁用上述复选框来完成的,以防止显示已在 router-outlet 中可见的组件的 aplit 区域。
导航-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 路由定义
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' }
和授权守卫:
@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;
}
}
上面使用 redux 来管理状态变化。该部分也在下面,以防万一有人感兴趣:
导航-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;
}
}
}
我有一个由 VS2017 Angular template 创建的示例应用程序,它是一个单页应用程序,在 app.module.ts
中定义了 3 个路由RouterModule.forRoot([
{ path: '', component: HomeComponent, pathMatch: 'full' },
{ path: 'counter', component: CounterComponent },
{ path: 'fetch-data', component: FetchDataComponent },
])
并在 app.component.html
<body>
<app-nav-menu></app-nav-menu>
<div class="container">
<router-outlet></router-outlet>
</div>
</body>
导航在 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>
选中计数器的正常情况如下所示(如果导航在侧面):
Home | Counter |
Counter (x) | |
Fetch | |
在某些情况下,我需要有 2 个 "main level" 组件可见,这样就不会在路由器出口中为 1 个组件设置区域,而是将区域分成 2 部分,并且 2 条路由会以某种方式处于活动状态。
Home | Counter | Fetch |
Counter (x) | | |
Fetch (x) | | |
可以或应该使用 angular 路由来完成吗? 正常使用还是只有1条route active,router-outlet区域不分。
这可以完成,例如使用 ngIf 并使用(切换)按钮而不是路由器链接。但是,我正在开发一个非常大的应用程序,如果可能的话,我有兴趣使用路由。
链接的"duplicate"是关于第二个路由器插座的,与此无关。在这里,我想要实现的是主路由器出口同时激活 2 条路由并且内容被拆分。这可能是不可能的,但这就是想法,而不是一些侧边栏辅助导航。
我尝试过的一个选择是有 3 个分割区域,其中 1 个有路由器插座。其他 2 个具有计数器和获取数据组件作为它们的内容。当用作单页应用程序时,只有第一个拆分区域可见。
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>
可以使用导航组件中的复选框将其他 2 个设置为可见,如下所示。请注意,在我的例子中,必须控制组件在 GUI 中只能显示一次。这是通过对路由使用 auth guards 并禁用上述复选框来完成的,以防止显示已在 router-outlet 中可见的组件的 aplit 区域。
导航-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 路由定义
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' }
和授权守卫:
@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;
}
}
上面使用 redux 来管理状态变化。该部分也在下面,以防万一有人感兴趣:
导航-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;
}
}
}