Skip to content

Commit

Permalink
feat(stark-core): add support for deep state navigation for states fr…
Browse files Browse the repository at this point in the history
…om lazy loaded modules. Adapt Showcase to make DemoModule and NewsModule lazy loaded

ISSUES CLOSED: #810
  • Loading branch information
christophercr committed Oct 31, 2018
1 parent 22d45de commit 4f731e3
Show file tree
Hide file tree
Showing 26 changed files with 514 additions and 289 deletions.
1 change: 1 addition & 0 deletions packages/stark-core/src/modules/session.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from "./session/actions";
export * from "./session/components";
export * from "./session/entities";
export * from "./session/reducers";
export * from "./session/services";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
import { Component, Inject, OnInit, ViewEncapsulation } from "@angular/core";
import {
STARK_LOGGING_SERVICE,
STARK_ROUTING_SERVICE,
starkAppExitStateName,
starkAppInitStateName,
StarkLoggingService,
StarkRoutingService
} from "@nationalbankbelgium/stark-core";
import { STARK_LOGGING_SERVICE, StarkLoggingService } from "../../logging/services";
import { STARK_ROUTING_SERVICE, StarkRoutingService } from "../../routing/services";
import { starkAppExitStateName, starkAppInitStateName } from "../routes";

/**
* Name of the component
*/
const componentName: string = "stark-app-container";

/**
* Component to coordinate the display of the init/exit states when the application starts or ends and hide the application content.
* For any other state it simply displays the application content hiding any init/exit state.
*/
@Component({
selector: "stark-app-container",
templateUrl: "./app-container.component.html",
encapsulation: ViewEncapsulation.None,
// tslint:disable-next-line: use-host-property-decorator
host: {
class: componentName
}
Expand All @@ -40,7 +40,7 @@ export class StarkAppContainerComponent implements OnInit {
}

/**
* Check if the current state is a "session-ui" state (with "starkAppInit" or "starkAppExit" as parent state name)
* Check if the current state is an init or exit state (with "starkAppInit" or "starkAppExit" as parent state name)
*/
public isAppInitOrExitState(): boolean {
return (
Expand Down
108 changes: 108 additions & 0 deletions packages/stark-core/src/modules/session/routes.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
import { Location } from "@angular/common";
import { Transition, StateDeclaration, LazyLoadResult, RawParams } from "@uirouter/core";
import { loadNgModule, Ng2StateDeclaration, NgModuleToLoad } from "@uirouter/angular";
import { from, Observable, of } from "rxjs";
import { map } from "rxjs/operators";
import { STARK_ROUTING_SERVICE, StarkRoutingService, StarkStateConfigWithParams } from "../routing/services";

/**
* Name of the initialization states of the application
*/
Expand Down Expand Up @@ -42,3 +49,104 @@ export const starkSessionLogoutStateName: string = starkAppExitStateName + ".sta
* URL of the SessionLogout state of the application
*/
export const starkSessionLogoutStateUrl: string = "/starkSessionLogout";

/**
* Configuration of the route state of the application
*/
export function resolveTargetRoute(
$location: Location,
$transition$: Transition,
routingService: StarkRoutingService
): Promise<StarkStateConfigWithParams | undefined> {
/**
* Get the corresponding registered state that matches with the given URL.
* If the state is part of a lazy loaded module, then such module is loaded first and then the URL is searched again to get the correct state
*/
function getTargetStateByUrl(targetUrl: string): StarkStateConfigWithParams | undefined {
let targetState: StarkStateConfigWithParams | undefined = routingService.getStateConfigByUrlPath(targetUrl);

// skip any init/exit state
const initOrExitStateRegex: RegExp = new RegExp("(" + starkAppInitStateName + "|" + starkAppExitStateName + ")");

if (
targetState &&
(<Function>targetState.state.$$state)().parent &&
(<Function>targetState.state.$$state)().parent.name.match(initOrExitStateRegex)
) {
targetState = undefined;
}

return targetState;
}

// get the path of the current URL in the browser's navigation bar
const targetUrlPath: string = $location.path();
const targetRoute: StarkStateConfigWithParams | undefined = getTargetStateByUrl(targetUrlPath);
let finalTargetRoute$: Observable<StarkStateConfigWithParams | undefined> = of(targetRoute);

// in case the state is part of a module to load lazily, we need to load it and search the url again
if (targetRoute && (<Function>targetRoute.state.$$state)().loadChildren) {
// so we call the needed function to lazy load the module
const moduleToLoad: NgModuleToLoad = (<Function>targetRoute.state.$$state)().loadChildren;
const lazyLoadNgModule: (transition: Transition, stateObject: StateDeclaration) => Promise<LazyLoadResult> = loadNgModule(
moduleToLoad
);

// once the module is loaded lazily, we search again for the right state and return the result
finalTargetRoute$ = from(lazyLoadNgModule($transition$, targetRoute.state)).pipe(
map((_lazyLoadResult: LazyLoadResult) => {
return getTargetStateByUrl(targetUrlPath);
})
);
}

return finalTargetRoute$.toPromise();
}

/**
* Check if targetRoute is defined and returns the name of the state OR undefined.
* @param targetRoute - returned value of resolveTargetRoute method
*/
export function resolveTargetState(targetRoute?: StarkStateConfigWithParams): Promise<string | undefined> {
return of(typeof targetRoute !== "undefined" ? targetRoute.state.name : undefined).toPromise();
}

/**
* Check if targetRoute is defined and returns the params of the state OR undefined.
* @param targetRoute - returned value of resolveTargetRoute method
*/
export function resolveTargetStateParams(targetRoute?: StarkStateConfigWithParams): Promise<RawParams | undefined> {
return of(typeof targetRoute !== "undefined" ? targetRoute.paramValues : undefined).toPromise();
}

/**
* States defined by Session Module
*/
export const SESSION_STATES: Ng2StateDeclaration[] = [
{
name: starkAppInitStateName, // parent state for any initialization state (used to show/hide the main app component)
abstract: true,

resolve: [
{
token: "targetRoute",
deps: [Location, Transition, STARK_ROUTING_SERVICE],
resolveFn: resolveTargetRoute
},
{
token: "targetState",
deps: ["targetRoute"],
resolveFn: resolveTargetState
},
{
token: "targetStateParams",
deps: ["targetRoute"],
resolveFn: resolveTargetStateParams
}
]
},
{
name: starkAppExitStateName, // parent state for any exit state (used to show/hide the main app component)
abstract: true
}
];
61 changes: 57 additions & 4 deletions packages/stark-core/src/modules/session/session.module.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,25 @@
import { ModuleWithProviders, NgModule, Optional, SkipSelf } from "@angular/core";
import { ApplicationInitStatus, Inject, ModuleWithProviders, NgModule, Optional, SkipSelf } from "@angular/core";
import { CommonModule, Location } from "@angular/common";
import { StoreModule } from "@ngrx/store";
import { UIRouterModule } from "@uirouter/angular";
import { from } from "rxjs";
import { starkSessionReducers } from "./reducers";
import { StarkSessionConfig, STARK_SESSION_CONFIG } from "./entities";
import { STARK_SESSION_SERVICE, StarkSessionServiceImpl } from "./services";
import { StarkUserModule } from "../user/user.module";
import { STARK_ROUTING_SERVICE, StarkRoutingService } from "../routing/services";
import { SESSION_STATES, starkLoginStateName, starkPreloadingStateName } from "./routes";
import { StarkAppContainerComponent } from "./components";

@NgModule({
imports: [StoreModule.forFeature("StarkSession", starkSessionReducers), StarkUserModule]
imports: [
CommonModule,
StoreModule.forFeature("StarkSession", starkSessionReducers),
UIRouterModule.forChild({
states: SESSION_STATES
})
],
declarations: [StarkAppContainerComponent],
exports: [StarkAppContainerComponent]
})
export class StarkSessionModule {
/**
Expand All @@ -20,6 +33,7 @@ export class StarkSessionModule {
return {
ngModule: StarkSessionModule,
providers: [
Location,
{ provide: STARK_SESSION_SERVICE, useClass: StarkSessionServiceImpl },
sessionConfig ? { provide: STARK_SESSION_CONFIG, useValue: sessionConfig } : []
]
Expand All @@ -30,14 +44,53 @@ export class StarkSessionModule {
* Prevents this module from being re-imported
* @link https://angular.io/guide/singleton-services#prevent-reimport-of-the-coremodule
* @param parentModule - the parent module
* @param routingService - The routing service of the application
* @param sessionConfig - The configuration of the session module
* @param appInitStatus - A class that reflects the state of running {@link APP_INITIALIZER}s
*/
public constructor(
@Optional()
@SkipSelf()
parentModule: StarkSessionModule
parentModule: StarkSessionModule,
@Inject(STARK_ROUTING_SERVICE) routingService: StarkRoutingService,
appInitStatus: ApplicationInitStatus,
@Optional()
@Inject(STARK_SESSION_CONFIG)
sessionConfig?: StarkSessionConfig
) {
if (parentModule) {
throw new Error("StarkSessionModule is already loaded. Import it in the AppModule only");
}

// this logic cannot be executed in an APP_INITIALIZER factory because the StarkRoutingService uses the StarkLoggingService
// which needs the "logging" state to be already defined in the Store (which NGRX defines internally via APP_INITIALIZER factories :p)
from(appInitStatus.donePromise).subscribe(() => {
if (ENV === "development") {
const loginStateName: string = this.getStateName(
starkLoginStateName,
sessionConfig ? sessionConfig.loginStateName : undefined
);
routingService.navigateTo(loginStateName);
} else {
const preloadingStateName: string = this.getStateName(
starkPreloadingStateName,
sessionConfig ? sessionConfig.preloadingStateName : undefined
);
routingService.navigateTo(preloadingStateName);
}
});
}

/**
* @ignore
*/
private getStateName(defaultState: string, configState?: string): string {
let finalStateName: string = defaultState;

if (typeof configState !== "undefined" && configState !== "") {
finalStateName = configState;
}

return finalStateName;
}
}
1 change: 1 addition & 0 deletions packages/stark-core/testing/tsconfig-build.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"allowEmptyCodegenFiles": false,
"annotateForClosureCompiler": true,
"skipTemplateCodegen": true,
"enableResourceInlining": true,
"flatModuleOutFile": "testing.js",
"flatModuleId": "@nationalbankbelgium/stark-core/testing"
}
Expand Down
8 changes: 3 additions & 5 deletions packages/stark-ui/src/modules/app-menu/app-menu.module.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
import { NgModule } from "@angular/core";
import { StarkAppMenuComponent, StarkAppMenuItemComponent } from "./components";
import { TranslateModule } from "@ngx-translate/core";
import { CommonModule } from "@angular/common";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { MatDividerModule } from "@angular/material/divider";
import { MatExpansionModule } from "@angular/material/expansion";
import { MatIconModule } from "@angular/material/icon";
import { MatListModule } from "@angular/material/list";
import { StarkSvgViewBoxModule } from "../svg-view-box/svg-view-box.module";
import { TranslateModule } from "@ngx-translate/core";
import { UIRouterModule } from "@uirouter/angular";
import { StarkAppMenuComponent, StarkAppMenuItemComponent } from "./components";
import { StarkSvgViewBoxModule } from "../svg-view-box/svg-view-box.module";

@NgModule({
declarations: [StarkAppMenuComponent, StarkAppMenuItemComponent],
imports: [
BrowserAnimationsModule,
CommonModule,
MatListModule,
MatDividerModule,
Expand Down
7 changes: 3 additions & 4 deletions packages/stark-ui/src/modules/dropdown/dropdown.module.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import { NgModule } from "@angular/core";
import { StarkDropdownComponent } from "./components";
import { TranslateModule } from "@ngx-translate/core";
import { CommonModule } from "@angular/common";
import { FormsModule } from "@angular/forms";
import { MatOptionModule } from "@angular/material/core";
import { MatSelectModule } from "@angular/material/select";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { TranslateModule } from "@ngx-translate/core";
import { StarkDropdownComponent } from "./components";

@NgModule({
declarations: [StarkDropdownComponent],
imports: [CommonModule, TranslateModule, FormsModule, MatSelectModule, MatOptionModule, BrowserAnimationsModule],
imports: [CommonModule, TranslateModule, FormsModule, MatSelectModule, MatOptionModule],
exports: [StarkDropdownComponent]
})
export class StarkDropdownModule {}
4 changes: 2 additions & 2 deletions packages/stark-ui/src/modules/pagination/pagination.module.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { NgModule } from "@angular/core";
import { CommonModule } from "@angular/common";
import { FormsModule } from "@angular/forms";
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { MatIconModule } from "@angular/material/icon";
import { MatInputModule } from "@angular/material/input";
import { MatButtonModule } from "@angular/material/button";
Expand All @@ -15,7 +15,7 @@ import { StarkDropdownModule } from "../dropdown/dropdown.module";
declarations: [StarkPaginationComponent],
exports: [StarkPaginationComponent],
imports: [
BrowserAnimationsModule,
CommonModule,
FormsModule,
MatButtonModule,
MatIconModule,
Expand Down
1 change: 0 additions & 1 deletion packages/stark-ui/src/modules/session-ui.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
export * from "./session-ui/session-ui.module";
export * from "./session-ui/pages";
export * from "./session-ui/components";
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@ import {
const componentName: string = "stark-login-page";

/**
* Login Page smart component
* Login Page smart component.
* **This page is to be used only in the development environment.**
*
* It shows a list of user profiles (provided by mock data or a back-end) that the user can choose and use it to impersonate himself as someone else.
* This makes it easy to run the application with different roles.
*/
@Component({
selector: "stark-login-page",
Expand All @@ -29,13 +33,30 @@ const componentName: string = "stark-login-page";
}
})
export class StarkLoginPageComponent implements OnInit {
/**
* Target page to navigate to after the user profile is loaded and automatically logged in.
*/
@Input()
public targetState: string;

/**
* Params to pass to the target page (if any).
*/
@Input()
public targetStateParams: RawParams;

/**
* User profiles to be displayed in the list where the user can choose from.
*/
public users: StarkUser[];

/**
* Class constructor
* @param logger - The logger of the application
* @param userService - The user service of the application
* @param sessionService - The session service of the application
* @param routingService - The routing service of the application
*/
public constructor(
@Inject(STARK_LOGGING_SERVICE) public logger: StarkLoggingService,
@Inject(STARK_USER_SERVICE) public userService: StarkUserService,
Expand Down
Loading

0 comments on commit 4f731e3

Please sign in to comment.