Once you internalize the Angular "Template Context", it becomes an indispensable practical framework for thinking—your primary compass for crafting top-tier Angular applications.

emoji_objects emoji_objects emoji_objects
Tomas Trajan

Tomas Trajan

@tomastrajan

Nov 22, 2022

15 min read

The Most Important Thing You Need To Understand About Angular
share

🧠 by Milad Fakurian 🖌️by Tomas Trajan

Foreword

Sure, this piece is quite extensive, but that’s not because Angular is inherently over-complicated. Rather, it’s about covering a broad spectrum and examining every possible perspective, so that by the end, you’ll walk away with a complete understanding.

You might skip straight to the section labeled “Template context” if you’re in a hurry, though I’d suggest going through everything in order 😉

TLDR; Within Angular, any declarables ( components, directives and pipes ) that reference one another in templates must belong to THE SAME “template context”, which is established via @NgModule (or by the component itself under the new stand-alone components STAC approach). A typical application features multiple “origin template contexts”: the eager ( root ) one for the initial load, plus one for every lazy loaded @NgModule. Conversely, each STAC comes with its own fully self-contained “template context”.


Update

Catch the recording of my ngIndia 2023 talk, where I dive into the Angular Template Context Mental Model!


Hi everyone!

A major wave of Angular excitement has recently swept through the community, and with good justification!

A new chapter of thrilling times for Angular has arrived!

Angular 13 marked the last release to drop the outdated ViewEngine rendering engine, making it the first purely IVY-based version.

With that hefty challenge behind them, the Angular team is now free to concentrate on rolling out impressive features such as fully typed reactive forms, enhanced developer experience, and the framework's own growth!

This area of focus primarily gave rise to the push enabling Angular applications to be built without @NgModules, instead leveraging the stand-alone components ( STAC ) approach, which debuted as a developer preview in Angular 14.

As Angular 15 has now been launched ( 17.11.2022 ), STACs have exited developer preview, gaining first-class backing from the Angular CLI and other framework elements.

Angular Stand-alone components mark a major milestone in the framework's evolution, which might revive earlier anxiety about a sweeping shift akin to migrating from Angular JS to Angular

Fortunately, that fear is completely unfounded!

What's more, we'll uncover that both the @NgModules-based strategy and the STAC approach rely on a shared foundational principle that remains consistently intact! Due to this, it offers proof to the contrary, Angular remains, and will continue to be, a highly stable and trustworthy technology.


I trust this has piqued your interest about that "shared foundational principle", and that's precisely what we'll dive into next!

However, before that, let's broaden our perspective and sketch out a bigger picture so everything clicks into place—starting with…

Mental models

A mental model is a way of explaining how someone perceives the way things operate in the real world. It captures the environment around us, the connections between its components, and a person's gut feelings about their own actions and outcomes. Mental models can influence behavior and define a strategy for tackling problems ( much like a personal algorithm ) and completing tasks Wikipedia

That formal description hints at its potential value, but let's examine it through a developer's lens using ideas we're all acquainted with.

Putting aside philosophical debates and aiming for a practical analogy, we can envision our brains as akin to a computer running software ( the mind ), where that software is trainable ( learning ).

Example: "A variable"

A handy piece of that "mind software" could be dubbed a "mental model". A prime instance of such a model, likely well-known to all of us, is "a variable", which typically gets explained like this…

  • variable holds a value ( typed / untyped / which type (depends on language) )

  • variable can be assigned a different value ( or not, if it's constant )

  • variable is reachable from certain spots but hidden from others ( e.g., scope, class property, … )

  • variable can be employed to fetch its value for calculations

  • the list goes on, varying with the specific language, scenario…

Armed with such a mental model, we can apply the idea of "variable" to reach objectives while coding features.

Each of us carries numerous mental models that we apply fluidly, often without realizing it, in everyday work!

Mental models frequently merge and stack on one another—take "variable" and "array", for instance, where arrays are often kept in variables but have their own mental model involving multiple values and processing methods like map or filter and beyond…

Limited mental models

Often, our mental model is just a simplified version, enough to accomplish tasks, and that's acceptable. Ultimately, the key is reaching the desired outcome, which can easily happen even with a surface-level grasp of the topic.

For instance, we could work with variables without any awareness of TypeScript's typing system. After all, we could label everything as any and still produce a functional app that users enjoy!

In such a scenario, we'd be relying on a constrained understanding, and while we might achieve results, those results would be less than ideal and might create problems later on, such as when we try to expand our untyped project or bring in a new developer to collaborate on it.

Drawing from my own work in Angular spanning development, consulting, and teaching across enterprise settings, it's common to see developers working with only a partial grasp of the "template context" concept. As with the earlier example, although apps get built successfully, they're frequently delivered in a flawed state that can restrict future growth and drive up maintenance expenses as business needs evolve!

A partial grasp of the "template context" concept often results in Angular apps that are in a poor state, which can hinder their progress and increase upkeep costs!


Templates

Let's shift to something more hands-on. In Angular, to render content, we rely on a component and, specifically, its template. Here, we use a declarative method to specify:

  • the content to show, e.g., <h1>Hello world</h1>

  • how it updates, e.g., <p>{{ user.name }}</p>

  • how to manage events, e.g., <button (click)="save()">Save</button>

This differs greatly from the outdated approach of imperative rendering, where you might directly assign to .innerHTML whenever a display update was needed.

Here’s a sample of what a component might look like...

@Component({
  selector: 'my-org-user-item',
  template: `
    <mat-card>
      <mat-card-header>
        <mat-card-title>{{ user.name }} {{ user.surname }}</mat-card-title>
        <mat-card-subtitle>{{ user.role }}</mat-card-subtitle>
      </mat-card-header>
      <mat-card-actions align="end" *ngIf="user.role === Role.ADMIN">
        <button mat-button (click)="edit()">Edit</button>
        <button mat-button (click)="remove()">Remove</button>
      </mat-card-actions>
    </mat-card>
  `,
})
export class UserItemComponent {
  @Input() user: User;

  edit() {
    /* ... */
  }

  remove() {
    /* ... */
  }
}

With Angular, you have the option to move your template into its own HTML file and point to it via the templateUrl attribute inside the @Component() decorator—though whether you do this is entirely up to personal taste.

Now, let’s zoom in on the template itself…

<mat-card>
  <mat-card-header>
    <mat-card-title>{{user.name}} {{user.surname}}</mat-card-title>
    <mat-card-subtitle>
      <mat-icon [svgIcon]="user.role"></mat-icon>
      {{user.role}}, {{user.lastLoggedIn | date }}
    </mat-card-subtitle>
  </mat-card-header>
  <mat-card-actions align="end" *ngIf="user.role === Role.ADMIN">
    <button mat-button (click)="edit()">Edit</button>
    <button mat-button (click)="remove()">Remove</button>
  </mat-card-actions>
</mat-card>

So what exactly are we looking at? The template mixes familiar pieces such as plain HTML <button> with less obvious constructs like <mat-card> or <mat-icon>.

In addition, the <button> carries an attribute that shares a similar naming style with those elements, namely mat-button. There’s also the | date pipe, which formats the value held in the lastLoggedIn property of the user object.

From this, it becomes clear that a template within an Angular component is able to work with the following:

  • native HTML elements, e.g. <button>, …

  • reusable Angular components, e.g. <mat-card>, <mat-icon>, …

  • Angular directives, like mat-button

  • Angular pipes, for instance | date or | json

Every Angular component has built-in access to all standard HTML elements inside its own template

The question remains: how do components, directives, and pipes from other sources get used?

Declarables

Within Angular, any construct tied to templates—covering components, directives, and pipes—is labeled a declarable. This is because each one must be listed in the declarations:[ ] array of exactly one parent @NgModule, or alternatively be launched as a standalone component, directive, or pipe…


Had we written the previous component and its template without any additional preparation…

<mat-card>
  <!-- other components -->
</mat-card>

Every Angular-based component (and other declarables) would trigger an error, as shown below.

Error: user-item.component.html:1:1 - error NG8001: 'mat-card' is not a known element:
1. If 'mat-card' is an Angular component, then verify that it is part of this module.
2. If 'mat-card' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '`@NgModule`.schemas' of this component to suppress this message.

For the time being, let's set aside the Web Component scenario and zero in solely on Angular. The error message provides a key clue: when is recognized as an Angular component, it must be included in “this module”.

A comparable situation arises if you adopt the fresh Angular stand-alone components methodology — allow me to demonstrate…

Error: user-item.component.html:1:1 - error NG8001: 'mat-card' is not a known element:
1. If 'mat-card' is an Angular component, then verify that it is included in the '@Component.imports' of this component.
2. If 'mat-card' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@Component.schemas' of this component to suppress this message.

The error we encounter is essentially the same, flagging that if <mat-card> is an Angular component, it must be included in the imports of “this component”.

Whether we work with ngModules or the newer standalone components, using another Angular component in our template triggers the identical error—showing that the “template context” concept remains just as vital and enduring.

This same principle applies to directives and pipes alike. If we try to use them without them being “a part” of the cur module or standalone component, an error will surface.

Before we reach our final point, let’s take one more brief detour. We’ll explore the finer details of the “template context”, offering practical examples and best practices for building real-world Angular apps, and then address the context itself…


Context

Context is the surrounding text that clarifies a word's meaning. Without it, we can't decide if “dish” points to the meal (like roasted chicken) or the plate it's served on.

In daily life, we often encounter contextual details—information that only makes sense within a specific framework, otherwise appearing irrelevant or confusing…

Fortunately, when it comes to software development, context takes on a much more specific definition!

Broadly speaking, context refers to elements that are connected, mutually accessible, or fall under the same scope.

Everything is now in its place. We’ve covered:

  • WHY the shift to NgModules and STAC is ahead, whether it's a cause for concern, and what consequences to expect.

  • HOW templates, components, directives, and pipes must share a module or component to work together—otherwise, errors occur.

  • the remaining piece is the WHAT: leveraging template contexts to create optimal Angular applications.

Now, let’s reshape your thinking with a new mental model—the “template context”—if you’re ready to go along! 😅

Template context

We’ll kick off with something simple: a component that incorporates another component in its template...

@Component({
  selector: 'my-org-root',
  template: ` <my-org-hello [name]="name"></my-org-hello> `,
})
export class AppComponent {
  name = 'Tomas Trajan';
}

The original NgModule approach

Suppose our AppComponent is associated with the AppModule, placing it inside that module’s declarations: [ ] array.

With @NgModule s, every component is tied to exactly one module, meaning it can appear in only a single declarations: [ ] array!

A basic version of our module might be structured as follows…

@NgModule({
  declarations: [AppComponent], // declared in exactly one module
  imports: [], // let's ignore this for now
  bootstrap: [AppComponent], // we have to bootstrap our app
})
export class AppModule {} // our first root template context

Attempting to compile this would trigger an error much like the previous ones, indicating that my-org-hello doesn’t belong to the same “template context” (Angular module).

The remedy is straightforward: simply include that component in the same declarations: [ ] array.

@NgModule({
  declarations: [AppComponent, HelloComponent], // <- added HelloComponent
  imports: [],
  bootstrap: [AppComponent],
})
export class AppModule {}
Single NgModule template context

Excellent — the build succeeds, and our app behaves exactly as it should!

Since both AppComponent and HelloComponent belong to the same “template context” (here, that’s @NgModule), we are free to place <my-org-hello> directly inside the template of AppComponent.

Multiple modules

Let’s take this scenario a step further by relocating HelloComponent into its own separate module, called HelloModule.

@NgModule({
  declarations: [HelloComponent], // <- declared in exactly one module
  imports: [],
  exports: [],
})
export class HelloModule {}

Because an Angular component can belong to the declarations: [ ] array of only one module, we must strip it from the declarations: [ ] of the AppModule, thereby excluding it from the module's “template context”.

Recall that each @NgModule establishes its own “template context”, within which declarables are mutually usable in templates. Our HelloComponent is thus now included in the fresh “template context” that the HelloModule creates. As a result, we end up with TWO “template contexts” that are at this point isolated from one another.

At this stage, the app breaks once more, displaying the same error as before: my-org-hello is treated as an unrecognized element.

Multiple NgModule template contexts (no reference)

Follow me on Twitter because you will get notified about new Angular blog posts and cool frontend stuff!😉


Privacy

To address this, we can include the HelloModule in the imports: [ ] array of the AppModule.

@NgModule({
  declarations: [AppComponent], // <- removed HelloComponent
  imports: [HelloModule], // <-added HelloModule
  bootstrap: [AppComponent],
})
export class AppModule {}

The HelloModule has now been included in the AppModule’s “template context”. That would seem like a step in the right direction, yet the very same error still halts the app?

Multiple NgModule template contexts (with reference / import)

The HelloComponent sits exclusively inside the HelloModule’s “template context” — it appears in the module's declarations: [ ] list but is omitted from exports: [ ], which is why this is happening.

Now, let’s correct that and observe the outcome.

@NgModule({
  declarations: [HelloComponent], // <- declared in exactly one module
  imports: [],
  exports: [HelloComponent], // <- added HelloComponent to exports
})
export class HelloModule {}
Multiple NgModule template contexts (with reference and export)

The application now works again!

Now, let’s pause and analyze the sequence of events:

  1. AppComponent is declared within the “template context” of AppModule

  2. HelloComponent is declared within the “template context” of HelloModule

  3. For AppComponent to reference HelloComponent in its template, the latter must reside in the same “template context” as the former, which is the one defined by AppModule

  4. We achieve that by placing HelloModule into the imports: [ ] array of AppModule—this grants AppModule visibility into the “template context” of HelloModule

  5. Additionally, HelloComponent must be included in the exports: [ ] array of HelloModule; otherwise, it remains hidden from the expanded “template context” of AppModule, which now encompasses HelloModule as well

Any component within a single “template context” stays inaccessible to other contexts unless it is explicitly exported via the exports: [ ] array!

Single Component Angular Module ( SCAM ) Pattern

The aforementioned HelloModule, which contains just one HelloComponent listed in both its declarations: [ ] and exports: [ ], embodies a well-established pattern in Angular called Single Component Angular Module, or SCAM ( an unfortunate acronym🤷♂️ ).

Keep this in mind when we later examine the stand-alone components approach, as each STAC functions precisely like a SCAM!

Useful privacy

In the earlier scenario, our HelloComponent was private to HelloModule, yet that privacy served no real purpose—it merely triggered a compilation error that we resolved by exposing the component. However, this does not imply that such privacy lacks value!

Consider, for instance, a specialized HelloAnimationComponent that renders a waving hand beside the name shown by HelloComponent.

This component is not meant—and should not be—used beyond the “template context” of HelloModule, to avoid unwanted dependencies, and therefore ought to remain private.

@NgModule({
  declarations: [HelloComponent, HelloAnimationComponent],
  imports: [],
  exports: [HelloComponent], // <- HelloAnimationComponentis NOT exported, it's private
})
export class HelloModule {}
Multiple NgModule template contexts (withprivacy)

Built-in modules

CommonModule and RouterModule are built-in modules in Angular that supply its own declarables, enabling their usage in our “template contexts.”

Consequently, these modules are no different from others and operate exactly according to the behavior described earlier.

Consider a practical case with the *ngIf directive, a highly useful tool that toggles portions of a component template based on whether the provided condition evaluates to true or false.

As documented, *ngIf belongs to both the declarations: [ ] and exports: [ ] arrays within CommonModule, which also encapsulates other standard directives and pipes such as *ngFor or | json.

@NgModule({
  declarations: [
    NgIf, // <- declared in exactly one module
    // other declarables... (some potentially private only for internal use
  ],
  exports: [
    NgIf,
    // other declarables which should be public...
  ],
})
export class CommonModule {} // just an example, part of Angular itself

Bringing CommonModule into the AppModule means every publicly exposed declarable inside it joins that “template context”, so the AppComponent template can reference them directly.

However, when you inspect a fresh Angular project, no CommonModule appears in the AppModule — yet using directives such as *ngIf or *ngFor still works without any error!? To pin down what’s happening here, we need to examine a new idea called…

Transitive “template context”

So far, we’ve established that placing a module (say HelloModule) inside the target module’s (e.g., AppModule) imports: [ ] array pulls all exported declarations: [ ] into that target module’s “template context”—but what occurs when we export additional modules alongside declarables?

Here’s a streamlined real-world case that captures this exact behavior, one that most Angular applications encounter in practice!

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule], // Angular module which sets up app infrastructure
}) // but also something else !
export class AppModule {}

@NgModule({
  declarations: [
    /* ... */
  ],
  imports: [],
  exports: [
    CommonModule, // <!-- important ! we're re-exporting a module
    // and other local declarables...
  ],
})
export class BrowserModule {} // just an example, part of Angular itself

@NgModule({
  declarations: [
    NgIf, // <- declared in exactly one module
    // other declarables... (some potentially private only for internal use
  ],
  exports: [
    NgIf,
    // other declarables which should be public...
  ],
})
export class CommonModule {} // just an example, part of Angular itself

By re-exporting a module ( say, the CommonModule ), all of its exported declarables ( for instance, NgIf ) automatically become available in the “template context” of the consuming module (the AppModule ).

Consequently, when gathering every declarable that should be available in the consumer’s “template context”, we have to traverse multiple template contexts — three in our current scenario, though the number can grow larger in real projects.

Lazy loading and bundling

The “template context” concept becomes critical when we consider bundle size optimization and lazy loading in an Angular application.

Let’s revisit our initial setup with AppModule and the HelloModule, and see what happens behind the scenes…

  • there are TWO distinct “template contexts”

  • since the AppModule serves as the application’s main entry point, it is loaded eagerly, which means it gets placed into the main.js file (the primary eager app bundle)

  • even though the HelloModule has its own “template context” ( giving it privacy ), it is included in the imports: [ ] array of the AppModule; as a result, it is also packed into the same eager main.js file

Both “template contexts” reside within the identical eagerly-loaded bundle, yet a hierarchy exists. The AppModule is the “origin template context” of the main.js bundle. All other “template contexts” that land in this same main.js bundle were brought in either directly or transitively, via chains of module references.

Interestingly, the same logic applies to lazy loaded @NgModules! Each lazy loaded module becomes a new “origin template context” for its bundle and gets its own some-lazy-bundle.js file.

For both eager and lazy loaded bundles, multiple “template contexts” can be grouped into a single bundle file

Real world use-cases and best practices

In real-world applications, we wouldn’t typically create a dedicated HelloModule just to establish a separate “template context” within the eager portion of the app. A common approach is to keep AppModule extremely lean, delegating all layout-related declarables (header, navigation, footer, menus, etc.) to a CoreModule.

Those declarables are then exported, enabling their use in the template of the root AppComponent.

As a result, the AppModule (which imports CoreModule) defines the “origin template context” for the eagerly loaded section of the application.

Every lazy feature, in turn, has its own “origin template context”, typically defined by the lazy loaded @NgModule ( for example, SomeLazyFeatureModule ).

Shared declarables used in multiple template contexts

As we’ve observed, a typical application is made up of a relatively small eager “template context” assembled by the AppModule, along with numerous lazy loaded features, each having its own lazy “template context”.

Each lazy feature usually depends on a shared set of base components — things that are helpful across all features, starting with what Angular’s CommonModule provides, and extending to custom buttons, form fields, and other generic widgets.

This creates a need to include those shared components in every lazy “template context”, and the standard solution is to create a SharedModule.

As previously established, any module can specify:

  • declarations - declarables confined to the local (module’s own) “template context”

  • imports - modules that bring their exported declarables into the local (module’s own) “template context”

  • exports - local declarables, plus any other re-exported modules, that get delivered into the consuming module’s “template context”

A prototypical implementation would be SharedModule

@NgModule({
  // local template context
  declarations: [
    // local declarables
    MyOrgAnimatedStarComponent, // can use *ngIf (CommonModule is in tpl ctx)
    MyOrgRatingComponent, // can use <animated-star> (MyOrgRatingComponent is in tpl ctx)
  ], // can use <mat-card> (MatCardModule is in tpl ctx)
  // CAN'T use <mat-toolbar> (export only)

  // brings their exported declarables into local template context
  imports: [
    CommonModule, // brings *ngIf, *ngFor, ...
    MatCardModule, // brings <mat-card>, ...
  ],

  // makes available for consumer template context
  export: [
    // delivers to consumer "template context"
    CommonModule, // consumer can use *ngIf, *ngFor, ...
    MatCardModule, // consumer can use <mat-card>, ...
    MatToolbarModule, // consumer can use <mat-toolbar>, ...
    MyOrgRatingComponent, // consumer can use <my-org-rating>
  ], // consumer CAN'T use <my-org-animated-start>
  // because it's not exported
})
export class SharedModule {} // defines "shared" "template context" (tpl ctx)

With this configuration, the dependency chain reaches three levels from the consumer’s perspective, for instance, when dealing with a lazily loaded UserModule. Suppose this UserModule includes SharedModule in its imports: [ ] array, yet omits CommonModule.

Under these circumstances, a component declared in UserModule (such as UserItemComponent) that relies on the *ngFor directive in its template would be resolved through the following sequence (where CTX denotes “template context”):

  1. CTX 1 | UserItemComponent references *ngFor in its template.

  2. CTX 1 | UserItemComponent is listed in the declarations: [ ] array of UserModule, thus it falls under the “template context” of that lazily loaded UserModule.

  3. CTX 1 | UserModule includes SharedModule in its imports: [ ] array.

  4. CTX 2 | SharedModule places CommonModule in its exports: [ ] array.

  5. CTX 3 | CommonModule exposes NgFor through its exports: [ ] array.

  6. CTX 3 | CommonModule also has NgFor in its declarations: [ ] array, meaning NgFor is owned by CommonModule (it can only be declared in one module) and is accessible solely by importing CommonModule.

Thus, in this scenario, we navigate through 3 distinct “template contexts” when assembling the template context for the lazily loaded UserModule and figuring out which directives are available in UserItemComponent’s template.

Stand-alone components approach

Now let’s run through the identical example, but with Angular STACs substituting for @NgModules.

@Component({
  standalone: true, // <-mark component as standalone
  selector: 'my-org-root',
  template: ` <my-org-hello [name]="name"></my-org-hello> `,
})
export class AppComponent {
  name = 'Tomas Trajan';
}

With a standalone AppComponent, the AppModule becomes unnecessary, and bootstrapping happens straight through boostrapApplication(AppComponent) instead — no module in sight!

This approach encounters the same kind of failure as before, with an error resembling those earlier ones around my-org-hello not being in the same “template context” ( except here the problem lies with the component imports rather than the module ).

@Component({
  standalone: true,
  selector: 'my-org-root',
  imports: [HelloComponent], // <-import another STAC
  template: ` <my-org-hello [name]="name"></my-org-hello> `,
})
export class AppComponent {
  name = 'Tomas Trajan';
}
Multiple STAC based template contexts

The solution involved bringing in the missing HelloComponent (now a standalone component) and including it in the imports: [ ] array of the parent standalone component, which takes on the role of handling its own “template context” since no @NgModuless are present.

Evidently, NgModules and Stand-alone components represent two paths to the same outcome—both carry the identical duty of defining and overseeing the “template context” for the components they encompass!

That’s precisely why the earlier claim was so bold: the “template context” ranks as “The Most Important Thing You Need To Understand About Angular,” since it captures the very essence of Angular’s functioning, and this core principle is set to remain unchanged for the long term!

STACs are similar to SCAMs

Take a look at the following example…

// SCAM

@Component({
  /* ... */
})
export class HelloComponent {}

@NgModule({
  declarations: [HelloComponent], // always only one declarable, the component
  exports: [HelloComponent], // always only one declarable, the component
  imports: [
    CommonModule,
    // other standard modules...
    // other SCAMs...
  ],
})
export class HelloModule {}

// is the same as

// STAC

@Component({
  standalone: true,
  imports: [
    // other STACs...
    // other standard modules...
    // other SCAMs...
  ],
  // with STACs we CAN'T specify exports: [] which fits the use case
  // because we should only export the component itself
  // and the component is exported implicitly (less verbose)
})
export class HelloComponent {}

Let’s take this basic case a step further and incorporate the *ngIf directive, which will conditionally display the hello message depending on user actions.

@Component({
  standalone: true,
  selector: 'my-org-root',
  imports: [HelloComponent], // <-import another STAC
  template: ` <my-org-hello [name]="name" *ngIf="showHello"></my-org-hello> `,
})
export class AppComponent {
  name = 'Tomas Trajan';
  showHello = true;
}

Once more, we’d hit an error, since we’re referencing a directive (a declarable) that doesn’t belong to our “template context”.

NG8103: The `*ngIf` directive was used in the template,
but neither the `NgIf` directive nor the `CommonModule` was imported.
Please make sure that either the `NgIf` directive or the `CommonModule`
is included in the `@Component.imports` array of this component

Because the parent @NgModule no longer exists, you must import CommonModule directly in the STAC component’s own imports: [ ] array.

@Component({
  standalone: true,
  selector: 'my-org-root',
  imports: [HelloComponent, CommonModule], // <-import CommonModule
  template: ` <my-org-hello [name]="name" *ngIf="showHello"></my-org-hello> `,
})
export class AppComponent {
  name = 'Tomas Trajan';
  showHello = true;
}

As one might anticipate, the STAC maintains its own “template context”, and when the CommonModule is brought into that same context, the *ngIf directive becomes available within it—allowing the template to leverage it without any extra effort.

Interestingly, Angular does more than just re-export the NgIf directive via CommonModule; it additionally offers this directive as a standalone entity ( SAD ?! 🤔😅 ), ready for direct use. Thanks to that, we’ll be in a position to place it straight into the imports: [ ] array of the parent STAC component.

@Component({
  standalone: true,
  selector: 'my-org-root',
  imports: [HelloComponent, NgIf], // <-import NgIf (STAC)
  template: ` <my-org-hello [name]="name" *ngIf="showHello"></my-org-hello> `,
})
export class AppComponent {
  name = 'Tomas Trajan';
  showHello = true;
}

Both approaches function identically; the sole distinction is that opting for CommonModule rather than NgIf results in a marginally larger bundle, since CommonModule includes implementations of additional common directives and pipes, such as *ngIf or | keyvalue.

Conversely, the majority of non-trivial applications will likely rely on most of these anyway, making this our first official encounter that reveals the trade-off between:

  • developer experience, which eliminates the necessity of manually curating an extensive, fully granular list of dependencies by leveraging entities like CommonModule that bundle them together

  • minimal bundle size, which requires handling a long, fully granular dependency list—demanding more developer effort but yielding the smallest possible bundle

The STACs do NOT support a multi-level transitive approach to “template context” when used in isolation, as they lack the ability to define an exports: [ ] array; consequently, each STAC must entirely outline its own template context*

  • (*) of course, STACs and the @NgModule approach can be seamlessly combined, and introducing @NgModules into the equation would restore multi-level transitivity capabilities

That was quite an intense journey! My guess is you were already acquainted with many facets of the template context, but I trust you discovered at least one or two novel insights nonetheless! 😅


Useful questions

Let’s examine several helpful questions to consider while developing your application!

  • is the current “template context” eager or lazy?

  • what is accessible in my present context? (do I possess everything required, or am I overloaded? (perf))

  • is a declarable accessible directly or in a transitive manner?

  • is a declarable introduced once or through several simultaneous imports? (this influences attempts to remove it from context when unneeded)

  • what will become inaccessible if I remove a specific import?

  • which declarables are utilized across multiple (or many) lazy-loaded “template contexts”? (ideal candidates for SharedModule)

  • do I aim to keep my “template contexts” as fine-grained as possible? (opt for STAC) or do I lean towards grouping for better DX? (SharedModule)

Wrap up

I hope you relished grasping the “template context” mental model, otherwise known as “The Most Important Thing You Need To Understand About Angular” 😉

Drop a comment if you’re interested in extending this discussion to topics like how it applies to Angular libraries and additional real-world scenarios of managing “template context” in large-scale Angular projects!

Kindly support this guide by forwarding it to colleagues who might find these insights valuable🙏.

Feel free to reach out with any queries via article responses or Twitter DMs at @tomastrajan.

And always remember, the future is bright

Obviously the bright Future

Sure, that’s the gleaming tomorrow! (📸 via Marc Zimmer )

Liking the look of the code preview? Check out our new theme plugin

Skol - the ultimate IDE theme

Skol - the ultimate IDE theme

Bring the aurora right into your editor. A straightforward yet striking dark theme that is easy on the eyes and looks fantastic.

Combine Angular with artificial intelligence for more robust interfaces

Video Training for Angular and AI

Angular + AI Video Course

This practical course walks you through embedding AI capabilities in Angular applications, using Hash Brown to craft responsive, smart interfaces.

Covering live streaming chat, tool invocation, generative UI, and structured output, one technique at a time.

Enjoying this content and eager to dive into Angular's cutting-edge Signal Forms?

Angular Signal Forms: A Practical, In-Depth Workshop

Angular Signal Forms: Hands-On Masterclass

Dive into Angular's cutting-edge Signal-Forms across 12 sequenced chapters, combining theory with practical exercises.

Explore form fundamentals, validation logic, custom control creation, nested form structures, migration paths, and much more.

Enjoying the content? Ready to take a deep dive into Angular's modern Signal Forms?

Angular Signal Forms: A Practical Deep Dive

Angular Signal Forms: Hands-On Masterclass

Angular’s Signal-Forms are broken down into 12 sequential chapters, each pairing conceptual lessons with practical exercises.

You’ll cover everything from core form handling and validation rules to building bespoke controls, nesting subforms, and planning your upgrade path.

Win win deal illustration

Stay in the loop
on fresh articles

Subscribe to Angular Experts Content Updates & News, and we will let you know the moment a new post about Angular, Ngrx, RxJs, or other exciting Frontend topics goes live!

Your email stays private with us—you have the option to opt out anytime!

Occasionally, emails might include promotional extras; check our Privacy policy for detailed info.

Your feedback & thoughts

Feel free to ask anything or contribute with your own insights and experiences regarding the subject

Tomas Trajan - GDE for Angular & Web Technologies

Tomas Trajan

Google Developer Expert (GDE)
for Angular & Web Technologies

Google Developer Experts logo X logo LinkedIn logo Github logo Github logo Spotify logo Medium logo public

My mission is to guide dev teams toward successful Angular projects by offering training and consulting work centered on Architecture and NgRx-based State management!

An Angular & Web Technologies GDE who splits his time between consulting and teaching Angular. These days he helps international enterprise customers build core features and architectures, adopt best practices, share know-how, and streamline how they work.

Tomas is constantly pushing to deliver real value to both clients and the developer community. That effort shows in his steady output of popular articles, his talks at global conferences and meetups, and his work on open-source projects.

52

Blog posts

4.7M

Blog views

3.5K

Github stars

612

Trained developers

39

Given talks

8

Capacity to eat another cake

You might also like

Check out following blog posts from Angular Experts to learn even more about related topics like Angular !

Top 10 Angular Architecture Mistakes You Really Want To Avoid

Top 10 Angular Architecture Mistakes You Really Want To Avoid

In 2024, Angular keeps changing for better with ever increasing pace, but the big picture remains the same which makes architecture know-how timeless and well worth your time!

emoji_objects emoji_objects emoji_objects
Tomas Trajan

Tomas Trajan

@tomastrajan

Sep 10, 2024

15 min read

Angular Signal Inputs

Angular Signal Inputs

Revolutionize Your Angular Components with the brand new Reactive Signal Inputs.

emoji_objects emoji_objects emoji_objects
Kevin Kreuzer

Kevin Kreuzer

@nivekcode

Jan 24, 2024

6 min read

Improving DX with new Angular @Input Value Transform

Improving DX with new Angular @Input Value Transform

Embrace the Future: Moving Beyond Getters and Setters! Learn how to leverage the power of custom transformers or the build in booleanAttribute and numberAttribute transformers.

emoji_objects emoji_objects emoji_objects
Kevin Kreuzer

Kevin Kreuzer

@nivekcode

Nov 18, 2023

3 min read

Leverage our vast know-how for your team's success

The Angular Experts team has accumulated years of hands-on experience consulting for both large corporations and early-stage startups, delivering hands-on workshops and tutorials, and maintaining a rich repository of open source projects. We take great pride in our extensive background in modern front-end technology and would be delighted to help your business flourish