Open Closed

abp-dynamic-layout renders empty after upgrade to ABP 10 / Angular 21 #10639


User avatar
0
mchtbrt created

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:

  • @abp/ng.core, @abp/ng.components, @abp/ng.theme.shared: 10.3.0
  • @volo/abp.commercial.ng.ui: 10.3.0
  • @volosoft/abp.ng.theme.lepton-x: 5.3.0
  • @angular/core: 21.2.10
  • rxjs: 7.8.2
  • zone.js: 0.15.1
  • typescript: 5.9.3

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

Markdown supported.
Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)

9 Answer(s)
  • User Avatar
    0
    AI-Bot created
    Support Team ABP Agent
    • In ABP Angular v10, abp-dynamic-layout still works, but it depends on ABP’s layout system being properly provided/bootstrapped and on routes being compatible with the post-v9 Angular routing approach (standalone + updated route factories).
    • The most common upgrade pitfall that results in an “empty dynamic layout” (only containers, no rendered layout + no routed content) is that the app is effectively running with incomplete ABP Angular provider setup (theme/layout providers not registered the “v10 way”), or the routes were migrated in a way that no longer matches what ABP expects for layout resolution.

    Do the following (these are the verified migration areas between v9 and v10 that affect Angular routing/layout behavior):

    1. Align routing to the newer ABP Angular approach (standalone routing factories) If you still have older lazy-loading patterns (module-based forLazy()), migrate them to the new pattern used by ABP packages, i.e. createRoutes(...).

    Example (conceptual):

    {
      path: 'identity',
      loadChildren: () => import('@volo/abp.ng.identity').then(m => m.createRoutes()),
    }
    

    If your own feature modules still use “old style” lazy loading, ABP layout resolution can silently fail to match the route tree as expected in newer Angular/ABP setups.

    1. Ensure your app is using the v10 standalone/provider-based bootstrapping correctly With Angular 19+ standalone defaults, ABP’s Angular packages are designed to be configured via ApplicationConfig providers (e.g. app.config.ts). Make sure you didn’t miss the ABP providers responsible for theme/layout.

    Practically:

    • Ensure you are calling the ABP theme shared + LeptonX theme providers as required by your template.
    • Ensure the route provider setup is present (ABP route provider).

    This is especially important if you upgraded “in place” instead of regenerating a v10 Angular UI and diffing the bootstrap/config.

    1. Verify you actually migrated to the newer ABP Angular package structure expectations (standalone support) ABP introduced official support for Angular standalone structure around v9.3, and this affects how routing is shaped and how components/modules are wired. Even if you keep module-based structure, your routing and app initialization should be compatible.

    If you want a concrete checklist for what changed in ABP Angular around routing/standalone, use these (verified) references and compare your project with the template produced by ABP 10.x:

    Sources (validated)

    • https://abp.io/docs/10.0/release-info/migration-guides/abp-10-0
    • https://abp.io/docs/latest/release-info/migration-guides/abp-9-3
    • https://abp.io/community/articles/zzi2rr2z

    This response was generated by AI. If it didn’t solve your problem, feel free to post a new answer — our support team will jump in to help you soon.

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    mchtbrt created

    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?

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    Hi Mucahit,

    The empty <abp-dynamic-layout> (only <!--ng-container--><!--container--> inside) means ReplaceableComponentsService.get('Theme.ApplicationLayoutComponent') returned undefined, i.e. the application layout component was never registered.

    In ABP v10 / LeptonX 5.x, layout registration moved to explicit provide*Layout() functions. provideThemeLeptonX() only registers the AccountLayout. The ApplicationLayout (and EmptyLayout) are registered by provideSideMenuLayout() (or provideTopMenuLayout() if your shell is top-menu). If that call is missing from your migrated app.config.ts, every admin route resolves to a layout key with no registered component, and the container renders nothing.

    Fix in app.config.ts:

    import { provideThemeLeptonX } from '@volosoft/abp.ng.theme.lepton-x';
    import { provideSideMenuLayout } from '@volosoft/abp.ng.theme.lepton-x/layouts';
    
    export const appConfig: ApplicationConfig = {
      providers: [
        // ...
        provideThemeLeptonX(),
        provideSideMenuLayout(),   // <-- the missing piece
        // ...
      ],
    };
    

    Use provideTopMenuLayout from the same /layouts entry if your shell is top-menu instead.

    If pages render with EmptyLayout instead of ApplicationLayout after the fix, make sure your admin routes go through RoutesService.add({ ..., layout: eLayoutType.application, ... }) (the standard ABP module pattern). The dynamic layout component reads the layout type from the RoutesService tree, not from Angular Router's route data on the root ActivatedRoute, so a plain data: { layout: 'application' } on a top-level route won't propagate.

    Could you also share your current app.config.ts (just the providers list) and how your admin routes are registered (via RoutesService or directly in provideRouter)? That will help us confirm the exact spot.

    Thanks

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    mchtbrt created

    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

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    mchtbrt created

    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.

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    Hi Mucahit,

    Thanks for the deep dive — your Bug 1 analysis is correct. We confirmed the race against the ABP 10.3 source. Commit e913c85 (fix(dynamic-layout): replace OnInit implementation with environment change listener, 2025-08-06) removed DynamicLayoutComponent.ngOnInit, which previously called getLayout() eagerly. After that change, getLayout() only runs from (a) the NavigationEnd subscription set up in the constructor, or (b) the EnvironmentService.createOnUpdateStream(...) listener — and that stream is backed by a plain Subject, so it does not replay the current value. Both signals can be missed depending on bootstrap timing, which is what you're hitting on NgModule bootstrap.

    We reproduced your exact DOM (<abp-dynamic-layout><!--ng-container--><!--container--></abp-dynamic-layout>) on a minimal Angular 21.2 + @abp/ng.core@10.3.0 NgModule app and validated the fix below.

    Bug 1 workaround

    Use this slightly stronger version in app.component.ts — it always calls getLayout() and forces a change-detection pass, so it covers both the case where the navigation event was missed and the case where layout was set but [ngComponentOutlet] didn't refresh:

    import {
      AfterViewInit,
      ChangeDetectorRef,
      Component,
      ViewChild,
    } from '@angular/core';
    import { DynamicLayoutComponent } from '@abp/ng.core';
    
    @Component({ /* ... */ })
    export class AppComponent implements AfterViewInit {
      @ViewChild(DynamicLayoutComponent)
      private dynamicLayout?: DynamicLayoutComponent;
    
      constructor(private cdr: ChangeDetectorRef) {}
    
      ngAfterViewInit() {
        setTimeout(() => {
          const dl = this.dynamicLayout as any;
          dl?.getLayout?.();
          this.cdr.detectChanges();
        });
      }
    }
    

    The setTimeout(0) is required to avoid NG0100 ExpressionChangedAfterItHasBeenCheckedError, as you already noted.

    We've opened https://github.com/abpframework/abp/issues/25373 to track the regression in DynamicLayoutComponent so consumers won't need this workaround in a future release.

    Bug 2 fix

    The "ProjectName" literal is the fallback used by LeptonX's BrandLogoComponent when LOGO_APP_NAME_TOKEN has no provider:

    // node_modules/@volosoft/ngx-lepton-x.core/.../brand-logo.component.ts
    protected readonly appName =
      inject(LOGO_APP_NAME_TOKEN, { optional: true }) ?? 'ProjectName';
    

    In v10, no ABP or LeptonX module registers this token automatically anymore — the standard ABP 10.x template registers it through provideLogo(withEnvironmentOptions(environment)) from @abp/ng.theme.shared. Add this to your app.module.ts providers (it returns EnvironmentProviders and is fine inside an NgModule's providers array):

    import { provideLogo, withEnvironmentOptions } from '@abp/ng.theme.shared';
    import { environment } from '../environments/environment';
    
    providers: [
      APP_ROUTE_PROVIDER,
      provideSideMenuLayout(),
      provideLogo(withEnvironmentOptions(environment)),
    ],
    

    This single call provides both LOGO_APP_NAME_TOKEN (from environment.application.name) and LOGO_URL_TOKEN (from environment.application.logoUrl), so you don't need to provide the tokens manually.

    NgModule bootstrap

    To answer your direct question: yes, NgModule bootstrap is still officially supported in ABP 10.x — it's not marked deprecated in the migration guide. The standalone app.config.ts style is the new template default, but the CoreModule.forRoot / ThemeSharedModule / ThemeLeptonXModule.forRoot() / SideMenuLayoutModule.forRoot() / AccountLayoutModule.forRoot() chain you're already using is intended to keep working in 10.x.

    Thanks

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    sumeyye.kurtulus created
    Support Team Angular Expert

    Hello,

    Thank you for the detailed explanation and the steps you shared. After reproducing the issue on my side, it appears to be related to change detection.

    The application layout is rendered by <abp-dynamic-layout />, which determines the appropriate layout by listening to router events and updating its internal state on navigation. These updates depend on Angular’s default Zone.js–based change detection to trigger a re-render. By adding provideZoneChangeDetection() in app.module.ts, we explicitly enable this mechanism, allowing the layout selected for each route (for example, eLayoutType.application from route.provider.ts) to render correctly. This resolved the issue in my testing. Without it, the layout selection logic still executes, but Angular does not receive a signal to refresh the view, so the layout is not displayed.

    We continue to support the module-based structure in Angular, so there’s no need to migrate to standalone components at this stage. Please try adding this provider on your end and let us know the result. Based on your feedback, we can update the migration guide if needed.

    Thank you for your cooperation and patience.

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    mchtbrt created

    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:

    1. 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.

    2. 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.

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    sumeyye.kurtulus created
    Support Team Angular Expert

    Hello,

    Thank you for taking the time to try this out and for informing us about your situation. We will update the migration documentation accordingly.

    You can find the latest guidance for logo replacement in the following documentation: Logo Component Replacement Documentation

    Please let us know if you need any further assistance. Thank you again for your cooperation.

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
Boost Your Development
ABP Live Training
Packages
See Trainings
Mastering ABP Framework Book
The Official Guide
Mastering
ABP Framework
Learn More
Mastering ABP Framework Book
Made with ❤️ on ABP v10.8.0-preview. Updated on September 28, 2026, 11:44
1
ABP Assistant
🔐 You need to be logged in to use the chatbot. Please log in first.