v2.9 to v3.0 Angular UI Migration Guide
What Changed in v3.0?
Angular 10
The new ABP Angular UI is based on Angular 10 and TypeScript 3.9, and we have dropped support for Angular 8. Nevertheless, ABP modules will keep working with Angular 9. Therefore, if your project is Angular 9, you do not need to update to Angular 10. The update is usually very easy though.
What to Do When Migrating?
Open a terminal at your root folder and run the following command:
yarn ng update @angular/cli @angular/core --force
This will make the following modifications:
- Update your package.json and install new packages
- Revise tsconfig.json files to create a "Solution Style" configuration
- Rename
browserslist
as.browserslistrc
On the other hand, it would be better if you check which packages to update first with yarn ng update
command alone. Angular will give you a list of packages to update.
When Angular reports the packages above, your command would look like this:
yarn ng update @angular/cli @angular/core ng-zorro-antd --force
If Angular complains about uncommited changes in your repo, you can either commit/stash them or add
--allow-dirty
parameter to the command.
Config Modules
In ABP v2.x, every lazy loaded module had a config module available via a separate package and module configuration was as follows:
import { AccountConfigModule } from '@abp/ng.account.config';
@NgModule({
imports: [
// other imports
AccountConfigModule.forRoot({ redirectUrl: '/' }),
],
// providers, declarations, and bootstrap
})
export class AppModule {}
...and in app-routing.module.ts...
const routes: Routes = [
// other route configuration
{
path: 'account',
loadChildren: () => import(
'./lazy-libs/account-wrapper.module'
).then(m => m.AccountWrapperModule),
},
];
Although working, this had a few disadvantages:
- Every module came in two independent packages, but in reality, those packages were interdependent.
- Configuring lazy loaded modules required a wrapper module.
- ABP Commercial had extensibility system and configuring extensible modules at the root module was increasing the bundle size.
In ABP v3.0, we have introduced a secondary entry points for each config module as well as a new way to configure lazy loaded modules without the wrappers. Now, the module configuration looks like this:
import { AccountConfigModule } from '@abp/ng.account/config';
@NgModule({
imports: [
// other imports
AccountConfigModule.forRoot(),
],
// providers, declarations, and bootstrap
})
export class AppModule {}
...and in app-routing.module.ts...
const routes: Routes = [
// other route configuration
{
path: 'account',
loadChildren: () => import('@abp/ng.account')
.then(m => m.AccountModule.forLazy({ redirectUrl: '/' })),
},
];
This change helped us reduce bundle size and build times substantially. We believe you will notice the difference in your apps.
A Better Example
AppModule:
import { AccountConfigModule } from '@abp/ng.account/config';
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 { ThemeBasicModule } from '@abp/ng.theme.basic';
import { ThemeSharedModule } from '@abp/ng.theme.shared';
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NgxsModule } from '@ngxs/store';
import { environment } from '../environments/environment';
import { AppRoutingModule } from './app-routing.module';
@NgModule({
imports: [
BrowserModule,
BrowserAnimationsModule,
AppRoutingModule,
CoreModule.forRoot({
environment,
sendNullsAsQueryParam: false,
skipGetAppConfiguration: false,
}),
ThemeSharedModule.forRoot(),
AccountConfigModule.forRoot(),
IdentityConfigModule.forRoot(),
TenantManagementConfigModule.forRoot(),
SettingManagementConfigModule.forRoot(),
ThemeBasicModule.forRoot(),
NgxsModule.forRoot(),
],
// providers, declarations, and bootstrap
})
export class AppModule {}
AppRoutingModule:
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
const routes: Routes = [
{
path: '',
pathMatch: 'full',
loadChildren: () => import('./home/home.module').then(m => m.HomeModule),
},
{
path: 'account',
loadChildren: () =>
import('@abp/ng.account').then(m => m.AccountModule.forLazy({ redirectUrl: '/' })),
},
{
path: 'identity',
loadChildren: () => import('@abp/ng.identity').then(m => m.IdentityModule.forLazy()),
},
{
path: 'tenant-management',
loadChildren: () =>
import('@abp/ng.tenant-management').then(m => m.TenantManagementModule.forLazy()),
},
{
path: 'setting-management',
loadChildren: () =>
import('@abp/ng.setting-management').then(m => m.SettingManagementModule.forLazy()),
},
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
})
export class AppRoutingModule {}
AppComponent:
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<abp-loader-bar></abp-loader-bar>
<abp-dynamic-layout></abp-dynamic-layout>
`,
})
export class AppComponent {}
You may have noticed that we used
<abp-dynamic-layout>
instead of<router-outlet>
in the AppComponent template. We made this change in order to avoid unnecessary renders and flickering. It is not mandatory, but we recommend doing the same in your AppComponent.
What to Do When Migrating?
- Remove config packages from your project using
yarn remove
. - Import config modules from secondary entry points (e.g.
@abp/ng.identity/config
). - Call static
forRoot
method of all new config modules, even if a configuration is not passed. - Call static
forRoot
method ofThemeBasicModule
(orThemeLeptonModule
if commercial) and removeSharedModule
from imports (unless you have added anything that is necessary for your root module in it). - Import lazy ABP modules directly in app routing module (e.g.
() => import('@abp/ng.identity').then(...)
). - Call static
forLazy
method of all lazy modules insidethen
, even if a configuration is not passed. - [OPTIONAL] Add the
<abp-dynamic-layout></abp-dynamic-layout>
to the AppComponent template and remove the<router-outlet></router-outlet>
for better performance and UX.
RoutesService
In ABP v2.x, adding a route to the menu was done by one of two ways:
As of v3.0, we have changed how routes are added and modified. We are no longer storing routes in ConfigState
(breaking change). Instead, there is a new service called RoutesService
which is used for adding, patching, or removing menu items. Please check the documentation for details.
What to Do When Migrating?
- Check if you have ever used
ConfigState
orConfigStateService
to add any routes. If so, replace them withadd
method ofRoutesService
. - Check if you have ever patched a route. If so, convert them to
patch
method ofRoutesService
. - Double-check that you are using absolute paths and providing a
parentName
instead ofchildren
property for sub-menu items inadd
orpatch
method calls.
NavItemsService
In ABP v2.x, adding a navigation element was done via LayoutStateService
As of v3.0, we have changed how navigation items are added and modified and previous method of doing so is no longer available (breaking change). Please check the documentation for details.
What to Do When Migrating?
- Replace all
dispatchAddNavigationElement
calls withaddItems
method ofNavItemsService
.
ngx-datatable
Until v3, we had been using a custom component, abp-table
, as the default table. However, data grids are complicated components and implementing a fully-featured one requires considerable effort, which we are planning to put in to other features and issues.
As of ABP v3, we have switched to a battle-tested, well-executed data grid: ngx-datatable. All ABP modules will come with ngx-datatable already implemented in them. ThemeSharedModule
already exports NgxDatatableModule
. So, if you install the package by running yarn add @swimlane/ngx-datatable
in your terminal, it will be available for use in all modules of your app.
For proper styling, you need to add the following in the styles section of your angular.json file (above all others):
"styles": [
{
"input": "node_modules/@swimlane/ngx-datatable/index.css",
"inject": true,
"bundleName": "ngx-datatable-index"
},
{
"input": "node_modules/@swimlane/ngx-datatable/assets/icons.css",
"inject": true,
"bundleName": "ngx-datatable-icons"
},
{
"input": "node_modules/@swimlane/ngx-datatable/themes/material.css",
"inject": true,
"bundleName": "ngx-datatable-material"
},
// other styles
]
Since abp-table
is not dropped yet, modules previously built by ABP v2.x will not suddenly lose all their tables. Yet, they will look and feel different from built-in ABP v3 modules. Therefore, you will probably want to convert the tables in those modules to ngx-datatable. In order to decrease the amount of work required to convert an abp-table into ngx-datatable, we have modified the ListService to work well with ngx-datatable and introduced two new directives: NgxDatatableListDirective
and NgxDatatableDefaultDirective
.
The usage of those directives is rather simple:
@Component({
providers: [ListService],
})
export class SomeComponent {
data$ = this.list.hookToQuery(
query => this.dataService.get(query)
);
constructor(
public readonly list: ListService,
public readonly dataService: SomeDataService,
) {}
}
...and in component template...
<ngx-datatable
[rows]="(data$ | async)?.items || []"
[count]="(data$ | async)?.totalCount || 0"
[list]="list"
default
>
<!-- column templates here -->
</ngx-datatable>
Once you bind the injected ListService
instance through NgxDatatableListDirective
, you no longer need to worry about pagination or sorting. Similarly, NgxDatatableDefaultDirective
gets rid of several property bindings to make ngx-datatable fit our styles.
A Better Example
<ngx-datatable
[rows]="items"
[count]="count"
[list]="list"
default
>
<!-- the grid actions column -->
<ngx-datatable-column
name=""
[maxWidth]="150"
[width]="150"
[sortable]="false"
>
<ng-template
ngx-datatable-cell-template
let-row="row"
let-i="rowIndex"
>
<abp-grid-actions
[index]="i"
[record]="row"
text="AbpUi::Actions"
></abp-grid-actions>
</ng-template>
</ngx-datatable-column>
<!-- a basic column -->
<ngx-datatable-column
prop="someProp"
[name]="'::SomeProp' | abpLocalization"
[width]="200"
></ngx-datatable-column>
<!-- a column with a custom template -->
<ngx-datatable-column
prop="someOtherProp"
[name]="'::SomeOtherProp' | abpLocalization"
[width]="250"
>
<ng-template
ngx-datatable-cell-template
let-row="row"
let-i="index"
>
<div abpEllipsis>{{ row.someOtherProp }}</div>
</ng-template>
</ngx-datatable-column>
</ngx-datatable>
What to Do When Migrating?
- Install
@swimlane/ngx-datatable
package. - Add ngx-datatable styles in the angular.json file.
- If you can, update your modules according to the example above.
- If you have to do that later and are planning to keep abp-table for a while, make sure you update your pagination according to the breaking change described here.
Important Note: The abp-table
is not removed, but is deprecated and will be removed in the future. Please consider switching to ngx-datatable.
Extensions System [COMMERCIAL]
The extensions system is open sourced now and is publicly available from @abp/ng.theme.shared/extensions
package instead of @volo/abp.commercial.ng.ui
. Also, according to the new structure of config packages, the configuration is given through forLazy
static methods as described above.
What to Do When Migrating?
If you have never used the extensions system before, you do not have to do anything. If you have, then please check the documentation again to see what changed. Extension system itself works the same as before. The only changes are the package you import from and the static method and the module you pass your contributors to.
Lepton Theme Logos [COMMERCIAL]
In ABP v2.x, Lepton had one light and one dark logo per color theme. We have realized we could make it work with only one light and one dark logo. So, we have changed how Lepton looks up logo images and now you just need to have a logo-light.png
and a logo-dark.png
in your project.
What to Do When Migrating?
If you have switched template logo PNGs before, the change is simple:
- Go to
/assets/images/logo
folder. - Rename
theme1.png
aslogo-light.png
andtheme1-reverse.png
aslogo-dark.png
. - Delete all other
theme*.png
files.
If you have replaced the logo component(s), the change is a little bit different, but still simple. The LayoutStateService
has a two new members: primaryLogoColor
and secondaryLogoColor
. They have an observable stream of 'light'
and 'dark'
strings as value. You can consume their value in your custom logo component templates with the async
pipe. Here is a complete example which covers both primary and secondary (account) layout logos.
import { AddReplaceableComponent } from '@abp/ng.core';
import { CommonModule } from '@angular/common';
import { APP_INITIALIZER, Component, Injector, NgModule } from '@angular/core';
import { Store } from '@ngxs/store';
import { eAccountComponents } from '@volo/abp.ng.account';
import {
AccountLayoutComponent,
eThemeLeptonComponents,
LayoutStateService,
} from '@volo/abp.ng.theme.lepton';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Component({
template: `
<div class="account-brand p-4 text-center mb-1" *ngIf="isAccount; else link">
<ng-template [ngTemplateOutlet]="link"></ng-template>
</div>
<ng-template #link>
<a [style.background-image]="logoUrl | async" class="navbar-brand" routerLink="/"></a>
</ng-template>
`,
})
export class LogoComponent {
isAccount: boolean;
logoColor: Observable<'dark' | 'light'>;
get logoUrl() {
return this.logoColor.pipe(map(color => `url(/assets/images/logo/logo-${color}.png)`));
}
constructor(injector: Injector) {
const layout = injector.get(LayoutStateService);
this.isAccount = Boolean(injector.get(AccountLayoutComponent, false));
this.logoColor = this.isAccount ? layout.secondaryLogoColor : layout.primaryLogoColor;
}
}
@NgModule({
imports: [CommonModule],
declarations: [LogoComponent],
exports: [LogoComponent],
})
export class LogoModule {}
export const APP_LOGO_PROVIDER = [
{ provide: APP_INITIALIZER, useFactory: switchLogos, multi: true, deps: [Store] },
];
export function switchLogos(store: Store) {
return () => {
store.dispatch(
new AddReplaceableComponent({
component: LogoComponent,
key: eThemeLeptonComponents.Logo,
}),
);
store.dispatch(
new AddReplaceableComponent({
component: LogoComponent,
key: eAccountComponents.Logo,
}),
);
};
}
Just add APP_LOGO_PROVIDER
to the providers of your root module (usually AppModule
) and you will have a custom logo component adjusting to the theme colors.
Deprecated Interfaces
Some interfaces have long been marked as deprecated and now they are removed.
What to Do When Migrating?
- Please check if you are still using anything listed in this issue