Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions apps/angular/4-typed-context-outlet/src/app/app.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<div class="person-section">
<person [person]="person">
<!-- Specify ="age" because otherwise it will be treated as string -->
<ng-template person let-name let-age="age">
<!-- Example simply doesn't work because it does not exist on number/string -->
<!-- {{ name }}: {{ age.itIsOkToBeDifferent() }} -->
<!-- In VS Code LSP works better than in JetBrains IDE, because VS Code shows types in template binding -->
<div class="person-card">
<div class="person-icon">👤</div>
<div class="person-data">
<div class="person-label">Profile</div>
<div class="person-name">{{ name }}</div>
<div class="person-age">Age: <span class="age-value">{{ age }}</span></div>
</div>
</div>
</ng-template>
</person>
</div>


<div class="students-section">
<h3>Students</h3>
<list [list]="students">
<ng-container *list="students as student; index as i">
<div class="student-card">
<div class="student-avatar">{{ student.name.charAt(0).toUpperCase() }}</div>
<div class="student-info">
<div class="student-name">{{ student.name }}</div>
<div class="student-age">{{ student.age }} years old</div>
</div>
<div class="student-badge">#{{ i + 1 }}</div>
</div>
</ng-container>
</list>
</div>

<div class="cities-section">
<h3>Cities Around the World</h3>
<list [list]="cities">
<ng-template [list]="cities" let-city let-i="index">
<div class="city-row">
<div class="city-flag">🏙️</div>
<div class="city-details">
<div class="city-name">{{ city.name }}</div>
<div class="city-country">{{ city.country }}</div>
<div class="city-meta">
<span class="city-continent">{{ city.continent }}</span> •
<span class="city-population">{{ city.population.toLocaleString() }} people</span> •
<span class="city-language">{{ city.language }}</span>
</div>
</div>
<div class="city-position">{{ i + 1 }}</div>
</div>
</ng-template>
</list>
</div>
61 changes: 34 additions & 27 deletions apps/angular/4-typed-context-outlet/src/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -1,44 +1,51 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { ListComponent } from './list.component';
import { PersonComponent } from './person.component';
import { ListComponent, ListDirective } from './list.component';
import { Person, PersonComponent, PersonDirective } from './person.component';

@Component({
imports: [PersonComponent, ListComponent],
selector: 'app-root',
template: `
<person [person]="person">
<ng-template #personRef let-name let-age="age">
{{ name }}: {{ age }}
</ng-template>
</person>
interface Student {
readonly name: string;
readonly age: number;
}

<list [list]="students">
<ng-template #listRef let-student let-i="index">
{{ student.name }}: {{ student.age }} - {{ i }}
</ng-template>
</list>
interface City {
readonly name: string;
readonly country: string;
readonly population: number;
readonly continent: string;
readonly language: string;
}

<list [list]="cities">
<ng-template #listRef let-city let-i="index">
{{ city.name }}: {{ city.country }} - {{ i }}
</ng-template>
</list>
`,
@Component({
imports: [PersonComponent, ListComponent, PersonDirective, ListDirective],
selector: 'app-root',
templateUrl: './app.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppComponent {
person = {
person: Person = {
name: 'toto',
age: 3,
};

students = [
students: Student[] = [
{ name: 'toto', age: 3 },
{ name: 'titi', age: 4 },
];

cities = [
{ name: 'Paris', country: 'France' },
{ name: 'Berlin', country: 'Germany' },
cities: City[] = [
{
name: 'Paris',
country: 'France',
population: 2161000,
continent: 'Europe',
language: 'French',
},
{
name: 'Berlin',
country: 'Germany',
population: 3645000,
continent: 'Europe',
language: 'German',
},
];
}
34 changes: 30 additions & 4 deletions apps/angular/4-typed-context-outlet/src/app/list.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,41 @@ import {
ChangeDetectionStrategy,
Component,
contentChild,
Directive,
input,
Signal,
TemplateRef,
} from '@angular/core';

interface ListContext<T> {
readonly $implicit: T;
// added list property only to match it with sugar syntax (which I don't like)
readonly list: T;
readonly index: number;
}

@Directive({
selector: 'ng-template[list]',
})
export class ListDirective<T> {
readonly list = input.required<T[]>();

static ngTemplateContextGuard<T>(
dir: ListDirective<T>,
ctx: unknown,
): ctx is ListContext<T> {
return true;
}
}

@Component({
selector: 'list',
template: `
@for (item of list(); track $index) {
<ng-container
*ngTemplateOutlet="
listTemplateRef() || emptyRef;
context: { $implicit: item, appList: item, index: $index }
context: { $implicit: item, list: item, index: $index }
" />
}

Expand All @@ -23,8 +46,11 @@ import {
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [NgTemplateOutlet],
})
export class ListComponent<TItem extends object> {
list = input.required<TItem[]>();
export class ListComponent<T> {
readonly list = input.required<T[]>();

listTemplateRef = contentChild('listRef', { read: TemplateRef });
// if required -> emptyRef becomes dead code
protected readonly listTemplateRef: Signal<
TemplateRef<ListContext<T>> | undefined
> = contentChild(ListDirective, { read: TemplateRef });
}
39 changes: 35 additions & 4 deletions apps/angular/4-typed-context-outlet/src/app/person.component.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,34 @@
import { NgTemplateOutlet } from '@angular/common';
import { Component, contentChild, input, TemplateRef } from '@angular/core';
import {
Component,
contentChild,
Directive,
input,
Signal,
TemplateRef,
} from '@angular/core';

interface PersonContext {
readonly $implicit: string;
readonly age: number;
}

export interface Person {
readonly name: string;
readonly age: number;
}

@Directive({
selector: 'ng-template[person]',
})
export class PersonDirective {
static ngTemplateContextGuard(
dir: PersonDirective,
ctx: unknown,
): ctx is PersonContext {
return true;
}
}

@Component({
imports: [NgTemplateOutlet],
Expand All @@ -15,7 +44,9 @@ import { Component, contentChild, input, TemplateRef } from '@angular/core';
`,
})
export class PersonComponent {
person = input.required<{ name: string; age: number }>();

personTemplateRef = contentChild('personRef', { read: TemplateRef });
readonly person = input.required<Person>();
protected readonly personTemplateRef: Signal<TemplateRef<PersonContext>> =
contentChild.required(PersonDirective, {
read: TemplateRef,
});
}
Loading
Loading