Browse Source

Merge pull request #5333 from abpframework/feat/5026

Created ReplaceableComponentsService and used it instead of ReplaceableComponentsState
pull/5350/head
Levent Arman Özak 6 years ago
committed by GitHub
parent
commit
c7043b3ccf
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 77
      docs/en/UI/Angular/Component-Replacement.md
  2. 12
      docs/en/UI/Angular/Permission-Management-Component-Replacement.md
  3. 3
      npm/ng-packs/packages/core/src/lib/actions/replaceable-components.actions.ts
  4. 9
      npm/ng-packs/packages/core/src/lib/components/dynamic-layout.component.ts
  5. 9
      npm/ng-packs/packages/core/src/lib/components/replaceable-route-container.component.ts
  6. 9
      npm/ng-packs/packages/core/src/lib/directives/replaceable-template.directive.ts
  7. 1
      npm/ng-packs/packages/core/src/lib/services/index.ts
  8. 57
      npm/ng-packs/packages/core/src/lib/services/replaceable-components.service.ts
  9. 42
      npm/ng-packs/packages/core/src/lib/states/replaceable-components.state.ts
  10. 63
      npm/ng-packs/packages/core/src/lib/tests/dynamic-layout.component.spec.ts
  11. 21
      npm/ng-packs/packages/core/src/lib/tests/replaceable-route-container.component.spec.ts
  12. 23
      npm/ng-packs/packages/core/src/lib/tests/replaceable-template.directive.spec.ts
  13. 17
      npm/ng-packs/packages/core/src/lib/utils/route-utils.ts
  14. 39
      npm/ng-packs/packages/theme-basic/src/lib/providers/styles.provider.ts

77
docs/en/UI/Angular/Component-Replacement.md

@ -8,27 +8,22 @@ The reason that you **can replace** but **cannot customize** default ABP compone
Create a new component that you want to use instead of an ABP component. Add that component to `declarations` and `entryComponents` in the `AppModule`. Create a new component that you want to use instead of an ABP component. Add that component to `declarations` and `entryComponents` in the `AppModule`.
Then, open the `app.component.ts` and dispatch the `AddReplaceableComponent` action to replace your component with an ABP component as shown below: Then, open the `app.component.ts` and execute the `add` method of `ReplaceableComponentsService` to replace your component with an ABP component as shown below:
```js ```js
import { AddReplaceableComponent } from '@abp/ng.core'; // imported AddReplaceableComponent action import { ReplaceableComponentsService } from '@abp/ng.core'; // imported ReplaceableComponentsService
import { eIdentityComponents } from '@abp/ng.identity'; // imported eIdentityComponents enum import { eIdentityComponents } from '@abp/ng.identity'; // imported eIdentityComponents enum
import { Store } from '@ngxs/store'; // imported Store
//... //...
@Component(/* component metadata */) @Component(/* component metadata */)
export class AppComponent { export class AppComponent {
constructor( constructor(
private store: Store // injected Store private replaceableComponents: ReplaceableComponentsService, // injected the service
) ) {
{ this.replaceableComponents.add({
// dispatched the AddReplaceableComponent action component: YourNewRoleComponent,
this.store.dispatch( key: eIdentityComponents.Roles,
new AddReplaceableComponent({ });
component: YourNewRoleComponent,
key: eIdentityComponents.Roles,
}),
);
} }
} }
``` ```
@ -59,28 +54,24 @@ Add the following code in your layout template (`my-layout.component.html`) wher
Open `app.component.ts` in `src/app` folder and modify it as shown below: Open `app.component.ts` in `src/app` folder and modify it as shown below:
```js ```js
import { AddReplaceableComponent } from '@abp/ng.core'; // imported AddReplaceableComponent import { ReplaceableComponentsService } from '@abp/ng.core'; // imported ReplaceableComponentsService
import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // imported eThemeBasicComponents enum for component keys import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // imported eThemeBasicComponents enum for component keys
import { Store } from '@ngxs/store'; // imported Store
import { MyApplicationLayoutComponent } from './my-application-layout/my-application-layout.component'; // imported MyApplicationLayoutComponent import { MyApplicationLayoutComponent } from './my-application-layout/my-application-layout.component'; // imported MyApplicationLayoutComponent
@Component(/* component metadata */) @Component(/* component metadata */)
export class AppComponent { export class AppComponent {
constructor( constructor(
private store: Store, // injected Store private replaceableComponents: ReplaceableComponentsService, // injected the service
) { ) {
// dispatched the AddReplaceableComponent action this.replaceableComponents.add({
this.store.dispatch( component: MyApplicationLayoutComponent,
new AddReplaceableComponent({ key: eThemeBasicComponents.ApplicationLayout,
component: MyApplicationLayoutComponent, });
key: eThemeBasicComponents.ApplicationLayout,
}),
);
} }
} }
``` ```
> If you like to replace a layout component at runtime (e.g: changing the layout by pressing a button), pass the second parameter of the AddReplaceableComponent action as true. DynamicLayoutComponent loads content using a router-outlet. When the second parameter of AddReplaceableComponent is true, the route will be refreshed, so use it with caution. Your component state will be gone and any initiation logic (including HTTP requests) will be repeated. > If you like to replace a layout component at runtime (e.g: changing the layout by pressing a button), pass the second parameter of the `add` method of `ReplaceableComponentsService` as true. DynamicLayoutComponent loads content using a router-outlet. When the second parameter of the `add` method is true, the route will be refreshed, so use it with caution. Your component state will be gone and any initiation logic (including HTTP requests) will be repeated.
### Layout Components ### Layout Components
@ -123,26 +114,22 @@ export class LogoComponent {}
Open `app.component.ts` in `src/app` folder and modify it as shown below: Open `app.component.ts` in `src/app` folder and modify it as shown below:
```js ```js
import { ..., AddReplaceableComponent } from '@abp/ng.core'; // imported AddReplaceableComponent import { ..., ReplaceableComponentsService } from '@abp/ng.core'; // imported ReplaceableComponentsService
import { Store } from '@ngxs/store'; // imported Store
import { LogoComponent } from './logo/logo.component'; // imported NavItemsComponent import { LogoComponent } from './logo/logo.component'; // imported NavItemsComponent
import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // imported eThemeBasicComponents import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // imported eThemeBasicComponents
//... //...
@Component(/* component metadata */) @Component(/* component metadata */)
export class AppComponent implements OnInit { export class AppComponent implements OnInit {
constructor(..., private store: Store) {} // injected Store constructor(..., private replaceableComponents: ReplaceableComponentsService) {} // injected ReplaceableComponentsService
ngOnInit() { ngOnInit() {
//... //...
// added dispatch this.replaceableComponents.add({
this.store.dispatch(
new AddReplaceableComponent({
component: LogoComponent, component: LogoComponent,
key: eThemeBasicComponents.Logo, key: eThemeBasicComponents.Logo,
}), });
);
} }
} }
``` ```
@ -166,7 +153,7 @@ yarn ng generate component routes --entryComponent
Open the generated `routes.component.ts` in `src/app/routes` folder and replace its content with the following: Open the generated `routes.component.ts` in `src/app/routes` folder and replace its content with the following:
```js ```js
import { ABP, ReplaceableComponents } from '@abp/ng.core'; import { ABP } from '@abp/ng.core';
import { import {
Component, Component,
HostBinding, HostBinding,
@ -309,26 +296,22 @@ Open the generated `routes.component.html` in `src/app/routes` folder and replac
Open `app.component.ts` in `src/app` folder and modify it as shown below: Open `app.component.ts` in `src/app` folder and modify it as shown below:
```js ```js
import { ..., AddReplaceableComponent } from '@abp/ng.core'; // imported AddReplaceableComponent import { ..., ReplaceableComponentsService } from '@abp/ng.core'; // imported ReplaceableComponentsService
import { Store } from '@ngxs/store'; // imported Store
import { RoutesComponent } from './routes/routes.component'; // imported NavItemsComponent import { RoutesComponent } from './routes/routes.component'; // imported NavItemsComponent
import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // imported eThemeBasicComponents import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // imported eThemeBasicComponents
//... //...
@Component(/* component metadata */) @Component(/* component metadata */)
export class AppComponent implements OnInit { export class AppComponent implements OnInit {
constructor(..., private store: Store) {} // injected Store constructor(..., private replaceableComponents: ReplaceableComponentsService) {} // injected ReplaceableComponentsService
ngOnInit() { ngOnInit() {
//... //...
// added dispatch this.replaceableComponents.add({
this.store.dispatch(
new AddReplaceableComponent({
component: RoutesComponent, component: RoutesComponent,
key: eThemeBasicComponents.Routes, key: eThemeBasicComponents.Routes,
}), });
);
} }
} }
``` ```
@ -511,26 +494,22 @@ Open the generated `nav-items.component.html` in `src/app/nav-items` folder and
Open `app.component.ts` in `src/app` folder and modify it as shown below: Open `app.component.ts` in `src/app` folder and modify it as shown below:
```js ```js
import { ..., AddReplaceableComponent } from '@abp/ng.core'; // imported AddReplaceableComponent import { ..., ReplaceableComponentsService } from '@abp/ng.core'; // imported ReplaceableComponentsService
import { Store } from '@ngxs/store'; // imported Store
import { NavItemsComponent } from './nav-items/nav-items.component'; // imported NavItemsComponent import { NavItemsComponent } from './nav-items/nav-items.component'; // imported NavItemsComponent
import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // imported eThemeBasicComponents import { eThemeBasicComponents } from '@abp/ng.theme.basic'; // imported eThemeBasicComponents
//... //...
@Component(/* component metadata */) @Component(/* component metadata */)
export class AppComponent implements OnInit { export class AppComponent implements OnInit {
constructor(..., private store: Store) {} // injected Store constructor(..., private replaceableComponents: ReplaceableComponentsService) {} // injected ReplaceableComponentsService
ngOnInit() { ngOnInit() {
//... //...
// added dispatch this.replaceableComponents.add({
this.store.dispatch(
new AddReplaceableComponent({
component: NavItemsComponent, component: NavItemsComponent,
key: eThemeBasicComponents.NavItems, key: eThemeBasicComponents.NavItems,
}), });
);
} }
} }
``` ```

12
docs/en/UI/Angular/Permission-Management-Component-Replacement.md

@ -473,24 +473,20 @@ Open the generated `permission-management.component.html` in `src/app/permission
Open `app.component.ts` in `src/app` folder and modify it as shown below: Open `app.component.ts` in `src/app` folder and modify it as shown below:
```js ```js
import { AddReplaceableComponent } from '@abp/ng.core'; import { ReplaceableComponentsService } from '@abp/ng.core';
import { ePermissionManagementComponents } from '@abp/ng.permission-management'; import { ePermissionManagementComponents } from '@abp/ng.permission-management';
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { Store } from '@ngxs/store';
import { PermissionManagementComponent } from './permission-management/permission-management.component'; import { PermissionManagementComponent } from './permission-management/permission-management.component';
//... //...
export class AppComponent implements OnInit { export class AppComponent implements OnInit {
constructor(private store: Store) {} // injected store constructor(private replaceableComponents: ReplaceableComponentsService) {} // injected ReplaceableComponentsService
ngOnInit() { ngOnInit() {
// added dispatching the AddReplaceableComponent action this.replaceableComponents.add({
this.store.dispatch(
new AddReplaceableComponent({
component: PermissionManagementComponent, component: PermissionManagementComponent,
key: ePermissionManagementComponents.PermissionManagement, key: ePermissionManagementComponents.PermissionManagement,
}) });
);
} }
} }
``` ```

3
npm/ng-packs/packages/core/src/lib/actions/replaceable-components.actions.ts

@ -1,7 +1,8 @@
import { ReplaceableComponents } from '../models/replaceable-components'; import { ReplaceableComponents } from '../models/replaceable-components';
// tslint:disable: max-line-length
/** /**
* @see usage: https://github.com/abpframework/abp/pull/2522#issue-358333183 * @deprecated To be deleted in v4.0. Use ReplaceableComponentsService instead. See the doc (https://docs.abp.io/en/abp/latest/UI/Angular/Component-Replacement)
*/ */
export class AddReplaceableComponent { export class AddReplaceableComponent {
static readonly type = '[ReplaceableComponents] Add'; static readonly type = '[ReplaceableComponents] Add';

9
npm/ng-packs/packages/core/src/lib/components/dynamic-layout.component.ts

@ -1,13 +1,12 @@
import { Component, Injector, Optional, SkipSelf, Type } from '@angular/core'; import { Component, Injector, Optional, SkipSelf, Type } from '@angular/core';
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router'; import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
import { Store } from '@ngxs/store';
import { eLayoutType } from '../enums/common'; import { eLayoutType } from '../enums/common';
import { ABP } from '../models'; import { ABP } from '../models';
import { ReplaceableComponents } from '../models/replaceable-components'; import { ReplaceableComponents } from '../models/replaceable-components';
import { LocalizationService } from '../services/localization.service'; import { LocalizationService } from '../services/localization.service';
import { ReplaceableComponentsService } from '../services/replaceable-components.service';
import { RoutesService } from '../services/routes.service'; import { RoutesService } from '../services/routes.service';
import { SubscriptionService } from '../services/subscription.service'; import { SubscriptionService } from '../services/subscription.service';
import { ReplaceableComponentsState } from '../states/replaceable-components.state';
import { findRoute, getRoutePath } from '../utils/route-utils'; import { findRoute, getRoutePath } from '../utils/route-utils';
import { TreeNode } from '../utils/tree-utils'; import { TreeNode } from '../utils/tree-utils';
@ -37,7 +36,7 @@ export class DynamicLayoutComponent {
constructor( constructor(
injector: Injector, injector: Injector,
private localizationService: LocalizationService, private localizationService: LocalizationService,
private store: Store, private replaceableComponents: ReplaceableComponentsService,
private subscription: SubscriptionService, private subscription: SubscriptionService,
@Optional() @SkipSelf() dynamicLayoutComponent: DynamicLayoutComponent, @Optional() @SkipSelf() dynamicLayoutComponent: DynamicLayoutComponent,
) { ) {
@ -67,7 +66,7 @@ export class DynamicLayoutComponent {
if (!expectedLayout) expectedLayout = eLayoutType.empty; if (!expectedLayout) expectedLayout = eLayoutType.empty;
const key = this.layouts.get(expectedLayout); const key = this.layouts.get(expectedLayout);
this.layout = this.getComponent(key).component; this.layout = this.getComponent(key)?.component;
} }
}); });
@ -82,6 +81,6 @@ export class DynamicLayoutComponent {
} }
private getComponent(key: string): ReplaceableComponents.ReplaceableComponent { private getComponent(key: string): ReplaceableComponents.ReplaceableComponent {
return this.store.selectSnapshot(ReplaceableComponentsState.getComponent(key)); return this.replaceableComponents.get(key);
} }
} }

9
npm/ng-packs/packages/core/src/lib/components/replaceable-route-container.component.ts

@ -1,10 +1,9 @@
import { Component, OnInit, Type } from '@angular/core'; import { Component, OnInit, Type } from '@angular/core';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { Store } from '@ngxs/store';
import { distinctUntilChanged } from 'rxjs/operators'; import { distinctUntilChanged } from 'rxjs/operators';
import { ReplaceableComponents } from '../models/replaceable-components'; import { ReplaceableComponents } from '../models/replaceable-components';
import { ReplaceableComponentsService } from '../services/replaceable-components.service';
import { SubscriptionService } from '../services/subscription.service'; import { SubscriptionService } from '../services/subscription.service';
import { ReplaceableComponentsState } from '../states/replaceable-components.state';
@Component({ @Component({
selector: 'abp-replaceable-route-container', selector: 'abp-replaceable-route-container',
@ -22,7 +21,7 @@ export class ReplaceableRouteContainerComponent implements OnInit {
constructor( constructor(
private route: ActivatedRoute, private route: ActivatedRoute,
private store: Store, private replaceableComponents: ReplaceableComponentsService,
private subscription: SubscriptionService, private subscription: SubscriptionService,
) {} ) {}
@ -31,8 +30,8 @@ export class ReplaceableRouteContainerComponent implements OnInit {
this.componentKey = (this.route.snapshot.data this.componentKey = (this.route.snapshot.data
.replaceableComponent as ReplaceableComponents.RouteData).key; .replaceableComponent as ReplaceableComponents.RouteData).key;
const component$ = this.store const component$ = this.replaceableComponents
.select(ReplaceableComponentsState.getComponent(this.componentKey)) .get$(this.componentKey)
.pipe(distinctUntilChanged()); .pipe(distinctUntilChanged());
this.subscription.addOne( this.subscription.addOne(

9
npm/ng-packs/packages/core/src/lib/directives/replaceable-template.directive.ts

@ -10,15 +10,14 @@ import {
Type, Type,
ViewContainerRef, ViewContainerRef,
} from '@angular/core'; } from '@angular/core';
import { Store } from '@ngxs/store';
import compare from 'just-compare'; import compare from 'just-compare';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { filter } from 'rxjs/operators'; import { filter } from 'rxjs/operators';
import snq from 'snq'; import snq from 'snq';
import { ABP } from '../models/common'; import { ABP } from '../models/common';
import { ReplaceableComponents } from '../models/replaceable-components'; import { ReplaceableComponents } from '../models/replaceable-components';
import { ReplaceableComponentsService } from '../services/replaceable-components.service';
import { SubscriptionService } from '../services/subscription.service'; import { SubscriptionService } from '../services/subscription.service';
import { ReplaceableComponentsState } from '../states/replaceable-components.state';
@Directive({ selector: '[abpReplaceableTemplate]', providers: [SubscriptionService] }) @Directive({ selector: '[abpReplaceableTemplate]', providers: [SubscriptionService] })
export class ReplaceableTemplateDirective implements OnInit, OnChanges { export class ReplaceableTemplateDirective implements OnInit, OnChanges {
@ -45,7 +44,7 @@ export class ReplaceableTemplateDirective implements OnInit, OnChanges {
private templateRef: TemplateRef<any>, private templateRef: TemplateRef<any>,
private cfRes: ComponentFactoryResolver, private cfRes: ComponentFactoryResolver,
private vcRef: ViewContainerRef, private vcRef: ViewContainerRef,
private store: Store, private replaceableComponents: ReplaceableComponentsService,
private subscription: SubscriptionService, private subscription: SubscriptionService,
) { ) {
this.context = { this.context = {
@ -58,8 +57,8 @@ export class ReplaceableTemplateDirective implements OnInit, OnChanges {
} }
ngOnInit() { ngOnInit() {
const component$ = this.store const component$ = this.replaceableComponents
.select(ReplaceableComponentsState.getComponent(this.data.componentKey)) .get$(this.data.componentKey)
.pipe( .pipe(
filter( filter(
(res = {} as ReplaceableComponents.ReplaceableComponent) => (res = {} as ReplaceableComponents.ReplaceableComponent) =>

1
npm/ng-packs/packages/core/src/lib/services/index.ts

@ -9,6 +9,7 @@ export * from './localization.service';
export * from './multi-tenancy.service'; export * from './multi-tenancy.service';
export * from './profile-state.service'; export * from './profile-state.service';
export * from './profile.service'; export * from './profile.service';
export * from './replaceable-components.service';
export * from './rest.service'; export * from './rest.service';
export * from './routes.service'; export * from './routes.service';
export * from './session-state.service'; export * from './session-state.service';

57
npm/ng-packs/packages/core/src/lib/services/replaceable-components.service.ts

@ -0,0 +1,57 @@
import { Injectable, NgZone } from '@angular/core';
import { Router } from '@angular/router';
import { ReplaceableComponents } from '../models/replaceable-components';
import { BehaviorSubject, Observable } from 'rxjs';
import { noop } from '../utils/common-utils';
import { map, filter } from 'rxjs/operators';
import { InternalStore } from '../utils/internal-store-utils';
import { reloadRoute } from '../utils/route-utils';
@Injectable({ providedIn: 'root' })
export class ReplaceableComponentsService {
private store: InternalStore<ReplaceableComponents.ReplaceableComponent[]>;
get replaceableComponents$(): Observable<ReplaceableComponents.ReplaceableComponent[]> {
return this.store.sliceState(state => state);
}
get replaceableComponents(): ReplaceableComponents.ReplaceableComponent[] {
return this.store.state;
}
get onUpdate$(): Observable<ReplaceableComponents.ReplaceableComponent[]> {
return this.store.sliceUpdate(state => state);
}
constructor(private ngZone: NgZone, private router: Router) {
this.store = new InternalStore([]);
}
add(replaceableComponent: ReplaceableComponents.ReplaceableComponent, reload?: boolean): void {
const replaceableComponents = [...this.store.state];
const index = replaceableComponents.findIndex(
component => component.key === replaceableComponent.key,
);
if (index > -1) {
replaceableComponents[index] = replaceableComponent;
} else {
replaceableComponents.push(replaceableComponent);
}
this.store.patch(replaceableComponents);
if (reload) reloadRoute(this.router, this.ngZone);
}
get(replaceableComponentKey: string): ReplaceableComponents.ReplaceableComponent {
return this.replaceableComponents.find(component => component.key === replaceableComponentKey);
}
get$(replaceableComponentKey: string): Observable<ReplaceableComponents.ReplaceableComponent> {
return this.replaceableComponents$.pipe(
map(components => components.find(component => component.key === replaceableComponentKey)),
);
}
}

42
npm/ng-packs/packages/core/src/lib/states/replaceable-components.state.ts

@ -1,11 +1,23 @@
import { Injectable, NgZone } from '@angular/core'; import { Injectable, isDevMode } from '@angular/core';
import { Router } from '@angular/router';
import { Action, createSelector, Selector, State, StateContext } from '@ngxs/store'; import { Action, createSelector, Selector, State, StateContext } from '@ngxs/store';
import snq from 'snq'; import snq from 'snq';
import { AddReplaceableComponent } from '../actions/replaceable-components.actions'; import { AddReplaceableComponent } from '../actions/replaceable-components.actions';
import { ReplaceableComponents } from '../models/replaceable-components'; import { ReplaceableComponents } from '../models/replaceable-components';
import { noop } from '../utils/common-utils'; import { ReplaceableComponentsService } from '../services/replaceable-components.service';
function logDeprecationMsg() {
if (isDevMode()) {
console.warn(`
ReplacableComponentsState has been deprecated. Use ReplaceableComponentsService instead.
See the doc https://docs.abp.io/en/abp/latest/UI/Angular/Component-Replacement
`);
}
}
// tslint:disable: max-line-length
/**
* @deprecated To be deleted in v4.0. Use ReplaceableComponentsService instead. See the doc (https://docs.abp.io/en/abp/latest/UI/Angular/Component-Replacement)
*/
@State<ReplaceableComponents.State>({ @State<ReplaceableComponents.State>({
name: 'ReplaceableComponentsState', name: 'ReplaceableComponentsState',
defaults: { replaceableComponents: [] } as ReplaceableComponents.State, defaults: { replaceableComponents: [] } as ReplaceableComponents.State,
@ -16,6 +28,7 @@ export class ReplaceableComponentsState {
static getAll({ static getAll({
replaceableComponents, replaceableComponents,
}: ReplaceableComponents.State): ReplaceableComponents.ReplaceableComponent[] { }: ReplaceableComponents.State): ReplaceableComponents.ReplaceableComponent[] {
logDeprecationMsg();
return replaceableComponents || []; return replaceableComponents || [];
} }
@ -23,6 +36,7 @@ export class ReplaceableComponentsState {
const selector = createSelector( const selector = createSelector(
[ReplaceableComponentsState], [ReplaceableComponentsState],
(state: ReplaceableComponents.State): ReplaceableComponents.ReplaceableComponent => { (state: ReplaceableComponents.State): ReplaceableComponents.ReplaceableComponent => {
logDeprecationMsg();
return snq(() => state.replaceableComponents.find(component => component.key === key)); return snq(() => state.replaceableComponents.find(component => component.key === key));
}, },
); );
@ -30,29 +44,15 @@ export class ReplaceableComponentsState {
return selector; return selector;
} }
constructor(private ngZone: NgZone, private router: Router) {} constructor(private service: ReplaceableComponentsService) {}
// TODO: Create a shared service for route reload and more
private reloadRoute() {
const { shouldReuseRoute } = this.router.routeReuseStrategy;
const setRouteReuse = (reuse: typeof shouldReuseRoute) => {
this.router.routeReuseStrategy.shouldReuseRoute = reuse;
};
setRouteReuse(() => false);
this.router.navigated = false;
this.ngZone.run(async () => {
await this.router.navigateByUrl(this.router.url).catch(noop);
setRouteReuse(shouldReuseRoute);
});
}
@Action(AddReplaceableComponent) @Action(AddReplaceableComponent)
replaceableComponentsAction( replaceableComponentsAction(
{ getState, patchState }: StateContext<ReplaceableComponents.State>, { getState, patchState }: StateContext<ReplaceableComponents.State>,
{ payload, reload }: AddReplaceableComponent, { payload, reload }: AddReplaceableComponent,
) { ) {
logDeprecationMsg();
let { replaceableComponents } = getState(); let { replaceableComponents } = getState();
const index = snq( const index = snq(
@ -69,6 +69,6 @@ export class ReplaceableComponentsState {
replaceableComponents, replaceableComponents,
}); });
if (reload) this.reloadRoute(); this.service.add(payload, reload);
} }
} }

63
npm/ng-packs/packages/core/src/lib/tests/dynamic-layout.component.spec.ts

@ -7,7 +7,11 @@ import { NEVER } from 'rxjs';
import { DynamicLayoutComponent, RouterOutletComponent } from '../components'; import { DynamicLayoutComponent, RouterOutletComponent } from '../components';
import { eLayoutType } from '../enums/common'; import { eLayoutType } from '../enums/common';
import { ABP } from '../models'; import { ABP } from '../models';
import { ApplicationConfigurationService, RoutesService } from '../services'; import {
ApplicationConfigurationService,
RoutesService,
ReplaceableComponentsService,
} from '../services';
import { ReplaceableComponentsState } from '../states'; import { ReplaceableComponentsState } from '../states';
@Component({ @Component({
@ -78,33 +82,7 @@ const routes: ABP.Route[] = [
}, },
]; ];
const storeData = {
ReplaceableComponentsState: {
replaceableComponents: [
{
key: 'Theme.ApplicationLayoutComponent',
component: DummyApplicationLayoutComponent,
},
{
key: 'Theme.AccountLayoutComponent',
component: DummyAccountLayoutComponent,
},
{
key: 'Theme.EmptyLayoutComponent',
component: DummyEmptyLayoutComponent,
},
],
},
};
describe('DynamicLayoutComponent', () => { describe('DynamicLayoutComponent', () => {
const mockActions: Actions = NEVER;
const mockStore = ({
selectSnapshot() {
return true;
},
} as unknown) as Store;
const createComponent = createRoutingFactory({ const createComponent = createRoutingFactory({
component: RouterOutletComponent, component: RouterOutletComponent,
stubsEnabled: false, stubsEnabled: false,
@ -113,10 +91,16 @@ describe('DynamicLayoutComponent', () => {
providers: [ providers: [
{ {
provide: RoutesService, provide: RoutesService,
useFactory: () => new RoutesService(mockActions, mockStore), useFactory: () =>
new RoutesService(NEVER, ({
selectSnapshot() {
return true;
},
} as unknown) as Store),
}, },
ReplaceableComponentsService,
], ],
imports: [RouterModule, DummyLayoutModule, NgxsModule.forRoot([ReplaceableComponentsState])], imports: [RouterModule, DummyLayoutModule, NgxsModule.forRoot()],
routes: [ routes: [
{ path: '', component: RouterOutletComponent }, { path: '', component: RouterOutletComponent },
{ {
@ -163,15 +147,26 @@ describe('DynamicLayoutComponent', () => {
}); });
let spectator: SpectatorRouting<RouterOutletComponent>; let spectator: SpectatorRouting<RouterOutletComponent>;
let store: Store; let replaceableComponents: ReplaceableComponentsService;
beforeEach(async () => { beforeEach(async () => {
spectator = createComponent(); spectator = createComponent();
store = spectator.inject(Store); replaceableComponents = spectator.inject(ReplaceableComponentsService);
const routesService = spectator.inject(RoutesService); const routesService = spectator.inject(RoutesService);
routesService.add(routes); routesService.add(routes);
store.reset(storeData); replaceableComponents.add({
key: 'Theme.ApplicationLayoutComponent',
component: DummyApplicationLayoutComponent,
});
replaceableComponents.add({
key: 'Theme.AccountLayoutComponent',
component: DummyAccountLayoutComponent,
});
replaceableComponents.add({
key: 'Theme.EmptyLayoutComponent',
component: DummyEmptyLayoutComponent,
});
}); });
it('should handle application layout from parent abp route and display it', async () => { it('should handle application layout from parent abp route and display it', async () => {
@ -204,8 +199,8 @@ describe('DynamicLayoutComponent', () => {
}); });
it('should not display any layout when layouts are empty', async () => { it('should not display any layout when layouts are empty', async () => {
store.reset({ ...storeData, ReplaceableComponentsState: {} }); const spy = jest.spyOn(replaceableComponents, 'get');
spy.mockReturnValue(null);
spectator.detectChanges(); spectator.detectChanges();
spectator.router.navigateByUrl('/withoutLayout'); spectator.router.navigateByUrl('/withoutLayout');

21
npm/ng-packs/packages/core/src/lib/tests/replaceable-route-container.component.spec.ts

@ -1,10 +1,9 @@
import { createHostFactory, SpectatorHost } from '@ngneat/spectator/jest';
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { Store } from '@ngxs/store'; import { createHostFactory, SpectatorHost } from '@ngneat/spectator/jest';
import { of, Subject, BehaviorSubject } from 'rxjs'; import { BehaviorSubject } from 'rxjs';
import { ReplaceableRouteContainerComponent } from '../components/replaceable-route-container.component'; import { ReplaceableRouteContainerComponent } from '../components/replaceable-route-container.component';
import { ReplaceableComponentsState } from '../states'; import { ReplaceableComponentsService } from '../services/replaceable-components.service';
@Component({ @Component({
selector: 'abp-external-component', selector: 'abp-external-component',
@ -30,18 +29,18 @@ const activatedRouteMock = {
}; };
describe('ReplaceableRouteContainerComponent', () => { describe('ReplaceableRouteContainerComponent', () => {
const selectResponse = new BehaviorSubject(undefined);
const mockSelect = jest.fn(() => selectResponse);
let spectator: SpectatorHost<ReplaceableRouteContainerComponent>; let spectator: SpectatorHost<ReplaceableRouteContainerComponent>;
const get$Res = new BehaviorSubject(undefined);
const createHost = createHostFactory({ const createHost = createHostFactory({
component: ReplaceableRouteContainerComponent, component: ReplaceableRouteContainerComponent,
providers: [ providers: [
{ provide: ActivatedRoute, useValue: activatedRouteMock }, { provide: ActivatedRoute, useValue: activatedRouteMock },
{ provide: Store, useValue: { select: mockSelect } }, { provide: ReplaceableComponentsService, useValue: { get$: () => get$Res } },
], ],
declarations: [ExternalComponent, DefaultComponent], declarations: [ExternalComponent, DefaultComponent],
entryComponents: [DefaultComponent, ExternalComponent], entryComponents: [DefaultComponent, ExternalComponent],
mocks: [Router],
}); });
beforeEach(() => { beforeEach(() => {
@ -55,11 +54,11 @@ describe('ReplaceableRouteContainerComponent', () => {
}); });
it("should display the external component if it's available in store.", () => { it("should display the external component if it's available in store.", () => {
selectResponse.next({ component: ExternalComponent }); get$Res.next({ component: ExternalComponent });
spectator.detectChanges(); spectator.detectChanges();
expect(spectator.query('p')).toHaveText('external'); expect(spectator.query('p')).toHaveText('external');
selectResponse.next({ component: null }); get$Res.next({ component: null });
spectator.detectChanges(); spectator.detectChanges();
expect(spectator.query('p')).toHaveText('default'); expect(spectator.query('p')).toHaveText('default');
}); });

23
npm/ng-packs/packages/core/src/lib/tests/replaceable-template.directive.spec.ts

@ -1,9 +1,11 @@
import { Component, EventEmitter, Inject, Input, OnInit, Optional, Output } from '@angular/core'; import { Component, EventEmitter, Inject, Input, OnInit, Optional, Output } from '@angular/core';
import { createDirectiveFactory, SpectatorDirective } from '@ngneat/spectator/jest'; import { createDirectiveFactory, SpectatorDirective } from '@ngneat/spectator/jest';
import { Store } from '@ngxs/store'; import { Store } from '@ngxs/store';
import { Subject } from 'rxjs'; import { BehaviorSubject } from 'rxjs';
import { ReplaceableTemplateDirective } from '../directives'; import { ReplaceableTemplateDirective } from '../directives';
import { ReplaceableComponents } from '../models'; import { ReplaceableComponents } from '../models';
import { Router } from '@angular/router';
import { ReplaceableComponentsService } from '../services/replaceable-components.service';
@Component({ @Component({
selector: 'abp-default-component', selector: 'abp-default-component',
@ -48,15 +50,15 @@ class ExternalComponent {
} }
describe('ReplaceableTemplateDirective', () => { describe('ReplaceableTemplateDirective', () => {
const selectResponse = new Subject();
const mockSelect = jest.fn(() => selectResponse);
let spectator: SpectatorDirective<ReplaceableTemplateDirective>; let spectator: SpectatorDirective<ReplaceableTemplateDirective>;
const get$Res = new BehaviorSubject(undefined);
const createDirective = createDirectiveFactory({ const createDirective = createDirectiveFactory({
directive: ReplaceableTemplateDirective, directive: ReplaceableTemplateDirective,
providers: [{ provide: Store, useValue: { select: mockSelect } }],
declarations: [DefaultComponent, ExternalComponent], declarations: [DefaultComponent, ExternalComponent],
entryComponents: [ExternalComponent], entryComponents: [ExternalComponent],
mocks: [Router],
providers: [{ provide: ReplaceableComponentsService, useValue: { get$: () => get$Res } }],
}); });
describe('without external component', () => { describe('without external component', () => {
@ -72,7 +74,7 @@ describe('ReplaceableTemplateDirective', () => {
`, `,
{ hostProps: { oneWay: { label: 'Test' }, twoWay: false, twoWayChange, someOutput } }, { hostProps: { oneWay: { label: 'Test' }, twoWay: false, twoWayChange, someOutput } },
); );
selectResponse.next(undefined);
const component = spectator.query(DefaultComponent); const component = spectator.query(DefaultComponent);
spectator.directive.context.initTemplate(component); spectator.directive.context.initTemplate(component);
spectator.detectChanges(); spectator.detectChanges();
@ -114,7 +116,8 @@ describe('ReplaceableTemplateDirective', () => {
`, `,
{ hostProps: { oneWay: { label: 'Test' }, twoWay: false, twoWayChange, someOutput } }, { hostProps: { oneWay: { label: 'Test' }, twoWay: false, twoWayChange, someOutput } },
); );
selectResponse.next({ component: ExternalComponent, key: 'TestModule.TestComponent' });
get$Res.next({ component: ExternalComponent, key: 'TestModule.TestComponent' });
}); });
afterEach(() => twoWayChange.mockClear()); afterEach(() => twoWayChange.mockClear());
@ -150,7 +153,7 @@ describe('ReplaceableTemplateDirective', () => {
const externalComponent = spectator.query(ExternalComponent); const externalComponent = spectator.query(ExternalComponent);
spectator.setHostInput({ oneWay: 'test' }); spectator.setHostInput({ oneWay: 'test' });
externalComponent.data.inputs.twoWay = true; externalComponent.data.inputs.twoWay = true;
selectResponse.next({ component: null, key: 'TestModule.TestComponent' }); get$Res.next({ component: null, key: 'TestModule.TestComponent' });
spectator.detectChanges(); spectator.detectChanges();
const component = spectator.query(DefaultComponent); const component = spectator.query(DefaultComponent);
spectator.directive.context.initTemplate(component); spectator.directive.context.initTemplate(component);
@ -161,14 +164,14 @@ describe('ReplaceableTemplateDirective', () => {
}); });
it('should reset default component subscriptions', () => { it('should reset default component subscriptions', () => {
selectResponse.next({ component: null, key: 'TestModule.TestComponent' }); get$Res.next({ component: null, key: 'TestModule.TestComponent' });
const component = spectator.query(DefaultComponent); const component = spectator.query(DefaultComponent);
spectator.directive.context.initTemplate(component); spectator.directive.context.initTemplate(component);
spectator.detectChanges(); spectator.detectChanges();
const unsubscribe = jest.fn(() => {}); const unsubscribe = jest.fn(() => {});
spectator.directive.defaultComponentSubscriptions.twoWayChange.unsubscribe = unsubscribe; spectator.directive.defaultComponentSubscriptions.twoWayChange.unsubscribe = unsubscribe;
selectResponse.next({ component: ExternalComponent, key: 'TestModule.TestComponent' }); get$Res.next({ component: ExternalComponent, key: 'TestModule.TestComponent' });
expect(unsubscribe).toHaveBeenCalled(); expect(unsubscribe).toHaveBeenCalled();
}); });
}); });

17
npm/ng-packs/packages/core/src/lib/utils/route-utils.ts

@ -2,6 +2,8 @@ import { PRIMARY_OUTLET, Router, UrlSegmentGroup } from '@angular/router';
import { ABP } from '../models/common'; import { ABP } from '../models/common';
import { RoutesService } from '../services/routes.service'; import { RoutesService } from '../services/routes.service';
import { TreeNode } from './tree-utils'; import { TreeNode } from './tree-utils';
import { noop } from './common-utils';
import { NgZone } from '@angular/core';
export function findRoute(routes: RoutesService, path: string): TreeNode<ABP.Route> { export function findRoute(routes: RoutesService, path: string): TreeNode<ABP.Route> {
const node = routes.find(route => route.path === path); const node = routes.find(route => route.path === path);
@ -23,3 +25,18 @@ export function getRoutePath(router: Router, url = router.url) {
return '/' + (primaryGroup || emptyGroup).segments.map(({ path }) => path).join('/'); return '/' + (primaryGroup || emptyGroup).segments.map(({ path }) => path).join('/');
} }
export function reloadRoute(router: Router, ngZone: NgZone) {
const { shouldReuseRoute } = router.routeReuseStrategy;
const setRouteReuse = (reuse: typeof shouldReuseRoute) => {
router.routeReuseStrategy.shouldReuseRoute = reuse;
};
setRouteReuse(() => false);
router.navigated = false;
ngZone.run(async () => {
await router.navigateByUrl(router.url).catch(noop);
setRouteReuse(shouldReuseRoute);
});
}

39
npm/ng-packs/packages/theme-basic/src/lib/providers/styles.provider.ts

@ -1,4 +1,4 @@
import { AddReplaceableComponent, CONTENT_STRATEGY, DomInsertionService } from '@abp/ng.core'; import { ReplaceableComponentsService, CONTENT_STRATEGY, DomInsertionService } from '@abp/ng.core';
import { APP_INITIALIZER } from '@angular/core'; import { APP_INITIALIZER } from '@angular/core';
import { Store } from '@ngxs/store'; import { Store } from '@ngxs/store';
import { AccountLayoutComponent } from '../components/account-layout/account-layout.component'; import { AccountLayoutComponent } from '../components/account-layout/account-layout.component';
@ -11,32 +11,33 @@ export const BASIC_THEME_STYLES_PROVIDERS = [
{ {
provide: APP_INITIALIZER, provide: APP_INITIALIZER,
useFactory: configureStyles, useFactory: configureStyles,
deps: [DomInsertionService, Store], deps: [DomInsertionService, ReplaceableComponentsService],
multi: true, multi: true,
}, },
]; ];
export function configureStyles(domInsertion: DomInsertionService, store: Store) { export function configureStyles(
domInsertion: DomInsertionService,
replaceableComponents: ReplaceableComponentsService,
) {
return () => { return () => {
domInsertion.insertContent(CONTENT_STRATEGY.AppendStyleToHead(styles)); domInsertion.insertContent(CONTENT_STRATEGY.AppendStyleToHead(styles));
initLayouts(store); initLayouts(replaceableComponents);
}; };
} }
function initLayouts(store: Store) { function initLayouts(replaceableComponents: ReplaceableComponentsService) {
store.dispatch([ replaceableComponents.add({
new AddReplaceableComponent({ key: eThemeBasicComponents.ApplicationLayout,
key: eThemeBasicComponents.ApplicationLayout, component: ApplicationLayoutComponent,
component: ApplicationLayoutComponent, });
}), replaceableComponents.add({
new AddReplaceableComponent({ key: eThemeBasicComponents.AccountLayout,
key: eThemeBasicComponents.AccountLayout, component: AccountLayoutComponent,
component: AccountLayoutComponent, });
}), replaceableComponents.add({
new AddReplaceableComponent({ key: eThemeBasicComponents.EmptyLayout,
key: eThemeBasicComponents.EmptyLayout, component: EmptyLayoutComponent,
component: EmptyLayoutComponent, });
}),
]);
} }

Loading…
Cancel
Save