Skip to content

Commit

Permalink
docs(core): add full example for IfLoadedDirective (#46425)
Browse files Browse the repository at this point in the history
This commit adds a full example for the IfLoadedDirective and moves
the code to the StackBlitz playground for structural directives.

Fixes #40739, #37119

PR Close #46425
  • Loading branch information
markostanimirovic authored and AndrewKushnir committed Jun 21, 2022
1 parent 639277b commit 0ac2e54
Show file tree
Hide file tree
Showing 6 changed files with 84 additions and 30 deletions.
Expand Up @@ -210,3 +210,7 @@ <h4>UnlessDirective with template</h4>
</p>
</ng-template>

<hr />

<h2 id="appIfLoaded">IfLoadedDirective</h2>
<app-hero></app-hero>
12 changes: 8 additions & 4 deletions aio/content/examples/structural-directives/src/app/app.module.ts
Expand Up @@ -5,15 +5,19 @@ import { BrowserModule } from '@angular/platform-browser';

import { AppComponent } from './app.component';
import { heroSwitchComponents } from './hero-switch.components';
import { HeroComponent } from './hero.component';
import { IfLoadedDirective } from './if-loaded.directive';
import { UnlessDirective } from './unless.directive';

@NgModule({
imports: [ BrowserModule, FormsModule ],
imports: [BrowserModule, FormsModule],
declarations: [
AppComponent,
heroSwitchComponents,
UnlessDirective
HeroComponent,
IfLoadedDirective,
UnlessDirective,
],
bootstrap: [ AppComponent ]
bootstrap: [AppComponent],
})
export class AppModule { }
export class AppModule {}
@@ -0,0 +1,19 @@
import { Component } from '@angular/core';

import { LoadingState } from './loading-state';
import { Hero, heroes } from './hero';

@Component({
selector: 'app-hero',
template: `
<button (click)="onLoadHero()">Load Hero</button>
<p *appIfLoaded="heroLoadingState">{{ heroLoadingState.data | json }}</p>
`,
})
export class HeroComponent {
heroLoadingState: LoadingState<Hero> = { type: 'loading' };

onLoadHero(): void {
this.heroLoadingState = { type: 'loaded', data: heroes[0] };
}
}
@@ -0,0 +1,30 @@
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';

import { Loaded, LoadingState } from './loading-state';

@Directive({ selector: '[appIfLoaded]' })
export class IfLoadedDirective<T> {
private isViewCreated = false;

@Input('appIfLoaded') set state(state: LoadingState<T>) {
if (!this.isViewCreated && state.type === 'loaded') {
this.viewContainerRef.createEmbeddedView(this.templateRef);
this.isViewCreated = true;
} else if (this.isViewCreated && state.type !== 'loaded') {
this.viewContainerRef.clear();
this.isViewCreated = false;
}
}

constructor(
private readonly viewContainerRef: ViewContainerRef,
private readonly templateRef: TemplateRef<unknown>
) {}

static ngTemplateGuard_appIfLoaded<T>(
dir: IfLoadedDirective<T>,
state: LoadingState<T>
): state is Loaded<T> {
return true;
}
}
@@ -0,0 +1,5 @@
export type Loaded<T> = { type: 'loaded', data: T };

export type Loading = { type: 'loading' };

export type LoadingState<T> = Loaded<T> | Loading;
44 changes: 18 additions & 26 deletions aio/content/guide/structural-directives.md
Expand Up @@ -245,35 +245,27 @@ The value of the property can be either a general type-narrowing function based

For example, consider the following structural directive that takes the result of a template expression as an input:

<code-example format="typescript" header="IfLoadedDirective" language="typescript">

export type Loaded&lt;T&gt; = { type: 'loaded', data: T };
export type Loading = { type: 'loading' };
export type LoadingState&lt;T&gt; = Loaded&lt;T&gt; | Loading;
export class IfLoadedDirective&lt;T&gt; {
&commat;Input('ifLoaded') set state(state: LoadingState&lt;T&gt;) {}
static ngTemplateGuard_state&lt;T&gt;(dir: IfLoadedDirective&lt;T&gt;, expr: LoadingState&lt;T&gt;): expr is Loaded&lt;T&gt; { return true; };
}

export interface Person {
name: string;
}

&commat;Component({
template: `&lt;div *ifLoaded="state">{{ state.data }}&lt;/div>`,
})
export class AppComponent {
state: LoadingState&lt;Person&gt;;
}

</code-example>
<code-tabs linenums="true">
<code-pane
header="src/app/if-loaded.directive.ts"
path="structural-directives/src/app/if-loaded.directive.ts">
</code-pane>
<code-pane
header="src/app/loading-state.ts"
path="structural-directives/src/app/loading-state.ts">
</code-pane>
<code-pane
header="src/app/hero.component.ts"
path="structural-directives/src/app/hero.component.ts">
</code-pane>
</code-tabs>

In this example, the `LoadingState<T>` type permits either of two states, `Loaded<T>` or `Loading`.
The expression used as the directive's `state` input is of the umbrella type `LoadingState`, as it's unknown what the loading state is at that point.
The expression used as the directive's `state` input (aliased as `appIfLoaded`) is of the umbrella type `LoadingState`, as it's unknown what the loading state is at that point.

The `IfLoadedDirective` definition declares the static field `ngTemplateGuard_state`, which expresses the narrowing behavior.
Within the `AppComponent` template, the `*ifLoaded` structural directive should render this template only when `state` is actually `Loaded<Person>`.
The type guard lets the type checker infer that the acceptable type of `state` within the template is a `Loaded<T>`, and further infer that `T` must be an instance of `Person`.
The `IfLoadedDirective` definition declares the static field `ngTemplateGuard_appIfLoaded`, which expresses the narrowing behavior.
Within the `AppComponent` template, the `*appIfLoaded` structural directive should render this template only when `state` is actually `Loaded<Hero>`.
The type guard lets the type checker infer that the acceptable type of `state` within the template is a `Loaded<T>`, and further infer that `T` must be an instance of `Hero`.

<a id="narrowing-context-type"></a>

Expand Down

0 comments on commit 0ac2e54

Please sign in to comment.