perf(web): provide form-field defaults per component, not at the root (#1928)

* perf(web): provide form-field defaults per component, not at the root

app.config.ts provided MAT_FORM_FIELD_DEFAULT_OPTIONS in the root
injector. Importing the token kept @angular/material/form-field in
main.js, and Material 22.2's form field imports @angular/forms/signals,
which would add @angular/forms to startup as well (the reason the
Angular 22.2 dependency update exceeds the initial-bytes ratchet). Every
form field renders in a lazy chunk, so the outline + dynamic defaults
now come from provideFormFieldDefaults()
(@iptvnator/ui/components/form-field-defaults) on each component that
renders <mat-form-field>.

tools/nx/check-form-field-defaults.mjs (ui:form-field-defaults:validate,
run in CI) reports a component that renders a form field without the
provider, and any use of the token outside the shared provider. The
Xtream import form test asserts outlined, dynamically sized fields with
no root provider; without the component provider they render as fill.

renderer.initialBytes: 1,596,201 -> 1,534,604 bytes (-61,597) on a local
production build; the baseline is lowered to the measured value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* perf(web): cite #1928 as the initial-bytes baseline evidence

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(ui): check form-field providers per component on the TypeScript AST

The guard matched templates and providers with regular expressions over
the whole file: a quoted inline template went unread, one component's
provider covered another in the same file, an earlier array-valued
provider hid the call, and provideFormFieldDefaults() in app.config or a
route passed although it loads Material's form field on startup again.
It now reads each @Component decorator's own template and providers
array, and reports any provideFormFieldDefaults() call outside one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: 4gray <fourgray@proton.me>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
authored and GitHub committed 2026-10-13 21:53:08 +02:00
1 parent c51ef83b56
commit ec4b9286a7
28 files changed
+500 -11

No files matched your search

-5
View File
@@ -9,7 +9,6 @@ import {
OverlayContainer,
} from '@angular/cdk/overlay';
import { ApplicationConfig, inject, importProvidersFrom } from '@angular/core';
import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field';
import { provideAnimations } from '@angular/platform-browser/animations';
import { provideRouter, withComponentInputBinding } from '@angular/router';
import { provideServiceWorker } from '@angular/service-worker';
@@ -162,9 +161,5 @@ export const appConfig: ApplicationConfig = {
...provideParentalLockPrompt(),
provideParentalLockPlaylistCleanup(),
...provideXtreamDataSource(),
{
provide: MAT_FORM_FIELD_DEFAULT_OPTIONS,
useValue: { appearance: 'outline', subscriptSizing: 'dynamic' },
},
],
};
@@ -20,6 +20,7 @@ import {
} from '@iptvnator/shared/interfaces';
import { UpdateChannelOption } from './settings.models';
import { markSectionForCheckOnFormEvents } from './settings-section-form-render';
import { provideFormFieldDefaults } from '@iptvnator/ui/components/form-field-defaults';
interface SettingsUpdateStatusChip {
readonly labelKey: string;
@@ -28,6 +29,7 @@ interface SettingsUpdateStatusChip {
@Component({
selector: 'app-settings-about-section',
providers: [provideFormFieldDefaults()],
imports: [
MatButtonModule,
MatFormFieldModule,
@@ -23,9 +23,11 @@ import { EpgSourceStatusComponent } from '@iptvnator/ui/epg';
import { TranslateModule } from '@ngx-translate/core';
import { EpgViewModeOption } from './settings.models';
import { markSectionForCheckOnFormEvents } from './settings-section-form-render';
import { provideFormFieldDefaults } from '@iptvnator/ui/components/form-field-defaults';
@Component({
selector: 'app-settings-epg-section',
providers: [provideFormFieldDefaults()],
imports: [
EpgSourceStatusComponent,
MatButtonModule,
@@ -20,9 +20,11 @@ import {
ThemeOption,
} from './settings.models';
import { markSectionForCheckOnFormEvents } from './settings-section-form-render';
import { provideFormFieldDefaults } from '@iptvnator/ui/components/form-field-defaults';
@Component({
selector: 'app-settings-general-section',
providers: [provideFormFieldDefaults()],
imports: [
CommonModule,
MatFormFieldModule,
@@ -9,9 +9,11 @@ import {
} from '@angular/material/slide-toggle';
import { TranslateModule } from '@ngx-translate/core';
import { ParentalLockRelockMinutes } from '@iptvnator/shared/interfaces';
import { provideFormFieldDefaults } from '@iptvnator/ui/components/form-field-defaults';
@Component({
selector: 'app-settings-parental-lock-section',
providers: [provideFormFieldDefaults()],
imports: [
MatButtonModule,
MatFormFieldModule,
@@ -22,9 +22,11 @@ import {
} from '@iptvnator/shared/interfaces';
import { SettingsPlayerOption } from './settings.models';
import { markSectionForCheckOnFormEvents } from './settings-section-form-render';
import { provideFormFieldDefaults } from '@iptvnator/ui/components/form-field-defaults';
@Component({
selector: 'app-settings-playback-section',
providers: [provideFormFieldDefaults()],
imports: [
CommonModule,
MatButtonModule,
@@ -15,9 +15,11 @@ import { MatTooltipModule } from '@angular/material/tooltip';
import { TranslateModule } from '@ngx-translate/core';
import { QRCodeComponent } from 'angularx-qrcode';
import { markSectionForCheckOnFormEvents } from './settings-section-form-render';
import { provideFormFieldDefaults } from '@iptvnator/ui/components/form-field-defaults';
@Component({
selector: 'app-settings-remote-control-section',
providers: [provideFormFieldDefaults()],
imports: [
MatButtonModule,
MatFormFieldModule,
@@ -17,11 +17,13 @@ import { TranslateModule } from '@ngx-translate/core';
import { TmdbApiService, TmdbCacheService } from '@iptvnator/services';
import type { TmdbCacheStats } from '@iptvnator/shared/interfaces';
import { markSectionForCheckOnFormEvents } from './settings-section-form-render';
import { provideFormFieldDefaults } from '@iptvnator/ui/components/form-field-defaults';
type TmdbKeyTestState = 'idle' | 'testing' | 'success' | 'error';
@Component({
selector: 'app-settings-tmdb-section',
providers: [provideFormFieldDefaults()],
imports: [
MatButtonModule,
MatFormFieldModule,
+3 -2
View File
@@ -279,8 +279,9 @@ html {
// ─── Global refined outlined inputs ──────────────────────────────────────────
// Applied app-wide AFTER mat.all-component-themes so these overrides win the
// cascade. Pairs with MAT_FORM_FIELD_DEFAULT_OPTIONS (outline + dynamic) set
// in app.config.ts.
// cascade. Pairs with the outline + dynamic MAT_FORM_FIELD_DEFAULT_OPTIONS
// that provideFormFieldDefaults() (@iptvnator/ui/components) sets on each
// component rendering a form field.
$app-refined-input-density: mat.define-theme((
density: (scale: -2),
));