Skip to content

Commit

Permalink
feat(stark-ui): add support for deep state navigation for states from…
Browse files Browse the repository at this point in the history
… 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 45b2a25
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 45b2a25

Please sign in to comment.