mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-16 22:26:16 -08:00
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:
28 files changed
+500
-11
No files matched your search
@@ -20,6 +20,7 @@ import {
|
||||
ProviderImportKind,
|
||||
SOURCE_TYPE_ICONS,
|
||||
} from '@iptvnator/shared/interfaces';
|
||||
import { provideFormFieldDefaults } from '@iptvnator/ui/components/form-field-defaults';
|
||||
|
||||
interface CandidateSummaryRow {
|
||||
labelKey: string;
|
||||
@@ -145,6 +146,7 @@ function maskUrlQueryPasswords(url: string): string {
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-auto-import',
|
||||
providers: [provideFormFieldDefaults()],
|
||||
templateUrl: './auto-import.component.html',
|
||||
styleUrl: './auto-import.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
|
||||
+2
@@ -50,6 +50,7 @@ import {
|
||||
toStalkerImportFeedback,
|
||||
toStalkerPlaylistIdentityFields,
|
||||
} from './stalker-import-identity';
|
||||
import { provideFormFieldDefaults } from '@iptvnator/ui/components/form-field-defaults';
|
||||
|
||||
/**
|
||||
* A MAC and the device IDs that belong to exactly it. Kept together because
|
||||
@@ -75,6 +76,7 @@ interface StalkerSettledIdentity {
|
||||
TranslatePipe,
|
||||
],
|
||||
selector: 'app-stalker-portal-import',
|
||||
providers: [provideFormFieldDefaults()],
|
||||
templateUrl: './stalker-portal-import.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
styles: [
|
||||
|
||||
@@ -12,9 +12,11 @@ import {
|
||||
} from '@angular/forms';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { provideFormFieldDefaults } from '@iptvnator/ui/components/form-field-defaults';
|
||||
|
||||
@Component({
|
||||
selector: 'app-text-import',
|
||||
providers: [provideFormFieldDefaults()],
|
||||
templateUrl: './text-import.component.html',
|
||||
styleUrls: ['./text-import.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
|
||||
@@ -15,9 +15,11 @@ import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { RuntimeCapabilitiesService } from '@iptvnator/services';
|
||||
import { provideFormFieldDefaults } from '@iptvnator/ui/components/form-field-defaults';
|
||||
|
||||
@Component({
|
||||
selector: 'app-url-upload',
|
||||
providers: [provideFormFieldDefaults()],
|
||||
templateUrl: './url-upload.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
|
||||
+20
@@ -228,6 +228,26 @@ describe('XtreamCodeImportComponent form', () => {
|
||||
);
|
||||
}
|
||||
|
||||
it('outlines its fields with a dynamic subscript without a root provider', () => {
|
||||
// The root injector no longer provides MAT_FORM_FIELD_DEFAULT_OPTIONS
|
||||
// (it pulled Material's form field and @angular/forms into startup);
|
||||
// the form carries provideFormFieldDefaults() itself.
|
||||
const { root } = render();
|
||||
const fields = Array.from(root.querySelectorAll('mat-form-field'));
|
||||
|
||||
expect(fields.length).toBeGreaterThan(0);
|
||||
for (const field of fields) {
|
||||
expect(field.classList).toContain(
|
||||
'mat-form-field-appearance-outline'
|
||||
);
|
||||
expect(
|
||||
field.querySelector(
|
||||
'.mat-mdc-form-field-subscript-dynamic-size'
|
||||
)
|
||||
).not.toBeNull();
|
||||
}
|
||||
});
|
||||
|
||||
it('labels the name field like the other add-source forms', () => {
|
||||
const { root } = render();
|
||||
|
||||
|
||||
+2
@@ -30,6 +30,7 @@ import {
|
||||
normalizeXtreamServerUrl,
|
||||
Playlist,
|
||||
} from '@iptvnator/shared/interfaces';
|
||||
import { provideFormFieldDefaults } from '@iptvnator/ui/components/form-field-defaults';
|
||||
|
||||
function xtreamServerUrlValidator(
|
||||
control: AbstractControl
|
||||
@@ -59,6 +60,7 @@ function xtreamServerUrlValidator(
|
||||
TranslatePipe,
|
||||
],
|
||||
selector: 'app-xtream-code-import',
|
||||
providers: [provideFormFieldDefaults()],
|
||||
templateUrl: './xtream-code-import.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
styles: [
|
||||
|
||||
+2
-1
@@ -62,6 +62,7 @@ import {
|
||||
STALKER_PLAYLIST_CONNECTION_EDITOR,
|
||||
STALKER_PLAYLIST_CONNECTION_EDITOR_STATUS,
|
||||
} from './stalker-playlist-connection-editor.token';
|
||||
import { provideFormFieldDefaults } from '@iptvnator/ui/components/form-field-defaults';
|
||||
|
||||
type DesktopFileSaveBridge = Pick<
|
||||
typeof window.electron,
|
||||
@@ -165,7 +166,7 @@ type DesktopFileSaveBridge = Pick<
|
||||
}
|
||||
`,
|
||||
],
|
||||
providers: [DatePipe],
|
||||
providers: [provideFormFieldDefaults(), DatePipe],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
ClipboardModule,
|
||||
|
||||
@@ -9,6 +9,7 @@ export * from './lib/confirm-dialog/confirm-dialog.component';
|
||||
export * from './lib/confirm-dialog/dialog.service';
|
||||
export * from './lib/content-hero/content-hero.component';
|
||||
export * from './lib/expandable-text/expandable-text.component';
|
||||
export * from './lib/form-field-defaults/form-field-defaults';
|
||||
export * from './lib/external-playback-dock/external-playback-dock.component';
|
||||
export * from './lib/portal-detail-shell/content-about.component';
|
||||
export * from './lib/portal-detail-shell/detail-template.directives';
|
||||
|
||||
+2
@@ -22,6 +22,7 @@ import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { EpgRuntimeBridgeService, EpgService } from '@iptvnator/epg/data-access';
|
||||
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
|
||||
import { debounceTime, from, Subject, switchMap } from 'rxjs';
|
||||
import { provideFormFieldDefaults } from '../../form-field-defaults/form-field-defaults';
|
||||
|
||||
export interface EpgMappingDialogData {
|
||||
channelKey: string;
|
||||
@@ -39,6 +40,7 @@ const SEARCH_MIN_CHARS = 2;
|
||||
|
||||
@Component({
|
||||
selector: 'app-epg-mapping-dialog',
|
||||
providers: [provideFormFieldDefaults()],
|
||||
templateUrl: './epg-mapping-dialog.component.html',
|
||||
styleUrl: './epg-mapping-dialog.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Provider } from '@angular/core';
|
||||
import {
|
||||
MAT_FORM_FIELD_DEFAULT_OPTIONS,
|
||||
MatFormFieldDefaultOptions,
|
||||
} from '@angular/material/form-field';
|
||||
|
||||
/** The app's outlined, dynamically sized form fields (see m3-theme.scss). */
|
||||
export const APP_FORM_FIELD_DEFAULTS: MatFormFieldDefaultOptions = {
|
||||
appearance: 'outline',
|
||||
subscriptSizing: 'dynamic',
|
||||
};
|
||||
|
||||
/**
|
||||
* Provide the app's form-field defaults on a component that renders
|
||||
* `<mat-form-field>`. They are not provided in the root injector: importing
|
||||
* the token there loads `@angular/material/form-field` (and, since Material
|
||||
* 22.2, `@angular/forms`) on startup, while every form field lives in a lazy
|
||||
* chunk. `tools/nx/check-form-field-defaults.mjs` enforces this.
|
||||
*/
|
||||
export function provideFormFieldDefaults(): Provider {
|
||||
return {
|
||||
provide: MAT_FORM_FIELD_DEFAULT_OPTIONS,
|
||||
useValue: APP_FORM_FIELD_DEFAULTS,
|
||||
};
|
||||
}
|
||||
+2
@@ -27,6 +27,7 @@ import {
|
||||
PARENTAL_LOCK_PIN_MAX_LENGTH,
|
||||
PARENTAL_LOCK_PIN_MIN_LENGTH,
|
||||
} from '@iptvnator/shared/interfaces';
|
||||
import { provideFormFieldDefaults } from '../form-field-defaults/form-field-defaults';
|
||||
|
||||
export type ParentalLockPinDialogMode = 'unlock' | 'set';
|
||||
|
||||
@@ -59,6 +60,7 @@ export interface ParentalLockPinDialogData {
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-parental-lock-pin-dialog',
|
||||
providers: [provideFormFieldDefaults()],
|
||||
templateUrl: './parental-lock-pin-dialog.component.html',
|
||||
styleUrl: './parental-lock-pin-dialog.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
|
||||
Reference in new issue
Block a user