02 Text článku
Tento článek vychází z přednášky Vojtecha Maška „Hidden gems in Angular“ z konference ngPoland 2025.
Angular mezi verzemi 16 a 20 přinesl řadu silných funkcí, které už dávno nejsou žhavá novinka, a přesto je překvapivě málokdo používá. Nejsou schované záměrně; jsou přímo v dokumentaci, jen se o nich málo ví. Tento článek projde tyhle „skryté poklady“: drobnosti, které dělají rozdíl a které jde pochopit a nasadit ještě dnes. Všechny ukázky sázejí na typovou bezpečnost, protože typově bezpečný TypeScript za to stojí.
<ng-content> fallback
ng-content existuje odjakživa, ale fallback (výchozí) obsah je poměrně nedávný přírůstek a snadno se přehlédne. Pokud do slotu nikdo žádný obsah nepošle, vykreslí se místo něj fallback:
<div class="card">
<h1>
<!-- když titulek chybí, použije se výchozí 👇 -->
<ng-content select=".title">Default title</ng-content>
</h1>
<p>
<ng-content select=".content"></ng-content>
</p>
</div>
Když se žádný .title nepromítne, automaticky se objeví „Default title“. Jednoduché a stojí to za použití.
Cast pipe: typová bezpečnost v šablonách
Tahle pipe za běhu nedělá vůbec nic: jen přetypuje vstup na zadaný typ:
({ name: 'cast' })
export class CastPipe implements PipeTransform {
transform<T>(input: unknown, _: T | undefined): T {
return input as T;
}
}
Samo o sobě to není nic moc. Zajímavé to začne být, jakmile ji v šabloně použijete stejně jako klíčové slovo as v TypeScriptu. Nejdřív v komponentě vystavte typy, na které chcete přetypovávat:
({})
export class MyComponent {
readonly Product!: Product;
readonly Advertisement!: Advertisement;
readonly Category!: Category;
}
A pak v šabloně:
@switch (item.__typename) {
@case ('Product') {
<my-product [product]="item | cast: Product" />
}
@case ('Advertisement') {
<my-advertisement [advert]="item | cast: Advertisement" />
}
@case ('Category') {
<my-category-preview [category]="item | cast: Category" />
}
}
Místo sahání po $any() nebo života s netypovanými šablonami dostanete pořádné přetypování. Pokud používáte GraphQL, diskriminátor __typename poznáte na první pohled. Máte tak jistotu, že typ sedí, a stejnou jistotu mají i komponenty, které ho přebírají.
Routing
Konfiguraci rout má v projektu každý a obvykle s sebou nese spoustu boilerplatu. Klasická modulová verze vypadá takto:
({
imports: [ RouterModule.forChild([
{
path: 'products',
loadComponent: () =>
import('./products.component')
.then(c => c.ProductsComponent),
},
{
path: 'product/:id',
loadChildren: () =>
import('./product.module')
.then(m => m.ProductModule),
}
])],
// ... zbytek modulu
})
export class ProductsModule {}
export const APP_ROUTES: Routes = [
{
path: 'home',
loadComponent: () =>
import('./home/home.component')
.then(c => c.HomeComponent),
},
{
path: 'platform',
loadChildren: () => import('./products.module')
.then(m => m.ProductsModule),
}
];
({
imports: [
// ... importy app modulu
RouterModule.forRoot(APP_ROUTES),
],
})
export class AppModule {}
Standalone routing s default exportem
Jedno malé klíčové slovo změní všechno: default u exportované komponenty.
({
selector: 'app-hello',
template: `...`
})
export default class MyComponent {}
export const ROUTES: Routes = [
{
path: 'lazy-hello',
loadComponent: () => import('./app-hello')
}
];
Protože se router může spolehnout na default export, z loadComponent zbude jen import(...). Žádný boilerplate .then(m => m.Something), který je v tuhle chvíli už jen šum bez významu. A má to ještě jeden příjemný vedlejší efekt: v souboru může být jen jeden default export, takže vás to tlačí k jedné komponentě na soubor.
Se standalone bootstrapem se celé nastavení routingu smrskne na minimum:
bootstrapApplication(
AppComponent,
{ providers: [ provideRouter(APP_ROUTES) ] }
);
loadChildren se řídí stejným pravidlem: soubor s routami je exportuje jako default a import zůstává holý.
const PRODUCTS_ROUTES: Routes = [
{
path: 'products',
loadComponent: () => import('./products.component'),
},
{
path: 'product/:id',
loadComponent: () => import('./product.component'),
},
];
export default PRODUCTS_ROUTES;
export const APP_ROUTES: Routes = [
{
path: 'home',
loadComponent: () => import('./home/home.component'),
},
{
path: 'platform',
loadChildren: () => import('./products.routing'),
},
];
Dynamické redirectTo
Věděli jste, že redirectTo může být funkce a že od Angularu 20 může dokonce vracet observable nebo promise? Moc lidí to neví.
export const APP_ROUTES: Routes = [
{
path: '',
pathMatch: 'full',
redirectTo: () => {
return inject(FeatureFlagService).getDefaultPage$();
},
},
{
path: 'a',
loadComponent: () => import('./a-page/a-page.component'),
},
{
path: 'b',
loadComponent: () => import('./b-page/b-page.component'),
},
];
S touhle syntaxí je A/B testování na routách hračka: načtete feature flag ze služby nebo z backendu a pošlete uživatele, kam potřebujete, bez jakéhokoli dalšího drátování.
Hybrid rendering
Díky routeru můžete používat celou sadu funkcí hybrid renderingu. Nejdřív rychlé připomenutí, jaké typy renderingu Angular nabízí:
Client-side rendering je to, co dělá většina single-page aplikací; přesně proto SPA vznikly. Server-side rendering je ten, u kterého se vtipkuje, že se vracíme k PHP. Nevracíme, a dělat na serveru to, co tam patří, je skvělé, zvlášť když to jde cachovat nebo před uživatelem schovat nějakou funkcionalitu. Statické generování může žít ve vaší CI nebo v lokálním buildu a je dost mocné: ve velkých produkčních projektech rychle zjistíte, že ne všechno se mění tak často a ne všechno je potřeba renderovat na vyžádání. Renderovat všechno on demand může stát stovky až tisíce dolarů na serverových účtech, takže stojí za to se nad tím zamyslet.
Dokumentace: angular.dev/guide/hybrid-rendering
Hybrid rendering na úrovni rout
Jakmile dodáte serverovou konfiguraci, můžete dělat server routing. V Angularu 19 to byl samostatný provider provideServerRouting(); dnes žije uvnitř provideServerRendering().
bootstrapApplication(
AppComponent,
{
providers: [
provideServerRendering(
withRoutes(SERVER_ROUTES)
),
],
},
);
Serverové routy vypadají skoro stejně jako ty klientské (píšete v podstatě totéž), ale umí víc: pro každou routu můžete určit render mode.
export const SERVER_ROUTES: ServerRoute[] = [
{
path: '', // výchozí routa "/" na klientovi (CSR)
renderMode: RenderMode.Client,
},
{
path: 'about-us', // statická stránka => prerender (SSG)
renderMode: RenderMode.Prerender,
},
{
path: 'dashboard', // potřebuje data konkrétního uživatele => SSR
renderMode: RenderMode.Server,
},
{
path: 'video/:id', // SSR nic nepřinese => renderovat jen na klientovi (CSR)
renderMode: RenderMode.Client,
},
{
path: '**', // Všechny ostatní routy se renderují na serveru (SSR)
renderMode: RenderMode.Server,
},
];
Zdůvodnění jednotlivých voleb:
Domovská stránka (
'') → Client. Je personalizovaná pro každého návštěvníka, od marketingových kampaní po A/B varianty, takže není moc co cachovat. Renderujte ji na klientovi, protože výpočetní výkon platí klient.O nás → Prerender. Tahle stránka se napsala jednou: udělali jste „export to HTML“ ve Figmě a hotovo. Nemá smysl ji pokaždé renderovat dynamicky. Prerender znamená, že se sestaví při buildu, uloží mezi assety a načte jako statické HTML velmi rychle. Žádné další náklady.
Dashboard → Server. Možná chcete něco schovat, třeba grafy nad drahým databázovým dotazem, který chcete spouštět jen jednou za pár minut. Udělejte to na serveru; tam to jde cachovat.
Video → Client. Jak byste vůbec prerenderovali video? Jediné, co by tam bylo, je placeholder, takže zvolíte client.
Všechno ostatní → Server. Wildcard je výchozí hodnota, ne pravidlo: každá nová routa dostane režim, který jí sedí, a právě tady se tenhle přístup začne vyplácet v architektuře.
Prerendering dynamických parametrů cesty
A co dynamická ID v routách, třeba články na blogu nebo produkty v e-shopu? Můžete se zeptat stejného CMS nebo backendu, získat všechna existující ID a vygenerovat všechny tyhle stránky, například jednou denně o půlnoci:
export const SERVER_ROUTES: ServerRoute[] = [
{
// prerender /product/1, /product/2 a /product/3 …
path: 'product/:id',
renderMode: RenderMode.Prerender,
async getPrerenderParams() {
const products = inject(ProductsService); // ['1', '2', '3', …]
const ids = await products.getAllProductIds();
return ids.map(id => ({ id })); // namapovat na { id: string }[]
}
}
];
Výsledek: rychlé, prerenderované stránky se všemi výhodami server renderingu, ale výpočet proběhne jen jednou, v CI během fáze prerenderingu. Od té chvíle je to jen statické HTML. A když uživatel naviguje někam, kde prerender není, pořád je to plně dynamická SPA. Těžko proti tomu něco namítat, zvlášť když si stavíte vlastní blog nebo začínáte na malém webu prodávat saunové doplňky.
Stavové kódy stránek
Serverové routy se hodí hlavně tehdy, když potřebujete vrátit správný stavový kód pro nenalezenou stránku (404). K requestu a response se pořád dostanete přes tokeny REQUEST a RESPONSE_INIT z @angular/core, ale pro statický stavový kód je definice routy o dost jednodušší.
export const SERVER_ROUTES: ServerRoute[] = [
{
path: 'page-not-found',
renderMode: RenderMode.Server,
status: 404,
},
];
Úhledné, jednoduché a bez hacků.
Transformace inputů
Inputy používá každý a mají spoustu přehlížených funkcí. transform na inputu je jedna z těch, po kterých byste měli sáhnout pokaždé, když máte chuť hodnotu ručně upravovat.
Transformace hodnoty se signálovými inputy
Tenhle příklad je důležitý proto, že místo [disabled]="true" můžete napsat holý atribut disabled a stejně dorazí jako boolean:
export class MyCheckboxComponent {
disabled = input(false, {
// podporuje zkrácený zápis <my-checkbox disabled />
transform:
(value: boolean | string) =>
typeof value === 'string' ? value === '' : value,
});
}
Angular přesně pro tenhle případ dodává booleanAttribute, takže v reálném kódu napíšete transform: booleanAttribute a ručně psanou verzi zahodíte. Jeho sourozenec numberAttribute dělá totéž pro čísla.
Transformace hodnoty s dekorátorem @Input
Transformy fungují i se starými dekorátorovými inputy, takže je můžete používat, i když jste ještě nemigrovali. Tady graf přijímá velikost jako čitelný řetězec a interně ji převádí na číslo:
({})
export class MyChartComponent {
// podporuje `<my-chart size="xl" />`
({
required: true,
transform: (value: 'sm' | 'md' | 'xl') => {
switch (value) {
case 'sm':
return 200;
case 'md':
return 400;
case 'xl':
return 800;
}
}
})
size!: number;
}
Jak dostat dynamická data do CSS
Teď transform zkombinujte s host bindingem na CSS proměnnou v pixelech a vaše CSS najednou zná velikost. Tím se řeší problém, na který jste nejspíš už narazili: jak dostat dynamická data do CSS.
({})
export class MyChartComponent {
('style.--chart-height.px') // 👈
({
required: true,
transform: (value: 'sm' | 'md' | 'xl') => {
switch (value) {
case 'sm':
return 200;
case 'md':
return 400;
case 'xl':
return 800;
}
}
})
size!: number;
}
Jde to i se signály? Samozřejmě. Totéž, jen místo @HostBinding použijete vlastnost host v definici komponenty:
({
host: {
'[style.--chart-height.px]': 'size()', // 👈
},
})
export class MyChartComponent {
size = input.required({
transform: (value: 'sm' | 'md' | 'xl') => {
switch (value) {
case 'sm':
return 200;
case 'md':
return 400;
case 'xl':
return 800;
}
}
});
}
Route input binding
Jediná funkce routeru odemkne spoustu možností: withComponentInputBinding().
bootstrapApplication(
AppComponent,
{
providers: [
provideRouter(
APP_ROUTES,
withComponentInputBinding() // 👈
)
]
}
);
Tím se inputy komponent začnou bindovat na parametry routy, query parametry, route data a výsledky resolverů. Žádné route.snapshot.paramMap.get(...): parametry se automaticky navážou na input, který se shoduje jménem, a při navigaci se samy aktualizují. Obzvlášť dobře to funguje se signálovými inputy:
({})
export default class ProductComponent {
// naváže se na :id z routy
id = input<string>();
// stejně to funguje s :productId
() productId!: string;
}
Funguje to i pro query parametry:
({})
export default class ProductsComponent {
// automaticky se naváže na query param ?page=42
page = input('1');
}
A můžete to začít kombinovat s transformy. Uživatelé čekají, že stránkování začíná na stránce 1, ne 0, tak dejte inputu výchozí hodnotu. A protože parametry routy vždy přijdou jako řetězce, prožeňte je přes numberAttribute:
({})
export default class ProductsComponent {
// ?page=4 převedeno z řetězce na číslo
page = input(1, { transform: numberAttribute });
}
Vaše stránka je správně inicializovaná na 1 a vždy je to číslo.
Vynucení typů direktivou
Případ z praxe: Swiper (swiper.js) se dnes dodává jako web component a v Angularu se používá snadno, ale config, který předáváte do <swiper-container>, je netypovaný. Custom elementy s sebou nenesou typy inputů tak jako komponenty Angularu. Řešení: malá direktiva, která se přes selektor připojí k elementu, deklaruje povinný typovaný input a hodnotu předá dál elementu:
({
selector: 'swiper-container[config]'
})
export class SwiperDirective {
config = input.required<SwiperOptions>();
private readonly element =
inject<ElementRef<SwiperContainer>>(ElementRef).nativeElement;
constructor() {
effect(() => Object.assign(this.element, this.config()));
afterNextRender(() => this.element.initialize());
}
}
<swiper-container init="false" [config]="swiperOptions">
<swiper-slide />
</swiper-container>
Stejný config, který se binduje na swiper container, se teď správně typově kontroluje. Předání dál není volitelné: když direktiva deklaruje input se stejným jménem, jako má bindovaná vlastnost, Angular předá hodnotu direktivě a na element ji nikdy nenastaví. Element samotný pořád potřebuje CUSTOM_ELEMENTS_SCHEMA na komponentě, která ho používá: direktiva typuje input, neučí Angular ten tag.
@defer
@defer se používá hodně, ale kde ho použít správně, už tak zřejmé není. Nejdřív proč vůbec existuje: před @defer znamenalo líné vykreslení těžké komponenty celou proceduru: lazy-loading komponenty, napsání wrapperu, ruční znovudeklarování a bindování všech inputů a outputů (tady s knihovnou třetí strany):
({
template: `
<ng-container
*ngxComponentOutlet="component | async"
></ng-container>`
})
class MyLazyComponent {
component = import('./my-actual-component')
.then(m => m.MyActualComponent);
// zkopírovat všechny inputy/outputy, aby se automaticky navázaly
() myInput;
() myOutput;
}
Developer experience nic moc. Dnes napíšete jen:
@defer (on viewport) {
<my-actual-component/>
} @placeholder {
<div class="my-actual-component-skeleton"></div>
}
Komponenta se načte, až když se placeholder posune do viewportu. Stejný výsledek, pár řádků. Pozor, on viewport ten blok @placeholder potřebuje: je to element, který prohlížeč ve skutečnosti sleduje.
Kdy deferovat? Jednoduché pravidlo
Kdykoli podmínka @if závisí na autentizaci, roli nebo feature flagu (na čemkoli, co nevidí každý uživatel), udělejte z ní místo toho @defer:
@defer (when isAdmin()) {
<admin-tools-panel/>
}
Uživatelé, kteří podmínkou nikdy neprojdou, kód nikdy nestáhnou. Jeden rozdíl oproti @if: when je trigger, ne binding. Jakmile se podmínka jednou splní, blok zůstane vykreslený, takže pokud má panel zase zmizet, nechte uvnitř @defer ještě @if.
View transitions v routeru
Jeden řádek a Angular začne automaticky animovat přechody mezi vašimi routami:
bootstrapApplication(
AppComponent,
{
providers: [
provideRouter(
APP_ROUTES,
withViewTransitions() // 👈
)
]
}
);
S malou definicí komponenty a pár řádky CSS můžete například při každé navigaci obsah prolnout a otočit:
({
template: `...`,
styles: `
.emoji {
view-transition-name: rotate;
}
`,
})
export class EmojiSlider {
count = input.required({ transform: numberAttribute });
emoji = computed(() =>
this.emojis[this.count() % this.emojis.length]
);
emojis = ['🤔', /*...*/];
}
bootstrapApplication(AppComponent, {
providers: [
provideRouter(
[
{ path: '', pathMatch: 'full', redirectTo: '/0' },
{ path: ':count', component: EmojiSlider },
],
withViewTransitions(),
withComponentInputBinding()
),
],
});
@keyframes rotate-out {
to {
transform: rotate(90deg);
}
}
@keyframes rotate-in {
from {
transform: rotate(-90deg);
}
}
::view-transition-old(rotate),
::view-transition-new(rotate) {
animation-duration: 200ms;
animation-name:
-ua-view-transition-fade-in, rotate-in;
}
::view-transition-old(rotate) {
animation-name:
-ua-view-transition-fade-out, rotate-out;
}
Výsledek: emoji, která se při každé navigaci mezi routami otáčejí a prolínají:

Migrace
Poslední poklad není API: je to fakt, že ty ostatní nikdy nemusíte zavádět ručně. Angular dodává oficiální schematic skoro pro každou funkci z tohoto článku a jsou to opravdové codemody, ne find-and-replace. Přepíšou vaše zdrojáky na místě, čeho nerozumí, toho se nedotknou, a vypíšou místa, která převést nedokázaly, takže přesně víte, co zbývá na vás. Všechny jsou sepsané na jedné stránce.
ng g @angular/core:standalone
ng g @angular/core:control-flow
ng g @angular/core:inject
ng g @angular/core:signal-input-migration
ng g @angular/core:output-migration
ng g @angular/core:signal-queries-migration
Co vám těch šest příkazů přinese:
standaloneběží ve třech průchodech, které si vyberete v promptu: převede každou komponentu, direktivu a pipe na standalone, smaže NgModuly, kterým pak nezbylo co deklarovat, a přesune aplikaci nabootstrapApplication- tedy na nastavení, které předpokládá sekce o routingu výše.control-flowpřepíše každé*ngIf,*ngFora*ngSwitchv šablonách na@if,@fora@switcha odstraní importy, které k tomu byly potřeba.injectpřesune injektování přes parametry konstruktoru na funkciinject(), kvůli přesnějším typům a podpoře generik, kterou dekorátorová forma nabídnout nedokáže.signal-input-migration,output-migrationasignal-queries-migrationpřevedou@Input,@Outputa dekorátorové query (@ViewChilda spol.) nainput(),output()a signálové query. Každá z nich přijímá--path, takže můžete převést jedinou feature složku a poslat ji ven, místo abyste otevírali pull request o tisíci souborech.
Referenční stránka jich uvádí víc, které stojí za klidné odpoledne: převod eagerly načítaných rout na lazy, úklid nepoužitých importů, self-closing tagy, NgClass a NgStyle na obyčejné class a style bindingy, RouterTestingModule v nastavení TestBedu a CommonModule na jednotlivé direktivy a pipes, které s sebou dřív tahal.
Spouštějte je po jedné na čistém working tree, přečtěte si diff, commitněte. Je to jedno odpoledne místo refaktoru na několik sprintů, který stejná změna stojí ručně, a každá další funkce Angularu, která přijde, počítá s tím, že už ho máte za sebou.
