Browse Source

Merge pull request #3383 from abpframework/feat/replaceable-layouts

Made layouts replaceable
pull/3408/head
Levent Arman Özak 6 years ago
committed by GitHub
parent
commit
eb73c4c9b0
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 53
      docs/en/UI/Angular/Component-Replacement.md
  2. 6
      docs/en/UI/Angular/Service-Proxies.md
  3. 2
      npm/ng-packs/angular.json
  4. 3
      npm/ng-packs/apps/dev-app/src/app/app.module.ts
  5. 41
      npm/ng-packs/packages/core/src/lib/components/dynamic-layout.component.ts
  6. 6
      npm/ng-packs/packages/core/src/lib/models/common.ts
  7. 102
      npm/ng-packs/packages/core/src/lib/tests/dynamic-layout.component.spec.ts
  8. 23
      npm/ng-packs/packages/theme-basic/src/lib/services/initial.service.ts
  9. 6
      templates/app/angular/src/app/app.module.ts
  10. 5
      templates/module/angular/.prettierrc
  11. 6
      templates/module/angular/src/app/app.module.ts

53
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 {
![Example Usage](./images/component-replacement.gif)
## 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 `<router-outlet></router-outlet>` 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
<router-outlet></router-outlet>
```
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 |
| -------------------------------------------------- | --------------------------------------------- |

6
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)

2
npm/ng-packs/angular.json

@ -526,5 +526,5 @@
}
}
},
"defaultProject": "core"
"defaultProject": "dev-app"
}

3
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: '/' }),

41
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<Config.Requirements>;
layout: Type<any>;
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() {}
}

6
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<Config.Environment>;
requirements: Config.Requirements;
/**
*
* @deprecated To be deleted in v3.0
*/
requirements?: Config.Requirements;
}
export type PagedResponse<T> = {

102
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: '<router-outlet></router-outlet>',
})
class DummyApplicationLayoutComponent {
static type = eLayoutType.application;
}
class DummyApplicationLayoutComponent {}
@Component({
selector: 'abp-layout-account',
template: '<router-outlet></router-outlet>',
})
class DummyAccountLayoutComponent {
static type = eLayoutType.account;
}
class DummyAccountLayoutComponent {}
@Component({
selector: 'abp-layout-empty',
template: '<router-outlet></router-outlet>',
})
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<RouterOutletComponent>;
let store: SpyObject<Store>;
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();

23
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() {

6
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: '/' }),

5
templates/module/angular/.prettierrc

@ -0,0 +1,5 @@
{
"printWidth": 100,
"singleQuote": true,
"trailingComma": "all"
}

6
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([]),

Loading…
Cancel
Save