diff --git a/docs/en/UI/Angular/Component-Replacement.md b/docs/en/UI/Angular/Component-Replacement.md
index b17c61ae73..d718b46117 100644
--- a/docs/en/UI/Angular/Component-Replacement.md
+++ b/docs/en/UI/Angular/Component-Replacement.md
@@ -1,10 +1,10 @@
-# Component Replacement
+## Component Replacement
You can replace some ABP components with your custom components.
The reason that you **can replace** but **cannot customize** default ABP components is disabling or changing a part of that component can cause problems. So we named those components as _Replaceable Components_.
-## How to Replace a Component
+### How to Replace a Component
Create a new component that you want to use instead of an ABP component. Add that component to `declarations` and `entryComponents` in the `AppModule`.
@@ -29,7 +29,54 @@ export class AppComponent {

-## Available Replaceable Components
+
+### How to Replace a Layout
+
+Each ABP theme module has 3 layouts named `ApplicationLayoutComponent`, `AccountLayoutComponent`, `EmptyLayoutComponent`. These layouts can be replaced with the same way.
+
+> A layout component template should contain `` element.
+
+The below example describes how to replace the `ApplicationLayoutComponent`:
+
+Run the following command to generate a layout in `angular` folder:
+
+```bash
+yarn ng generate component shared/my-application-layout --export --entryComponent
+
+# You don't need the --entryComponent option in Angular 9
+```
+
+Add the following code in your layout template (`my-layout.component.html`) where you want the page to be loaded.
+
+```html
+
+```
+
+Open the `app.component.ts` and add the below content:
+
+```js
+import { ..., AddReplaceableComponent } from '@abp/ng.core'; // imported AddReplaceableComponent
+import { MyApplicationLayoutComponent } from './shared/my-application-layout/my-application-layout.component'; // imported MyApplicationLayoutComponent
+import { Store } from '@ngxs/store'; // imported Store
+//...
+export class AppComponent {
+ constructor(..., private store: Store) {} // injected Store
+
+ ngOnInit() {
+ // added below content
+ this.store.dispatch(
+ new AddReplaceableComponent({
+ component: MyApplicationLayoutComponent,
+ key: 'Theme.ApplicationLayoutComponent',
+ }),
+ );
+
+ //...
+ }
+}
+```
+
+### Available Replaceable Components
| Component key | Description |
| -------------------------------------------------- | --------------------------------------------- |
diff --git a/docs/en/UI/Angular/Service-Proxies.md b/docs/en/UI/Angular/Service-Proxies.md
index 97b43b7513..6209350349 100644
--- a/docs/en/UI/Angular/Service-Proxies.md
+++ b/docs/en/UI/Angular/Service-Proxies.md
@@ -25,9 +25,9 @@ The files generated with the `--module all` option like below:
### Services
-Each generated service matches a back-end controller. The services methods call back-end APIs via [RestService](./Http-Requests.md#restservice).
+Each generated service matches a back-end controller. The services methods call back-end APIs via [RestService](./Http-Requests#restservice).
-A variable named `apiName` (available as of v2.4) is defined in each service. `apiName` matches the module's RemoteServiceName. This variable passes to the `RestService` as a parameter at each request. If there is no microservice API defined in the environment, `RestService` uses the default. See [getting a specific API endpoint from application config](./Http-Requests.md#how-to-get-a-specific-api-endpoint-from-application-config)
+A variable named `apiName` (available as of v2.4) is defined in each service. `apiName` matches the module's RemoteServiceName. This variable passes to the `RestService` as a parameter at each request. If there is no microservice API defined in the environment, `RestService` uses the default. See [getting a specific API endpoint from application config](./Http-Requests#how-to-get-a-specific-api-endpoint-from-application-config)
The `providedIn` property of the services is defined as `'root'`. Therefore no need to add a service as a provider to a module. You can use a service by injecting it into a constructor as shown below:
@@ -64,4 +64,4 @@ Initial values can optionally be passed to each class constructor.
## What's Next?
-* [HTTP Requests](./Http-Requests.md)
+* [HTTP Requests](./Http-Requests)
diff --git a/npm/ng-packs/angular.json b/npm/ng-packs/angular.json
index d0289505fe..af185688b2 100644
--- a/npm/ng-packs/angular.json
+++ b/npm/ng-packs/angular.json
@@ -526,5 +526,5 @@
}
}
},
- "defaultProject": "core"
+ "defaultProject": "dev-app"
}
diff --git a/npm/ng-packs/apps/dev-app/src/app/app.module.ts b/npm/ng-packs/apps/dev-app/src/app/app.module.ts
index f1324c233a..64f8150f01 100644
--- a/npm/ng-packs/apps/dev-app/src/app/app.module.ts
+++ b/npm/ng-packs/apps/dev-app/src/app/app.module.ts
@@ -21,9 +21,6 @@ const LOGGERS = [NgxsLoggerPluginModule.forRoot({ disabled: false })];
imports: [
CoreModule.forRoot({
environment,
- requirements: {
- layouts: LAYOUTS,
- },
}),
ThemeSharedModule.forRoot(),
AccountConfigModule.forRoot({ redirectUrl: '/' }),
diff --git a/npm/ng-packs/packages/core/src/lib/components/dynamic-layout.component.ts b/npm/ng-packs/packages/core/src/lib/components/dynamic-layout.component.ts
index 7fc1826efe..e835dd777d 100644
--- a/npm/ng-packs/packages/core/src/lib/components/dynamic-layout.component.ts
+++ b/npm/ng-packs/packages/core/src/lib/components/dynamic-layout.component.ts
@@ -1,12 +1,12 @@
-import { Component, Input, OnDestroy, Type, Injector } from '@angular/core';
+import { Component, OnDestroy, Type } from '@angular/core';
import { ActivatedRoute, NavigationEnd, Router, UrlSegment } from '@angular/router';
-import { Select, Store } from '@ngxs/store';
-import { Observable } from 'rxjs';
+import { Store } from '@ngxs/store';
import snq from 'snq';
import { eLayoutType } from '../enums/common';
-import { Config } from '../models/config';
import { ABP } from '../models/common';
+import { ReplaceableComponents } from '../models/replaceable-components';
import { ConfigState } from '../states/config.state';
+import { ReplaceableComponentsState } from '../states/replaceable-components.state';
import { takeUntilDestroy } from '../utils/rxjs-utils';
@Component({
@@ -20,24 +20,10 @@ import { takeUntilDestroy } from '../utils/rxjs-utils';
`,
})
export class DynamicLayoutComponent implements OnDestroy {
- @Select(ConfigState.getOne('requirements')) requirements$: Observable;
-
layout: Type;
constructor(private router: Router, private route: ActivatedRoute, private store: Store) {
- const {
- requirements: { layouts },
- routes,
- } = this.store.selectSnapshot(ConfigState.getAll);
-
- if ((this.route.snapshot.data || {}).layout) {
- this.layout = layouts
- .filter(l => !!l)
- .find(
- (l: any) =>
- snq(() => l.type.toLowerCase().indexOf(this.route.snapshot.data.layout), -1) > -1,
- );
- }
+ const { routes } = this.store.selectSnapshot(ConfigState.getAll);
router.events.pipe(takeUntilDestroy(this)).subscribe(event => {
if (event instanceof NavigationEnd) {
@@ -45,15 +31,24 @@ export class DynamicLayoutComponent implements OnDestroy {
{ path: router.url.replace('/', '') },
] as any);
- const layout = (this.route.snapshot.data || {}).layout || findLayout(segments, routes);
+ const layouts = {
+ application: this.getComponent('Theme.ApplicationLayoutComponent'),
+ account: this.getComponent('Theme.AccountLayoutComponent'),
+ empty: this.getComponent('Theme.EmptyApplicationLayoutComponent'),
+ };
- this.layout = layouts
- .filter(l => !!l)
- .find((l: any) => snq(() => l.type.toLowerCase().indexOf(layout), -1) > -1);
+ const expectedLayout =
+ (this.route.snapshot.data || {}).layout || findLayout(segments, routes);
+
+ this.layout = layouts[expectedLayout].component;
}
});
}
+ getComponent(key: string): ReplaceableComponents.ReplaceableComponent {
+ return this.store.selectSnapshot(ReplaceableComponentsState.getComponent(key));
+ }
+
ngOnDestroy() {}
}
diff --git a/npm/ng-packs/packages/core/src/lib/models/common.ts b/npm/ng-packs/packages/core/src/lib/models/common.ts
index 8b4fb76cb0..79792a1b61 100644
--- a/npm/ng-packs/packages/core/src/lib/models/common.ts
+++ b/npm/ng-packs/packages/core/src/lib/models/common.ts
@@ -6,7 +6,11 @@ import { Subject } from 'rxjs';
export namespace ABP {
export interface Root {
environment: Partial;
- requirements: Config.Requirements;
+ /**
+ *
+ * @deprecated To be deleted in v3.0
+ */
+ requirements?: Config.Requirements;
}
export type PagedResponse = {
diff --git a/npm/ng-packs/packages/core/src/lib/tests/dynamic-layout.component.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/dynamic-layout.component.spec.ts
index 54a602b84a..0a123b6a4f 100644
--- a/npm/ng-packs/packages/core/src/lib/tests/dynamic-layout.component.spec.ts
+++ b/npm/ng-packs/packages/core/src/lib/tests/dynamic-layout.component.spec.ts
@@ -1,36 +1,36 @@
import { Component, NgModule } from '@angular/core';
import { ActivatedRoute, RouterModule } from '@angular/router';
-import { createRoutingFactory, SpectatorRouting, SpyObject } from '@ngneat/spectator/jest';
-import { Store } from '@ngxs/store';
+import { createRoutingFactory, SpectatorRouting } from '@ngneat/spectator/jest';
+import { NgxsModule, Store } from '@ngxs/store';
+import { DynamicLayoutComponent, RouterOutletComponent } from '../components';
import { eLayoutType } from '../enums';
import { ABP } from '../models';
-import { DynamicLayoutComponent, RouterOutletComponent } from '../components';
+import { ConfigState, ReplaceableComponentsState } from '../states';
+import { ApplicationConfigurationService } from '../services';
@Component({
selector: 'abp-layout-application',
template: '',
})
-class DummyApplicationLayoutComponent {
- static type = eLayoutType.application;
-}
+class DummyApplicationLayoutComponent {}
@Component({
selector: 'abp-layout-account',
template: '',
})
-class DummyAccountLayoutComponent {
- static type = eLayoutType.account;
-}
+class DummyAccountLayoutComponent {}
@Component({
selector: 'abp-layout-empty',
template: '',
})
-class DummyEmptyLayoutComponent {
- static type = eLayoutType.empty;
-}
+class DummyEmptyLayoutComponent {}
-const LAYOUTS = [DummyApplicationLayoutComponent, DummyAccountLayoutComponent, DummyEmptyLayoutComponent];
+const LAYOUTS = [
+ DummyApplicationLayoutComponent,
+ DummyAccountLayoutComponent,
+ DummyEmptyLayoutComponent,
+];
@NgModule({
imports: [RouterModule],
@@ -47,13 +47,57 @@ class DummyComponent {
constructor(public route: ActivatedRoute) {}
}
+const storeData = {
+ ConfigState: {
+ routes: [
+ {
+ path: '',
+ wrapper: true,
+ children: [
+ {
+ path: 'parentWithLayout',
+ layout: eLayoutType.application,
+ children: [
+ { path: 'childWithoutLayout' },
+ { path: 'childWithLayout', layout: eLayoutType.account },
+ ],
+ },
+ ],
+ },
+ { path: 'withData', layout: eLayoutType.application },
+ ,
+ ] as ABP.FullRoute[],
+ environment: { application: {} },
+ },
+ ReplaceableComponentsState: {
+ replaceableComponents: [
+ {
+ key: 'Theme.ApplicationLayoutComponent',
+ component: DummyApplicationLayoutComponent,
+ },
+ {
+ key: 'Theme.AccountLayoutComponent',
+ component: DummyAccountLayoutComponent,
+ },
+ {
+ key: 'Theme.EmptyLayoutComponent',
+ component: DummyEmptyLayoutComponent,
+ },
+ ],
+ },
+};
+
describe('DynamicLayoutComponent', () => {
const createComponent = createRoutingFactory({
component: RouterOutletComponent,
stubsEnabled: false,
- mocks: [Store],
declarations: [DummyComponent, DynamicLayoutComponent],
- imports: [RouterModule, DummyLayoutModule],
+ mocks: [ApplicationConfigurationService],
+ imports: [
+ RouterModule,
+ DummyLayoutModule,
+ NgxsModule.forRoot([ConfigState, ReplaceableComponentsState]),
+ ],
routes: [
{ path: '', component: RouterOutletComponent },
{
@@ -100,33 +144,13 @@ describe('DynamicLayoutComponent', () => {
});
let spectator: SpectatorRouting;
- let store: SpyObject;
- const mockStoreData = {
- requirements: { layouts: LAYOUTS },
- routes: [
- {
- path: '',
- wrapper: true,
- children: [
- {
- path: 'parentWithLayout',
- layout: eLayoutType.application,
- children: [{ path: 'childWithoutLayout' }, { path: 'childWithLayout', layout: eLayoutType.account }],
- },
- ],
- },
- { path: 'withData', layout: eLayoutType.application },
- ,
- ] as ABP.FullRoute[],
- environment: { application: {} },
- };
- let storeSpy: jest.SpyInstance;
+ let store: Store;
beforeEach(async () => {
spectator = createComponent();
store = spectator.get(Store);
- storeSpy = jest.spyOn(store, 'selectSnapshot');
- storeSpy.mockReturnValue(mockStoreData);
+
+ store.reset(storeData);
});
it('should handle application layout from parent abp route and display it', async () => {
@@ -159,7 +183,7 @@ describe('DynamicLayoutComponent', () => {
});
it('should not display any layout when layouts are empty', async () => {
- storeSpy.mockReturnValue({ ...mockStoreData, requirements: { layouts: [] } });
+ store.reset({ ...storeData, ReplaceableComponentsState: {} });
spectator.detectChanges();
diff --git a/npm/ng-packs/packages/theme-basic/src/lib/services/initial.service.ts b/npm/ng-packs/packages/theme-basic/src/lib/services/initial.service.ts
index 33c7c18770..21fb1756bc 100644
--- a/npm/ng-packs/packages/theme-basic/src/lib/services/initial.service.ts
+++ b/npm/ng-packs/packages/theme-basic/src/lib/services/initial.service.ts
@@ -1,12 +1,29 @@
+import { LazyLoadService, AddReplaceableComponent } from '@abp/ng.core';
import { Injectable } from '@angular/core';
-import { Router } from '@angular/router';
-import { LazyLoadService } from '@abp/ng.core';
+import { Store } from '@ngxs/store';
import styles from '../constants/styles';
+import { ApplicationLayoutComponent } from '../components/application-layout/application-layout.component';
+import { AccountLayoutComponent } from '../components/account-layout/account-layout.component';
+import { EmptyLayoutComponent } from '../components/empty-layout/empty-layout.component';
@Injectable({ providedIn: 'root' })
export class InitialService {
- constructor(private lazyLoadService: LazyLoadService) {
+ constructor(private lazyLoadService: LazyLoadService, private store: Store) {
this.appendStyle().subscribe();
+ this.store.dispatch([
+ new AddReplaceableComponent({
+ key: 'Theme.ApplicationLayoutComponent',
+ component: ApplicationLayoutComponent,
+ }),
+ new AddReplaceableComponent({
+ key: 'Theme.AccountLayoutComponent',
+ component: AccountLayoutComponent,
+ }),
+ new AddReplaceableComponent({
+ key: 'Theme.EmptyLayoutComponent',
+ component: EmptyLayoutComponent,
+ }),
+ ]);
}
appendStyle() {
diff --git a/templates/app/angular/src/app/app.module.ts b/templates/app/angular/src/app/app.module.ts
index 5df354fb38..928dff5172 100644
--- a/templates/app/angular/src/app/app.module.ts
+++ b/templates/app/angular/src/app/app.module.ts
@@ -3,7 +3,6 @@ import { CoreModule } from '@abp/ng.core';
import { IdentityConfigModule } from '@abp/ng.identity.config';
import { SettingManagementConfigModule } from '@abp/ng.setting-management.config';
import { TenantManagementConfigModule } from '@abp/ng.tenant-management.config';
-import { LAYOUTS } from '@abp/ng.theme.basic';
import { ThemeSharedModule } from '@abp/ng.theme.shared';
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
@@ -20,10 +19,7 @@ const LOGGERS = [NgxsLoggerPluginModule.forRoot({ disabled: false })];
@NgModule({
imports: [
CoreModule.forRoot({
- environment,
- requirements: {
- layouts: LAYOUTS
- }
+ environment
}),
ThemeSharedModule.forRoot(),
AccountConfigModule.forRoot({ redirectUrl: '/' }),
diff --git a/templates/module/angular/.prettierrc b/templates/module/angular/.prettierrc
new file mode 100644
index 0000000000..5e2863a11f
--- /dev/null
+++ b/templates/module/angular/.prettierrc
@@ -0,0 +1,5 @@
+{
+ "printWidth": 100,
+ "singleQuote": true,
+ "trailingComma": "all"
+}
diff --git a/templates/module/angular/src/app/app.module.ts b/templates/module/angular/src/app/app.module.ts
index c0fef36c9a..93e09321ec 100644
--- a/templates/module/angular/src/app/app.module.ts
+++ b/templates/module/angular/src/app/app.module.ts
@@ -3,7 +3,6 @@ import { CoreModule } from '@abp/ng.core';
import { IdentityConfigModule } from '@abp/ng.identity.config';
import { SettingManagementConfigModule } from '@abp/ng.setting-management.config';
import { TenantManagementConfigModule } from '@abp/ng.tenant-management.config';
-import { LAYOUTS } from '@abp/ng.theme.basic';
import { ThemeSharedModule } from '@abp/ng.theme.shared';
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
@@ -11,11 +10,11 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NgxsLoggerPluginModule } from '@ngxs/logger-plugin';
import { NgxsModule } from '@ngxs/store';
import { OAuthModule } from 'angular-oauth2-oidc';
+import { MyProjectNameConfigModule } from '../../projects/my-project-name-config/src/public-api';
import { environment } from '../environments/environment';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { SharedModule } from './shared/shared.module';
-import { MyProjectNameConfigModule } from '../../projects/my-project-name-config/src/public-api';
const LOGGERS = [NgxsLoggerPluginModule.forRoot({ disabled: false })];
@@ -25,9 +24,6 @@ const LOGGERS = [NgxsLoggerPluginModule.forRoot({ disabled: false })];
ThemeSharedModule.forRoot(),
CoreModule.forRoot({
environment,
- requirements: {
- layouts: LAYOUTS,
- },
}),
OAuthModule.forRoot(),
NgxsModule.forRoot([]),