1 2
1 2
1 2
Components
Manfred Steyer
This book is for sale at http://leanpub.com/standalone-components
This is a Leanpub book. Leanpub empowers authors and publishers with the Lean Publishing
process. Lean Publishing is the act of publishing an in-progress ebook using lightweight tools and
many iterations to get reader feedback, pivot until you have the right book and build traction once
you do.
Intro . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
Trainings and Consultancy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
The source code for this can be found in the form of a traditional Angular CLI workspace²
and as an Nx workspace³ that uses libraries as a replacement for NgModules.
²https://github.com/manfredsteyer/standalone-example-cli
³https://github.com/manfredsteyer/standalone-example-nx
Mental Model & Compatibility 3
However, the community was never really happy with this decision. Having another modular system
besides that of EcmaScript didn’t feel right. In addition, it raised the entry barrier for new Angular
developers. That is why the Angular team designed the new Ivy compiler so that the compiled
application works without modules at runtime. Each component compiled with Ivy has its own
compilation context. Even if that sounds grandiose, this context is just represented by two arrays
that refer to adjacent components, directives, and pipes.
Since the old compiler and the associated execution environment have now been permanently
removed from Angular as of Angular 13, it was time to anchor this option in Angular’s public
API. For some time there has been a design document and an associated RFC [RFC]. Both describe
a world where Angular modules are optional. The word optional is important here: Existing code
that relies on modules is still supported.
1 @Component({
2 standalone: true,
3 selector: 'app-root',
4 imports: [
5 RouterOutlet,
6 NavbarComponent,
7 SidebarComponent,
8 ],
9 templateUrl: './app.component.html',
10 styleUrls: ['./app.component.css']
11 })
12 export class AppComponent {
13 [...]
14 }
The imports define the compilation context: all the other building blocks the Standalone Compo-
nents is allowed to use. For instance, you use it to import further Standalone Component, but also
existing NgModules.
The exhaustive listing of all these building blocks makes the component self-sufficient and thus
increases its reusability in principle. It also forces us to think about the component’s dependencies.
Unfortunately, this task turns out to be extremely monotonous and time consuming.
Therefore, there are considerations to implement a kind of auto-import in the Angular Language
Service used by the IDEs. Analogous to the auto-import for TypeScript modules, the IDE of choice
could also suggest placing the corresponding entry in the imports array the first time a component,
pipe or directive is used in the template.
Mental Model & Compatibility 4
Mental Model
This is similar to Lars Nielsen⁴’s SCAM pattern. However, while SCAM uses an explicit module, here
we only talk about a thought one.
While this mental model is useful for understanding Angular’s behavior, it’s also important to see
that Angular doesn’t implement Standalone Components that way underneath the covers.
⁴https://twitter.com/LayZeeDK
Mental Model & Compatibility 5
1 @Pipe ({
2 standalone: true,
3 name: 'city',
4 pure: true
5 })
6 export class CityPipe implements PipeTransform {
7
8 transform (value: string, format: string): string {[…]}
9
10 }
1 @Directive ({
2 standalone: true,
3 selector: 'input [appCity]',
4 providers: […]
5 })
6 export class CityValidator implements Validator {
7
8 [...]
9
10 }
Thanks to tree-shakable providers, on the other hand, services have worked without modules for
quite a time. For this purpose the property providedIn has to be used:
1 @Injectable ({
2 providedIn: 'root'
3 })
4 export class FlightService {[…]}
The Angular team recommends, to use providedIn: 'root' whenever possible. It might come as
a surprise, but providedIn: 'root' also works with lazy loading: If you only use a service in lazy
parts of your application, it is loaded alongside them.
1 // main.ts
2
3 import { bootstrapApplication } from '@angular/platform-browser';
4 import { provideAnimations } from '@angular/platform-browser/animations';
5 import { AppComponent } from './app/app.component';
6 import { APP_ROUTES } from './app/app.routes';
7 import { provideRouter } from '@angular/router';
8 import { importProvidersFrom } from '@angular/core';
9
10 [...]
11
12 bootstrapApplication(AppComponent, {
13 providers: [
14 importProvidersFrom(HttpClientModule),
15 provideRouter(APP_ROUTES),
16 provideAnimations(),
17 importProvidersFrom(TicketsModule),
18 importProvidersFrom(LayoutModule),
19 ]
20 });
The first argument passed to bootstrapApplication is the main component. Here, it’s our
AppComponent. Via the second argument, we pass application-wide service providers. These are the
providers, you would register with the AppModule when going with NgModules.
The provided helper function importProvidersFrom allows bridging the gap to existing
NgModules. Please also note, that importProvidersFrom works with both NgModules but
also ModuleWithProviders as returned by methods like forRoot and forChild.
While this allows to immediately leverage existing NgModule-based APIs, we will see more and
more functions that replace the usage of importProvidersFrom in the future. For instance, to
register the router with a given configuration, the function provideRouter is used. Similarly,
provideAnimations setup up the Animation module.
But on the other side, we can also import a Standalone Component (Directive, Pipe) into an existing
NgModule:
1 @NgModule({
2 imports: [
3 CommonModule,
4
5 // Imported Standalone Component:
6 FlightCardComponent,
7 [...]
8 ],
9 declarations: [
10 MyTicketsComponent
11 ],
12 [...]
13 })
14 export class TicketsModule { }
Interestingly, standalone components are imported like modules and not declared like classic
components. This may be confusing at first glance, but it totally fits the mental model that views a
standalone component a component with its very own NgModule.
Also, declaring a traditional component defines a strong whole-part relationship: A traditional
component can only be declared by one module and then, it belongs to this module. However, a
standalone component doesn’t belong to any NgModule but it can be reused in several places. Hence,
using imports here really makes sense.
Mental Model & Compatibility 8
1 import {
2 AsyncPipe,
3 JsonPipe,
4 NgForOf,
5 NgIf
6 } from "@angular/common";
7
8 [...]
9
10 @Component({
11 standalone: true,
12 imports: [
13 // CommonModule,
14 NgIf,
15 NgForOf,
16 AsyncPipe,
17 JsonPipe,
18
19 FormsModule,
20 FlightCardComponent,
21 CityValidator,
22 ],
23 selector: 'flight-search',
24 templateUrl: './flight-search.component.html'
25 })
26 export class FlightSearchComponent implements OnInit {
27 [...]
28 }
This is possible, because the Angular team made Standalone Directives and Standalone Pipes out of
the building blocks provided by the CommonModule. Importing these building blocks in a fine grained
way will be especially interesting once IDEs provide auto-imports for standalone components. In
this case, the first usage of an building block like *ngIf will make the IDE to add it to the imports
array.
As we will see in a further part of this book, meanwhile also the RouterModule comes
with Standalone building-blocks. Hence, we can directly import RouterOutlet instead of
Mental Model & Compatibility 9
going with the whole RouterModule. When writing this, this was not yet possible for other
modules like the FormsModule or the HttpClientModule.
The source code for this can be found in the form of a traditional Angular CLI workspace⁵
and as an Nx workspace⁶ that uses libraries as a replacement for NgModules.
⁵https://github.com/manfredsteyer/standalone-example-cli
⁶https://github.com/manfredsteyer/standalone-example-nx
Architecture with Standalone Components 11
The best of this is, you get real modularization: Everything the barrel experts can be used by other
parts of your application. Everything else is your secret. You can modify these secrets as you want,
as long as the public API defined by your barrel stays backwards compatible.
In order to use the barrel, just point to it with an import:
1 import {
2 NavbarComponent,
3 SidebarComponent
4 } from './shell/index';
5
6 @Component({
7 standalone: true,
8 selector: 'app-root',
9 imports: [
10 RouterOutlet,
11
12 NavbarComponent,
13 SidebarComponent,
14 ],
15 templateUrl: './app.component.html',
16 styleUrls: ['./app.component.css']
17 })
18 export class AppComponent {
19 [...]
20 }
If you call your barrel index.ts, you can even omit the file name, as index is the default name when
configuring the TypeScript compiler to use Node.js-based conventions. Something that is the case
in the world of Angular and the CLI:
Architecture with Standalone Components 12
1 import {
2 NavbarComponent,
3 SidebarComponent
4 } from './shell';
5
6 @Component({
7 standalone: true,
8 selector: 'app-root',
9 imports: [
10 RouterOutlet,
11
12 NavbarComponent,
13 SidebarComponent,
14 ],
15 templateUrl: './app.component.html',
16 styleUrls: ['./app.component.css']
17 })
18 export class AppComponent {
19 [...]
20 }
Interestingly, such arrays remind us to the exports section of NgModules. Please note that your array
needs to be a constant. This is needed because the Angular Compiler uses it already at compile time.
Such arrays can be directly put into the imports array. No need for spreading them:
One more time I want to stress out that this array-based style should only be used with caution.
While it allows to group things that always go together it also makes your code less tree-shakable.
To bypass this, you can define path mappings for your barrels you import from in your TypeScript
configuration (tsconfig.json in the project’s root):
1 "paths": {
2 "@demo/shell": ["src/app/shell/index.ts"],
3 [...]
4 }
This allows direct access to the barrel using a well-defined name without having to worry about -
sometimes excessive - relative paths:
workspace. Libraries seem to be the better solution anyway, especially since they subdivide it more
and Nx prevents bypassing the barrel of a library.
This means that every library consists of a public – actually published – and a private part. The
library’s public and private APIs are also mentioned here. Everything the library exports through
its barrel is public. The rest is private and therefore a “secret” of the library that other parts of the
application cannot access.
It is precisely these types of “secrets” that are a simple but effective key to stable architectures,
especially since everything that is not published can easily be changed afterwards. The public API,
on the other hand, should only be changed with care, especially since a breaking change can cause
problems in other areas of the project.
An Nx project (workspace) that represents the individual sub-areas of the Angular solution as
libraries could use the following structure:
Structure of an Nx Solution
Each library receives a barrel that reflects the public API. The prefixes in the library names
reflect a categorization recommended by the Nx team. For example, feature libraries contain smart
components that know the use cases, while UI libraries contain reusable dumb components. The
domain library comes with the client-side view of our domain model and the services operating on
it, and utility libraries contain general auxiliary functions.
On the basis of such categories, Nx allows the definition of linting rules that prevent undesired access
between libraries. For example, you could specify that a domain library should only have access to
utility libraries and not to UI libraries:
Architecture with Standalone Components 16
If you want to see all of this in action, feel free to have a look at the Nx version of the example used
here. Your find the Source Code at GitHub⁸.
⁸https://github.com/manfredsteyer/demo-nx-standalone
Architecture with Standalone Components 17
Free ebook
Conclusion
Standalone Components make the future of Angular applications more lightweight. We don’t need
NgModules anymore. Instead, we just use EcmaScript modules. This makes Angular solutions more
straightforward and lowers the entry barrier into the world of the framework. Thanks to the mental
model, which regards standalone components as a combination of a component and a NgModule,
this new form of development remains compatible with existing code.
For the grouping of related building blocks, simple barrels are ideal for small solutions. For larger
projects, the transition to monorepos as offered by the CLI extension Nx seems to be the next logical
step. Libraries subdivide the overall solution here and offer public APIs based on barrels. In addition,
dependencies between libraries can be visualized and avoided using linting.
⁹https://www.angulararchitects.io/book
Routing and Lazy Loading
Since its first days, the Angular Router has always been quite coupled to NgModules. Hence, one
question that comes up when moving to Standalone Components is: How will routing and lazy
loading work without NgModules? This chapter provides answers and also shows, why the router
will become more important for Dependency Injection.
The source code for the examples used here can be found in the form of a traditional
Angular CLI workspace¹⁰ and as an Nx workspace¹¹ that uses libraries as a replacement
for NgModules.
1 // main.ts
2
3 import { importProvidersFrom } from '@angular/core';
4 import { bootstrapApplication } from '@angular/platform-browser';
5 import {
6 PreloadAllModules,
7 provideRouter,
8 withDebugTracing,
9 withPreloading,
10 withRouterConfig
11 }
12 from '@angular/router';
13
14 import { APP_ROUTES } from './app/app.routes';
15 [...]
16
17 bootstrapApplication(AppComponent, {
18 providers: [
19 importProvidersFrom(HttpClientModule),
¹⁰https://github.com/manfredsteyer/standalone-example-cli
¹¹https://github.com/manfredsteyer/standalone-example-nx
Routing and Lazy Loading 19
20 provideRouter(APP_ROUTES,
21 withPreloading(PreloadAllModules),
22 withDebugTracing(),
23 ),
24
25 [...]
26
27 importProvidersFrom(TicketsModule),
28 provideAnimations(),
29 importProvidersFrom(LayoutModule),
30 ]
31 });
The function provideRouter not only takes the root routes but also the implementation of additional
router features. These features are passed with functions having the naming pattern withXYZ, e. g.
withPreloading or withDebugTracing. As functions can easily be tree-shaken, this design decisions
makes the whole router more tree-shakable.
With the discussed functions, the Angular team also introduces a naming pattern, library
authors should follow. Hence, when adding a new library, we just need to look out for an
provideXYZ and for some optional withXYZ functions.
As currently not every library comes with a provideXYZ function yet, Angular comes with the
bridging function importProvidersFrom. It allows to get hold of all the providers defined in existing
NgModules and hence is the key for using them with Standalone Components.
I’m quite sure, the usage of importProvidersFrom will peak off over time, as more and more
libraries will provide functions for directly configuring their providers. For instance, NGRX recently
introduced a provideStore and a provideEffects function.
1 @Component({
2 standalone: true,
3 selector: 'app-root',
4 imports: [
5 // Just import the RouterModule:
6 // RouterModule,
7
8 // Better: Just import what you need:
9 RouterOutlet,
10 RouterLinkWithHref, // Angular 14
11 // RouterLink // Angular 15+
12
13 NavbarComponent,
14 SidebarComponent,
15 ],
16 templateUrl: './app.component.html',
17 styleUrls: ['./app.component.css']
18 })
19 export class AppComponent {
20 [...]
21 }
Just importing the actually needed directives is possible, because the router exposes them as
Standalone Directives. Please note that in Angular 14, RouterLinkWithHref is needed if you use
routerLink with an a-tag; in all other cases you should import RouterLink instead. As this is a bit
confusing, the Angular Team refactored this for Angular 15: Beginning with this version, RouterLink
is used in all cases.
In most cases, this is nothing we need to worry about when IDEs start providing auto-imports for
Standalone Components.
1 // app.routes.ts
2
3 import { Routes } from '@angular/router';
4 import { HomeComponent } from './home/home.component';
5
6 export const APP_ROUTES: Routes = [
7 {
8 path: '',
9 pathMatch: 'full',
10 redirectTo: 'home'
11 },
12 {
13 path: 'home',
14 component: HomeComponent
15 },
16
17 // Option 1: Lazy Loading another Routing Config
18 {
19 path: 'flight-booking',
20 loadChildren: () =>
21 import('./booking/flight-booking.routes')
22 .then(m => m.FLIGHT_BOOKING_ROUTES)
23 },
24
25 // Option 2: Directly Lazy Loading a Standalone Component
26 {
27 path: 'next-flight',
28 loadComponent: () =>
29 import('./next-flight/next-flight.component')
30 .then(m => m.NextFlightComponent)
31 },
32 [...]
33 ];
This removes the indirection via an NgModule and makes our code more explicit. As an alternative,
a lazy route can also directly point to a Standalone Component. For this, the above shown
loadComponent property is used.
I expect that most teams will favor the first option, because normally, an application needs to lazy
loading several routes that go together.
Routing and Lazy Loading 22
1 // booking/flight-booking.routes.ts
2
3 export const FLIGHT_BOOKING_ROUTES: Routes = [{
4 path: '',
5 component: FlightBookingComponent,
6 providers: [
7 provideBookingDomain(config)
8 ],
9 children: [
10 {
11 path: '',
12 pathMatch: 'full',
13 redirectTo: 'flight-search'
14 },
15 {
16 path: 'flight-search',
17 component: FlightSearchComponent
18 },
19 {
20 path: 'passenger-search',
21 component: PassengerSearchComponent
22 },
23 {
24 path: 'flight-edit/:id',
25 component: FlightEditComponent
26 }
27 ]
28 }];
As shown here, we can provide services for several routes by grouping them as child routes. In these
cases, a component-less parent route with an empty path (path: '') is used. This pattern is already
used for years to assign Guards to a group of routes.
Technically, using adding a providers array to a router configuration introduces a new injector at
the level of the route. Such an injector is called Environment Injector and replaces the concept of the
Routing and Lazy Loading 23
former (Ng)Module Injectors. The root injector and the platform injector are further Environment
Injectors.
Interestingly, this also decouples lazy loading from introducing further injection scopes. Previously,
each lazy NgModule introduced a new injection scope, while non-lazy NgModules never did. Now,
lazy loading itself doesn’t influence the scopes. Instead, now, you define new scopes by adding a
providers array to your routes, regardless if the route is lazy or not.
The Angular team recommends to use this providers array with caution and to favor providedIn:
'root' instead. As already mentioned in a previous chapter, also providedIn: 'root' allows for
lazy loading. If you just use a services provided with providedIn: 'root' in lazy parts of your
application, they will only be loaded together with them.
However, there is one situation where providedIn: 'root' does not work and hence the providers
array shown is needed, namely if you need to pass a configuration to a library. I’ve already indicated
this in the above example by passing a config object to my custom provideBookingDomain. The next
section provides a more elaborated example for this using NGRX.
21 provideEffects([]),
22 provideStoreDevtools(),
23
24 importProvidersFrom(TicketsModule),
25 provideAnimations(),
26 importProvidersFrom(LayoutModule),
27 ]
28 });
For this, we go with the functions provideStore, provideEffects, and provideStoreDevtools NGRX
comes with since version 14.3.
To allow lazy parts of the application to have their own feature slices, we call provideState and
provideEffects in the respective routing configuration:
While provideStore sets up the store at root level, provideState sets up additional feature slices.
For this, you can provide a feature or just a branch name with a reducer. Interestingly, the function
Routing and Lazy Loading 25
provideEffects is used at the root level but also at the level of lazy parts. Hence, it provides the
initial effects but also effects needed for a given feature slice.
25 `],
26 encapsulation: ViewEncapsulation.ShadowDom
27 })
28 export class ToggleComponent {
29
30 @Input() active = false;
31 @Output() change = new EventEmitter<boolean>();
32
33 toggle(): void {
34 this.active = !this.active;
35 this.change.emit(this.active);
36 }
37
38 }
1 npm i @angular/elements
In former days, @angular/elements also supported ng add. This support came with a schematic for
adding a needed polyfill. However, meanwhile, all browsers supported by Angular can deal with
Web Components natively. Hence, there is no need for such a polyfill anymore and so the support
for ng add was already removed some versions ago.
1 // main.ts
2
3 import { createCustomElement } from '@angular/elements';
4 import { createApplication } from '@angular/platform-browser';
5 import { ToggleComponent } from './app/toggle/toggle.component';
6
7 (async () => {
8
9 const app = await createApplication({
10 providers: [
11 /* your global providers here */
12 ],
13 });
14
15 const toogleElement = createCustomElement(ToggleComponent, {
16 injector: app.injector,
17 });
18
19 customElements.define('my-toggle', toogleElement);
20
21 })();
We could pass an array with providers to createApplication. This allows to provide services like
the HttpClient via the application’s root scope. In general, this option is needed when we want to
configure these providers, e. g. with a forRoot method or a provideXYZ function. In all other cases,
it’s preferable to just go with tree-shakable providers (providedIn: 'root').
The result of createApplication is a new ApplicationRef. We can pass it’s Injector alongside the
ToggleComponent to createCustomElement. The result is a custom element that can be registered
with the browser using customElements.define.
Please note that the current API does not allow for setting an own zone instance like the noop zone.
Instead, the Angular team wants to concentrate on new features for zone-less change detection in
the future.
Besides working with custom elements, the ApplicationRef at hand also allows for bootstrapping
several components as Angular applications:
1 app.injector.get(NgZone).run(() => {
2 app.bootstrap(ToggleComponent, 'my-a');
3 app.bootstrap(ToggleComponent, 'my-b');
4 });
When bootstrapping a component this way, one can overwrite the selector to use. Please note, that
one has to call bootstrap within a zone in order to get change detection.
Bootstrapping several components was originally done by placing several components in your
AppModule’s bootstrap array. The bootstrapApplication function used for bootstrapping Stan-
dalone Components does, however, not allow for this as the goal was to provide a simple API for
the most common use case.
As a custom element is threaded by the browser as a normal DOM node, we can use traditional
DOM calls to set up events and to assign values to properties:
Angular Elements with Standalone Components 30
1 <script>
2 const myToggle = document.getElementById('myToggle');
3
4 myToggle.addEventListener('change', (event) => {
5 console.log('active', event.detail);
6 });
7
8 setTimeout(() => {
9 myToggle.active = true;
10 }, 3000);
11 </script>
This Standalone Component calls our my-toggle web component. While the Angular compiler
is aware of all possible Angular components, it doesn’t know about web components. Hence, it
would throw an error when seeing the my-toggle tag. To avoid this, we need to register the
CUSTOM_ELEMENTS_SCHEMA schema.
Before, we did this with all the NgModules we wanted to use together with Web Components. Now,
we can directly register this schema with Standalone Components. Technically, this just disables the
compiler checks regarding possible tag names. This is binary - the checks are either on or off – and
there is no way to directly tell the compiler about the available web components.
To make this component appear on our page, we need to bootstrap it:
1 // main.ts
2
3 [...]
4 // Register web components ...
5 [...]
6
7 app.injector.get(NgZone).run(() => {
8 app.bootstrap(AppComponent);
9 });
1 <app-root></app-root>
1 "build": {
2 "builder": "@angular-devkit/build-angular:browser-esbuild",
3 [...]
4 }
Normally, you just have to add -esbuild at the end of the default builder.
The resulting bundles look like this:
Angular Elements with Standalone Components 32
1 948 favicon.ico
2 703 index.html
3 100 177 main.43BPAPVS.js
4 33 916 polyfills.M7XCYQVG.js
5 0 styles.VFXLKGBH.css
If you use your web component in an other web site, e. g. a CMS-driven one, just reference the main
bundle there and add a respective tag. Also, reference the polyfills. However, when using several
such bundles, you have to make sure, you only load the polyfills once.
Migrating for Angular Standalone
Components
After getting started with Standalone Components the question arises how to migrate an existing
Angular solution for a future without Angular modules. In this chapter I show four options to do so.
Even if that sounds smug, there is actually nothing wrong with it. Nobody is forcing us to convert
applications to Standalone Components. Angular will continue to support Angular modules. After
all, the entire ecosystem is based on it. You can therefore safely ignore Standalone Components or
only use this new option in new applications or application parts.
wonderfully together with Angular modules. Angular modules can be imported into Standalone
Components and vice versa.
For instance, the following listing shows a Standalone Component importing further NgModules:
To illustrate the other way round, this listing shows an NgModule importing a Standalone
Component:
1 @NgModule({
2 imports: [
3 CommonModule,
4
5 // Imported Standalone Component:
6 FlightCardComponent,
7 [...]
8 ],
9 declarations: [
10 MyTicketsComponent
11 ],
12 [...]
13 })
14 export class TicketsModule { }
This mutual compatibility is made possible by the mental model¹³ behind Standalone Components.
¹³https://www.angulararchitects.io/en/aktuelles/angulars-future-without-ngmodules-lightweight-solutions-on-top-of-standalone-
components/
Migrating for Angular Standalone Components 35
If the barrel is called index.ts, it is sufficient to import only the barrel folder. In addition to grouping,
this approach also has the advantage that barrels can be used to define public APIs: All building
blocks exported by the barrel can be used by other parts of the application. They just need to
import from the barrel. Everything else is considered an implementation detail that should not be
accessed by other application parts. Hence, such implementation details are quite easy to change
without producing breaking changes somewhere else. This is a simple but effective measure for
stable software architectures.
In a further step, each barrel could also receive a path mapping in the tsconfig.json. In this case,
the application can access the barrel using nice names similar to npm package names:
However, barrels also come with challenges: For example, they are often the cause of cyclical
dependencies:
Here, b.ts on the one hand is referenced by the barrel index.ts and on the other hand accesses the
barrel.
This problem can be avoided from the start with two simple rules that must be followed consistently:
• A barrel may only publish elements from its “area”. The “area” extends over the barrel’s folder
as well as its subfolders.
• Within each “area”, files reference each other using relative paths without using the barrel.
Although these rules sound a bit abstract at first glance, the implementation of this rule is easier
than you would think:
Migrating for Angular Standalone Components 37
Here, b.ts directly accesses a.ts located in the same “area” to avoid the cycle shown earlier. The
detour the barrel is avoided.
Another disadvantage is that each part of the program can bypass the specified barrels - and thus the
public API created with them. Relative paths to private parts of the respective “areas” are sufficient
for this.
This problem can be solved with linting. A linting rule would have to detect and denounce
unauthorized access. The popular tool Nx¹⁵ comes with such a rule, which can also be used to prevent
other unwanted accesses. The next section takes up this idea.
This linting rule allows enforcing a fixed frontend architecture. For example, the Nx team recom-
mends dividing a large application vertically by subject domains and horizontally by technical
library categories:
Architecture Matrix
Feature libraries contain smart components that implement use cases, while UI libraries house
reusable dump components. Domain libraries encapsulate the client-side domain model and services
that operate on it, and utility libraries group general utility functions.
With the linting rules mentioned, it can now be ensured that each layer may only access the
layers below it. Access to other domains can also be prevented. Libraries from the Booking area
are therefore not allowed to access libraries in Boarding. If you want to use certain constructs across
domains, they should be placed in the shared area, for example.
If someone violates one of these rules, the linter gives instant feedback:
Migrating for Angular Standalone Components 39
The folder structure used for this by Nx reflects the architecture matrix shown:
Structure of Nx workspace
The subfolders in libs represent the domains. The libraries found in it get a prefix like feature- or
domain-. These prefixes reflect the technical categories and thus the layers.
The nice thing about this fourth option is that it has long proven itself in interaction with Angular
modules for structuring large solutions:
Migrating for Angular Standalone Components 40
In this case, only the libraries are used for structuring: their barrels group related building blocks,
such as Standalone Components, and thanks to the linting rules mentioned, we can enforce our
architectures.
Free ebook
Save your ticket¹⁹ for one of our remote or on-site workshops now or request a company workshop²⁰
(online or In-House) for you and your team!
Besides this, we provide the following topics as part of our training or consultancy workshops:
²¹https://www.angulararchitects.io/en/angular-workshops/
²²https://www.angulararchitects.io/subscribe/
²³https://twitter.com/ManfredSteyer