Hi,
provideZoneChangeDetection() did fix the layout rendering — confirmed on our end. We removed the @ViewChild workaround we had been using and the layout now resolves correctly through the normal NavigationEnd path. Thank you for the precise diagnosis.
Two notes for the migration guide while you're there:
This isn't a one-off — every NgModule-based app upgrading from ABP 9 will hit this in some form, because Angular's NgModule path no longer auto-installs Zone.js change detection the way it did in earlier versions. A line in the 9 → 10 migration guide saying "Add provideZoneChangeDetection() to your AppModule providers" would have saved us a lot of time. The symptom (<abp-dynamic-layout> rendering only <!--ng-container--><!--container-->) gives no hint that change detection is the cause — getLayout() runs, the layout component is registered, the route tree is correct, but nothing is visible.
While debugging this we hit one unrelated regression that provideZoneChangeDetection() does not fix:
Brand name shows the literal "ProjectName" instead of environment.application.name
LOGO_APP_NAME_TOKEN (from @abp/ng.theme.shared) defaults to 'ProjectName'. The new path to bind it is provideLogo(withEnvironmentOptions(...)) which wraps the providers in makeEnvironmentProviders — but in NgModule context this didn't bind the token for us. We worked around it by providing the token directly:
{ provide: LOGO_APP_NAME_TOKEN, useValue: environment.application.name }, In ABP 9, this binding happened automatically from environment.application — there was no consumer-side configuration needed.
Thanks again for the help.
Hi again, We managed to get our app working after deep-diving into the ABP/LeptonX source. We're sharing the full reproduction here because the problem is four separate bugs/regressions in the 9 → 10 migration, not one. All of them are related to the standalone-component / makeEnvironmentProviders migration in NgModule-based apps. Our app uses NgModule bootstrap (AppModule, not standalone app.config.ts) — this is the relevant context for everything below.
Bug 1 — <abp-dynamic-layout> renders empty (<!--ng-container--><!--container-->) Root cause: the commit e913c85 ("fix(dynamic-layout): replace OnInit implementation with environment change listener") removed the ngOnInit from DynamicLayoutComponent. After that change, getLayout() is only triggered by:
NavigationEndevents (constructor subscription), or EnvironmentService.createOnUpdateStream(oAuthConfig)withtake(1)andresponseType === 'code'. In ABP 10.3 DynamicLayoutComponent is also standalone, so it is instantiated lazily when AppComponent's view initializes. By that time, the initial NavigationEnd has already fired, and its constructor-level subscription misses it. The environment listener only fires on subsequent updates, not on the initial value (it uses a plain Subject-backed createOnUpdateStream, not a BehaviorSubject). So getLayout() is never called, layout stays undefined, and [ngComponentOutlet] renders nothing. In ABP 9.x, ngOnInit itself called getLayout() eagerly, which masked this race. The commit above removed that safety net. Diagnostic confirmation: in our AppComponent.ngAfterViewInit, replaceableComponents.get('Theme.ApplicationLayoutComponent') returns the registered component, the RoutesService tree has the correct layout: 'application' for the current route, router.url === '/' and router.navigated === true — yet <abp-dynamic-layout> is still empty. So all prerequisites are satisfied; the bug is purely that getLayout() is never invoked. Workaround:
// app.component.ts
@ViewChild(DynamicLayoutComponent) private dynamicLayout?: DynamicLayoutComponent;
constructor(private cdr: ChangeDetectorRef, /* ... */) {}
ngAfterViewInit() {
setTimeout(() => {
const dl = this.dynamicLayout as any;
if (dl && !dl.layout) {
dl.getLayout?.();
this.cdr.detectChanges();
}
});
}
The setTimeout(0) is required to avoid NG0100 ExpressionChangedAfterItHasBeenCheckedError.
Bug 2 — Brand name shows the literal string "ProjectName" instead of environment.application.name Root cause: LeptonX reads the brand name from LOGO_APP_NAME_TOKEN (@abp/ng.theme.shared), with the fallback string 'ProjectName'. The migration removed/changed the path that previously bound this token to environment.application.name. The new path is provideLogo(withEnvironmentOptions({ application: { name } })), which again uses makeEnvironmentProviders and exhibits the same NgModule-context unreliability as Bug 2. Workaround: provide the tokens directly:
import { LOGO_APP_NAME_TOKEN, LOGO_URL_TOKEN } from '@abp/ng.theme.shared';
providers: [
// ...
{ provide: LOGO_APP_NAME_TOKEN, useValue: environment.application.name },
{ provide: LOGO_URL_TOKEN, useValue: '' },
],
Combined working setup (NgModule, ABP 10.3 / Angular 21)
import {
LOGO_APP_NAME_TOKEN,
LOGO_URL_TOKEN,
ThemeSharedModule,
} from '@abp/ng.theme.shared';
import {
initLayouts,
provideSideMenuLayout,
SideMenuLayoutModule,
} from '@volosoft/abp.ng.theme.lepton-x/layouts';
import { provideAppInitializer } from '@angular/core';
@NgModule({
imports: [
// ...
ThemeLeptonXModule.forRoot(),
AccountLayoutModule.forRoot(),
// ...
],
providers: [
APP_ROUTE_PROVIDER,
provideSideMenuLayout(),
{ provide: LOGO_APP_NAME_TOKEN, useValue: environment.application.name },
{ provide: LOGO_URL_TOKEN, useValue: '' },
],
})
export class AppModule {}
Plus the @ViewChild workaround in AppComponent.ngAfterViewInit for Bug 1.
Asks
Could you confirm whetherNgModule-bootstrap is still officially supportedin ABP 10.x, or has it been silently de-facto-deprecated in favor ofapp.config.tsstandalone bootstrap? If the latter, please document this in the migration guide. The commite913c85introduced a real race that any consumer hitting the standaloneDynamicLayoutComponentpath will encounter. Restoring an eager call togetLayout()(e.g. inngAfterContentInitor as a deferred microtask in the constructor) would fix it without needing the consumer-side@ViewChildhack.
Hi,
Thanks for the suggestion. Unfortunately adding provideSideMenuLayout() to the providers list did not change the behavior — <abp-dynamic-layout> still renders empty (<!--ng-container--><!--container-->).
Note that our app is NgModule-based (not standalone bootstrap with app.config.ts). We're already importing SideMenuLayoutModule.forRoot(), which internally calls provideSideMenuLayout(). We additionally added provideSideMenuLayout() directly to the providers as you suggested — same result.
Admin routes are registered through RoutesService.add({ ..., layout: eLayoutType.application, ... }) via an APP_INITIALIZER in route.provider.ts, which is the standard ABP pattern.
Below is our current trimmed app.module.ts with non-relevant feature modules removed for clarity. Could you take a look and let us know what we're missing?
import { CoreModule } from '@abp/ng.core';
import { AbpOAuthModule } from '@abp/ng.oauth';
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 { AccountAdminConfigModule } from '@volo/abp.ng.account/admin/config';
import { AccountPublicConfigModule } from '@volo/abp.ng.account/public/config';
import { IdentityConfigModule } from '@volo/abp.ng.identity/config';
import { registerLocale } from '@volo/abp.ng.language-management/locale';
import {
HttpErrorComponent,
ThemeLeptonXModule,
} from '@volosoft/abp.ng.theme.lepton-x';
import { AccountLayoutModule } from '@volosoft/abp.ng.theme.lepton-x/account';
import {
provideSideMenuLayout,
SideMenuLayoutModule,
} from '@volosoft/abp.ng.theme.lepton-x/layouts';
import { environment } from '../environments/environment';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { APP_ROUTE_PROVIDER } from './route.provider';
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
BrowserAnimationsModule,
AppRoutingModule,
CoreModule.forRoot({
environment,
registerLocaleFn: registerLocale(),
}),
AbpOAuthModule.forRoot(),
ThemeSharedModule.forRoot({
httpErrorConfig: {
errorScreen: {
component: HttpErrorComponent,
forWhichErrors: [401, 403, 404, 500],
hideCloseIcon: true,
},
},
}),
AccountAdminConfigModule.forRoot(),
AccountPublicConfigModule.forRoot(),
IdentityConfigModule.forRoot(),
ThemeLeptonXModule.forRoot(),
SideMenuLayoutModule.forRoot(),
AccountLayoutModule.forRoot(),
],
providers: [APP_ROUTE_PROVIDER, provideSideMenuLayout()],
bootstrap: [AppComponent],
})
export class AppModule {}
route.provider.ts (relevant excerpt):
import { eLayoutType, RoutesService } from '@abp/ng.core';
import { APP_INITIALIZER } from '@angular/core';
export const APP_ROUTE_PROVIDER = [
{
provide: APP_INITIALIZER,
useFactory: configureRoutes,
deps: [RoutesService],
multi: true,
},
];
function configureRoutes(routes: RoutesService) {
return () => {
routes.add([
{
name: 'Daisy::Menu:Dashboard',
path: '/',
layout: eLayoutType.application,
order: 1,
iconClass: 'fas fa-tachometer-alt',
},
// ... more application-layout routes
]);
};
}
Rendered DOM after the fix:
<abp-dynamic-layout class=""><!--ng-container--><!--container--></abp-dynamic-layout>
Same as before — the inner <ng-container [ngComponentOutlet]="layout"> anchor is created, but layout is undefined, so nothing mounts. This means either getLayout() is never called on NavigationEnd, or replaceableComponents.get('Theme.ApplicationLayoutComponent') still returns undefined.
Worth noting: the same codebase works on ABP 9.2 / Angular 19 with no layout-related config. The only changes were the framework upgrade.
Could you share a minimal working AppModule example for ABP 10.3 + Angular 21 (NgModule-based, not standalone)? Or confirm whether NgModule-based bootstrap is still officially supported in 10.3?
Thanks
We reviewed the related documentation and already tried the suggested migration steps, but the issue still persists.
Routing works correctly with router outlet, but abp dynamic layout renders empty, so the problem seems specific to layout resolution in v10.
Is there any breaking change in how layout is resolved from route data or registered in v10?
Hi,
After upgrading our project from ABP 9.2.0 (Angular 19.1.0) to ABP 10.3.0 (Angular 21.2.10), the abp-dynamic-layout component renders nothing. The resulting DOM is:
<abp-dynamic-layout class=""><!--ng-container--><!--container--></abp-dynamic-layout>
Replacing it with a plain <router-outlet> works correctly and all routed components render as expected, so the issue seems specific to dynamic layout resolution.
Key versions on the broken setup:
The same routing setup and app.component template work without any change on ABP 9.2.0 + Angular 19.1.0.
Is there a known migration step or breaking change between v9 and v10 around layout resolution, route data shape, or replaceable components that could cause the layout to not resolve? Happy to share both package.json files and a minimal repro if needed.
Thanks, Mucahit
ABP (LeptonX) sidebar navigation forces all root menu items that have children to be collapsible.
Currently, there is no built-in or supported way to disable this collapse behavior and keep a menu group always expanded.
This limitation affects use cases such as Quick Links section which are expected to remain permanently visible in the sidebar.
disableCollapsealwaysExpandedexpandedByDefault (locked)The only non-collapsible option is using menu-title, but:
menu-title items cannot contain child routesProvide an optional configuration to disable collapse behavior for a menu group, for example:
This should be an opt-in feature and should not change the existing default behavior.
Thank you.
Hi, I added the paths configuration to apps/angular/tsconfig.json, but the error is still happening. Is there anything else I should try? Also, has there been an official announcement or update about this issue?
We are getting a runtime error when we build the Angular project. This error came from nowhere. Nothing changed and we can't run even a version from 2 months ago. The error is coming from the project core dependency (@abp/ng.core). The error message is:
NullInjectorError: NullInjectorError: No provider for InjectionToken CORE_OPTIONS!
at Yf.get (core.mjs:1604:21)
at Dc.get (core.mjs:2134:27)
at Dc.get (core.mjs:2134:27)
at nk (core.mjs:1110:28)
at R (core.mjs:1116:40)
at rn.ɵfac [as factory] (abp-ng.core.mjs:973:42)
at Dc.hydrate (core.mjs:2251:33)
at Dc.get (core.mjs:2125:23)
at nk (core.mjs:1110:28)
at R (core.mjs:1116:40)
{
"dependencies": {
"@abp/ng.components": "~9.1.0",
"@abp/ng.core": "~9.1.0",
"@abp/ng.oauth": "~9.1.0",
"@abp/ng.setting-management": "~9.1.0",
"@abp/ng.theme.shared": "~9.1.0",
"@abp/signalr": "^9.1.0",
"@abp/uppy": "9.1.0",
"@angular/animations": "~19.2.3",
"@angular/common": "~19.2.3",
"@angular/compiler": "~19.2.3",
"@angular/core": "~19.2.3",
"@angular/elements": "19.2.3",
"@angular/forms": "~19.2.3",
"@angular/localize": "~19.2.3",
"@angular/platform-browser": "~19.2.3",
"@angular/platform-browser-dynamic": "~19.2.3",
"@angular/router": "~19.2.3",
"@formio/angular": "^8.0.0",
"@formio/js": "^5.0.1",
"@mescius/activereportsjs-angular": "^5.2.0",
"@sentry/angular": "^9.7.0",
"@storybook/test": "^8.6.7",
"@volo/abp.commercial.ng.ui": "~9.1.0",
"@volo/abp.ng.account": "~9.1.0",
"@volo/abp.ng.audit-logging": "~9.1.0",
"@volo/abp.ng.identity": "~9.1.0",
"@volo/abp.ng.language-management": "~9.1.0",
"@volo/abp.ng.openiddictpro": "~9.1.0",
"@volo/abp.ng.saas": "~9.1.0",
"@volo/abp.ng.text-template-management": "~9.1.0",
"@volosoft/abp.ng.theme.lepton-x": "~4.1.0",
"angular-gridster2": "^19.0.0",
"dayjs": "^1.11.13",
"highcharts": "11.4.8",
"monaco-editor": "^0.52.2",
"ngx-angular-query-builder": "~18.0.0",
"ngx-extended-pdf-viewer": "^22.3.9",
"ngx-flexmonster": "2.9.99",
"ngx-monaco-editor-v2": "^19.0.2",
"ngx-quill": "27.0.1",
"quill": "2.0.3",
"rxjs": "7.8.2",
"swagger-ui": "5.17.14",
"tslib": "2.8.1",
"zone.js": "~0.15.0"
},
"devDependencies": {
"@abp/ng.schematics": "~9.1.0",
"@angular-devkit/build-angular": "~19.2.4",
"@angular-eslint/builder": "~19.2.1",
"@angular-eslint/eslint-plugin": "~19.2.1",
"@angular-eslint/eslint-plugin-template": "~19.2.1",
"@angular-eslint/schematics": "~19.2.1",
"@angular-eslint/template-parser": "~19.2.1",
"@angular/cli": "~19.2.4",
"@angular/compiler-cli": "~19.2.3",
"@angular/language-service": "~19.2.3",
"@compodoc/compodoc": "^1.1.26",
"@storybook/addon-essentials": "^8.6.7",
"@storybook/addon-interactions": "^8.6.7",
"@storybook/addon-links": "^8.6.7",
"@storybook/angular": "^8.6.7",
"@storybook/blocks": "^8.6.7",
"@storybook/test-runner": "^0.22.0",
"@types/jasmine": "~5.1.7",
"@types/node": "^22.13.10",
"@typescript-eslint/eslint-plugin": "^8.27.0",
"@typescript-eslint/parser": "^8.27.0",
"eslint": "9.22.0",
"eslint-config-prettier": "^10.1.1",
"eslint-plugin-prettier": "^5.2.3",
"eslint-plugin-storybook": "^0.11.6",
"jasmine-core": "~5.6.0",
"karma": "~6.4.4",
"karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.1",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "^2.1.0",
"ng-packagr": "^19.2.0",
"preact": "^10.26.4",
"prettier": "^3.5.3",
"prettier-eslint": "^16.3.0",
"storybook": "^8.6.7",
"typescript": "5.8.2"
}
}
I tried:
@abp/@9.x.x packages.@abp/* packages to 8.3.0.@angular/*@19.x.x packages well.