Standalone Component with Optional NgModule

Angular v14 + v15 Features

  • Typed Forms
  • Standalone Components
  • Page Title Strategy
  • Protected variables
  • Extended developer diagnostics
  • More built-in improvements
  • inject API
  • Directive composition API
  • etc.

pankajparkar

Pankaj P. Parkar

Sr. Technology Consultant, Virtusa

  • Ex- Microsoft MVP (2016-22)

  • Angular GDE

  • Stackoverflow Topuser

About Me!

pankajparkar

// app.module.ts
@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    BrowserAnimationsModule,
    RouterModule.forRoot([]),
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }
// app.component.ts
@Component({
  selector: 'sc-root',
  template: `
    <h1>Hello {{ title }}</h1>
  `
})
export class AppComponent {
  title = 'Angular';
}
// main.ts
if (environment.production) {
  enableProdMode();
}

platformBrowserDynamic()
  .bootstrapModule(AppModule)
  .catch(err => console.error(err));

Bare Minimum Angular

AppModule

 

App

Component

Routing

Browser

Module

 

Forms

Module

 

main.ts

(bootstrap)

pankajparkar

Demo Application

pankajparkar

@NgModule({
  declarations: [
    AppComponent,
    AdminComponent,
    DashboardComponent,
    NavbarComponent,
    BarChartComponent,
    PieChartComponent,
    PieChartComponent,
    ProductComponent,
    RolesComponent,
    RoleDetailsComponent,
  ],
  imports: [
    MatToolbarModule,
    MatButtonModule,
    AppRoutingModule,
    BrowserModule,
    BrowserAnimationsModule,
    HttpClientModule,
    SharedModule,
    ...
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }
const routes: Routes = [
  {
    path: 'dashboard',
    component: DashboardComponent,
  },
  {
    path: 'roles',
    children: [{
      path: '',
      component: RolesComponent,
    }, {
      path: 'details/:id',
      component: RolesDetailsComponent,
    }],
  },
  {
    path: 'admin',
    component: AdminComponent,
  },
  {
    path: 'product',
    component: ProductComponent,
  },
  {
    path: '**',
    redirectTo: 'dashboard'
  },
];

pankajparkar

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    MatToolbarModule,
    MatButtonModule,
    AppRoutingModule,
    BrowserModule,
    BrowserAnimationsModule,
    HttpClientModule,
    SharedModule,
    DashboardModule,
    ProductModule,
    RolesModule,
    SharedModule,
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

😄

Still we have to improve

pankajparkar

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    BrowserAnimationsModule,
    AppRoutingModule,
    HttpClientModule,
    SharedModule,
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }
const routes: Routes = [
  {
    path: 'dashboard',
    loadChildren: () => 
      import('./dashboard/dashboard.module')
       .then(m => m.DashboardModule),
  },
  {
    path: 'roles',
    loadChildren: () =>
      import('./roles/roles.module')
        .then(m => m.RolesModule),
  },
  {
    path: 'admin',
    loadChildren: () =>
      import('./admin/admin.module')
        .then(m => m.AdminModule),
  },
  {
    path: 'product',
    loadChildren: () =>
      import('./product/product.module')
        .then(m => m.ProductModule),
  },
  {
    path: '**',
    redirectTo: 'dashboard'
  },
];
@NgModule({
  declarations: [],
  imports: [
    RouterModule.forRoot(routes),
  ],
  exports: [RouterModule],
})
export class AppRoutingModule { }

Standalone Components 

  • Another way of creating angular components
  • Helps to create moduleless isolated components
ng generate component standalone --standalone
@Component({
  selector: 'app-standalone',
  standalone: true,
  imports: [CommonModule], // <-- all imports goes here
  templateUrl: './standalone.component.html',
  styleUrls: ['./standalone.component.scss']
})
export class StandaloneComponent implements OnInit {
  ...
}

pankajparkar

Goals

Non Goals

pankajparkar

  • Another way to define component
  • The learning curve for a new developer would be less
  • Backward compatible
  • Library authors can take good benefits from it
  • Isolated component
  • Not a mandatory change to upgrade all components to standalone
  • Not a breaking change
const routes = [
  {
    path: 'standalone',
    component: StandaloneComponent,
  },
  ...
];

Routing

pankajparkar

Routing Lazy Load

const routes = [
  {
    path: 'standalone-lazy',
    loadComponent: () => import('./components/standalone-lazy.component')
    	.then(s => s.StandaloneLazyComponent),
  },
]
// dashboard.module.ts
const matModules = [
  MatGridListModule,
  MatCardModule,
];

@NgModule({
  imports: [
    ...matModules,
    CommonModule,
    DashboardRoutingModule,
    SharedModule,
  ],
  declarations: [
    DashboardComponent
  ],
})
export class DashboardModule { }
<!--dashboard.component.html-->
<mat-grid-list cols="2">
    <mat-grid-tile>
        <mat-card>
            <app-bar-chart></app-bar-chart>
        </mat-card>
    </mat-grid-tile>
    <mat-grid-tile>
        <mat-card>
            <app-bar-chart></app-bar-chart>
        </mat-card>
    </mat-grid-tile>
</mat-grid-list>
//dashboard.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-dashboard',
  templateUrl: './dashboard.component.html',
  styleUrls: ['./dashboard.component.scss'],

  
})
export class DashboardComponent {
}

imports: [...]​,

standalone: true,

// dashboard.routes.ts

const routes = [{
  path: '',
  component: DashboardComponent
}];

@NgModule({
  declarations: [],
  imports: [
    RouterModule.forChild(routes),
  ],
  exports: [RouterModule],
})
export class DashboardRoutingModule {

}

Routing

pankajparkar

export const routes: Routes = [{
  path: 'dashboard',
  component: DashboardComponent,
}];
// roles.routing.ts
const matModules = [
  MatTableModule,
  MatCardModule,
  MatButtonModule,
  MatDividerModule,
];

@NgModule({
  declarations: [
    RolesComponent,
    RoleDetailsComponent
  ],
  imports: [
    ...matModules,
    RolesRoutingModule,
    CommonModule,
  ]
})
export class RolesModule { }
<mat-card>
  <mat-card-title>Roles</mat-card-title>
    <mat-card-subtitle>
      admin can modify roles
    </mat-card-subtitle>
    <table mat-table [dataSource]="dataSource">
      <!-- Position Column -->
	  <ng-container matColumnDef="position">
        ...
      </ng-container>
      <!-- Name Column -->
      <ng-container matColumnDef="name">
        ...
      </ng-container>
      <tr mat-header-row 
        *matHeaderRowDef="displayedColumns"></tr>
      <tr mat-row 
        *matRowDef="let row; columns: displayedColumns;">
      </tr>
  </table>
</mat-card>
@Component({
  selector: 'app-roles',
  templateUrl: './roles.component.html',
  styleUrls: ['./roles.component.scss']
  
  
})
export class RolesComponent {
  private apiService = inject(ApiService);
  displayedColumns: string[] = ['id', 'name'];
  dataSource: PeriodicElement[] = 
   this.apiService.getPeriodicElements();
}

imports: [...]​,

standalone: true,

const routes = [{
  path: '',
  component: RolesComponent
}, {
  path: 'details/:id',
  component: RoleDetailsComponent
}];

@NgModule({
  declarations: [],
  imports: [
    RouterModule.forChild(routes),
  ],
  exports: [RouterModule],
})
export class RolesRoutingModule { }

Children routes

export const routes = [{
  path: '',
  component: RolesComponent
}, {
  path: 'details/:id',
  component: RoleDetailsComponent
}];

pankajparkar

How all routes are loaded now ?

const routes: Routes = [
  {
    path: 'dashboard',
    loadChildren: () => import('./dashboard/routes').then(
      i => i.routes
    ),
  },
  {
    path: 'roles',
    loadChildren: () => import('./roles/routes').then(
      i => i.routes
    ),
  },
  {
    path: 'product',
    loadChildren: () => import('./product/routes').then(
      i => i.routes
    ),
  },
  {
    path: '**',
    redirectTo: 'dashboard'
  },
];

default import support

export const routes = [{
  path: '',
  component: RolesComponent
}, {
  path: 'details/:id',
  component: RoleDetailsComponent
}];
export default [{
  path: '',
  component: RolesComponent
}, {
  path: 'details/:id',
  component: RoleDetailsComponent
}];

Route with Default Import

const routes: Routes = [
  {
    path: 'dashboard',
    loadChildren: () => import('./dashboard/routes'),
  },
  {
    path: 'roles',
    loadChildren: () => import('./roles/routes'),
  },
  {
    path: 'product',
    loadChildren: () => import('./product/routes')
  },
  {
    path: '**',
    redirectTo: 'dashboard'
  },
];

Demo + Code

Standalone

+

Single File Component

pankajparkar

Standalone Components 

ctd.

Bootstrap app without AppModule using `bootstrapApplication`

import {
  bootstrapApplication,
} from '@angular/platform-browser';

bootstrapApplication(
  AppComponent, {
    providers: []
  }
);

Standalone Components 

ctd.


@NgModule({
  declarations: [AppComponent],
  bootstrap:    [AppComponent],
  imports: [
    BrowserModule,
    AppRoutingModule,
    BrowserAnimationsModule,
  ],
  providers: [],
})
export class AppModule { }

Bootstrap app without AppModule using `bootstrapApplication`

@Component({
  selector: 'app-root',
  standalone: true, // <- add standalone
  imports: [], // <- and imports array
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {
}
bootstrapApplication(
  AppComponent, 
  {
    providers: [
      importProvidersFrom([
        BrowserModule,
        RouterModule.forRoot(routes),
        BrowserAnimationsModule,
      ])
    ]
  }
)

importProvidersFrom

Collects providers from all NgModules and standalone components, including transitively imported ones.

importProvidersFrom([
  BrowserModule,
  RouterModule.forRoot(routes),
  BrowserAnimationsModule,
  HttpClientModule,
]),

pankajparkar

const routes: Routes = [
  {
    path: 'dashboard',
    loadChildren: () => import('./dashboard/routes'),
  },
  {
    path: 'roles',
    loadChildren: () => import('./roles/routes'),
  },
  {
    path: 'product',
    loadChildren: () => import('./product/routes')
  },
  {
    path: '**',
    redirectTo: 'dashboard'
  },
];

Route

Current Way of using Library

npm i -S weather-widget
@NgModule({
  imports: [
    ...,
    // import library NgModule
    WeatherWidgetModule,
    ...
  ]
})
export class AppModule { }
<sc-weather-widget
  [city]="cityDetails">
</sc-weather-widget>
@NgModule({
  imports: [
    ...,
    // import component directly
    WeatherWidgetComponent,
    ...
  ]
})
export class DashboardComponent { }

pankajparkar

More Granular Imports

pankajparkar

HttpModule => provideHttpClient()

RouterModule => providerRouter()

CommonMdoule => NgIf, NgForOf, NgSwitch, etc.

Angular Elements with Standalone Component

bootstrapApplication(AppComponent)
  .then((ref) => {
    const wc = createCustomElement(
      HelloComponent, {
        injector: ref['injector'],
      }
    );
  });

pankajparkar

Angular Elements with Standalone Component

// get a hand on the environment injector
const app = await createApplication({
  providers: [
    /* your global providers here */
  ],
});

// create a constructor of a custom element
const NgElementCtor = createCustomElement(TestStandaloneCmp, {
  injector: app.injector
});

// register in a browser
customElements.define('test-standalone-cmp', NgElementCtor);

Ref - https://github.com/angular/angular/pull/46475

pankajparkar

EnvironmentInjector

and ENVIRONMENT_INITIALIZER

{
  provide: ENVIRONMENT_INITIALIZER,
  useValue: (fooService: FooService) => {
    fooService.initialize();
  },
  deps: [FooService]
},

pankajparkar

Migration

  • If you're using SCAM, it's very easy to migrate
  • Otherwise, new components can be created as a standalone
  • Library components can be converted to standalone.
  • You can use canMatch  on route level to migrate based on feature flag
import { 
  EditEmployeePage as NewEditEmployeePage,
} from './components/employees/edit-employee';
import { EditEmployeePage } from './employees/edit-employee';
...

provideRouter([
  {
    path: 'edit-employee/:id',
    component: NewEditEmployeePage,
    canMatch: () => inject(FeatureFlagService).isNewEditEmployeePageEnabled
  },
  {
    path: 'edit-employee/:id',
    component: EditEmployeePage
  }
])

SCAM

Single Component Angular Module

@Component(...)
export Class AutocompleteComponent {
  ...
}
@NgModule({
  imports: [
    CommonModule,
    ...
  ],
  declarations: [
    AutocompleteComponent
  ],
  exports: [AutocompleteComponent],
})
export Class AutocompleteModule {
  ...
}

Takeaways

  • Ng Modules are not going to deprecate 
  • The Standalone component is another architectural pattern to construct isolated component
  • Library authors would get good benefits from it
  • It will help new developers to absorb angular fast (without understanding NgModule)
  • Backward compatible

Demo - https://github.com/pankajparkar/sc-with-optional-ngmodule

pankajparkar

pankajparkar

Merry Chrismas

&

Happy New year in Advance

https://pankajparkar.dev

Pankaj P. Parkar

pankajparkar

Q & A

pankajparkar

pankajparkar

References

  • https://blog.angular.io/angular-v14-is-now-available-391a6db736af

  • https://netbasal.com/handling-page-titles-in-angular-40b53823af4a

  • https://blog.angular.io/angular-extended-diagnostics-53e2fa19ece9

  • https://marmicode.io/blog/angular-inject-and-injection-functions

  • https://nartc.me/blog/inheritance-angular-inject

  • https://netbasal.com/getting-to-know-the-environment-initializer-injection-token-in-angular-9622cb824f57

  • https://github.com/angular/angular/pull/46475

pankajparkar

Standalone Component with Optional NgModule

By Pankaj Parkar

Standalone Component with Optional NgModule

Standalone Component with Optional NgModule

  • 95