mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-16 22:26:16 -08:00
perf(web): take the form-field defaults off the initial path
app.config.ts provided MAT_FORM_FIELD_DEFAULT_OPTIONS at the root, and that one import put the whole 54 KB chunk of @angular/material/form-field into the initial bundle, although every form field renders in a lazy route or a dialog. The bundler places a module in one chunk, so one eager import of any export carries the module's entire code. The defaults now travel with the element: AppFormFieldModule (@iptvnator/ui/components/form-field) exports MatFormFieldModule together with a directive that matches mat-form-field and provides the options in its element injector. That also reaches the dialogs that root services open, which a route-level provider would miss. Every component that renders a form field imports it, and tools/nx/check-form-field-defaults.mjs (CI) rejects one that does not. renderer.initialBytes: 1,596,201 -> 1,534,606 bytes (-61,595) on a local production build. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
8bf92f9d65
commit
73575ff092
26 files changed
+615
-35
No files matched your search
@@ -10,6 +10,7 @@ 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/external-playback-dock/external-playback-dock.component';
|
||||
export * from './lib/form-field/app-form-field.module';
|
||||
export * from './lib/portal-detail-shell/content-about.component';
|
||||
export * from './lib/portal-detail-shell/detail-template.directives';
|
||||
export * from './lib/portal-detail-shell/portal-detail-shell.component';
|
||||
|
||||
+2
-2
@@ -14,11 +14,11 @@ import {
|
||||
MatDialogModule,
|
||||
MatDialogRef,
|
||||
} from '@angular/material/dialog';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { AppFormFieldModule } from '../../form-field/app-form-field.module';
|
||||
import { EpgRuntimeBridgeService, EpgService } from '@iptvnator/epg/data-access';
|
||||
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
|
||||
import { debounceTime, from, Subject, switchMap } from 'rxjs';
|
||||
@@ -46,7 +46,7 @@ const SEARCH_MIN_CHARS = 2;
|
||||
FormsModule,
|
||||
MatButtonModule,
|
||||
MatDialogModule,
|
||||
MatFormFieldModule,
|
||||
AppFormFieldModule,
|
||||
MatIconModule,
|
||||
MatInputModule,
|
||||
MatProgressSpinnerModule,
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { AppFormFieldModule } from './app-form-field.module';
|
||||
|
||||
@Component({
|
||||
imports: [AppFormFieldModule, MatInputModule],
|
||||
template: `
|
||||
<mat-form-field>
|
||||
<mat-label>Name</mat-label>
|
||||
<input matInput />
|
||||
<mat-hint>Short</mat-hint>
|
||||
</mat-form-field>
|
||||
`,
|
||||
})
|
||||
class WithDefaultsComponent {}
|
||||
|
||||
@Component({
|
||||
imports: [MatFormFieldModule, MatInputModule],
|
||||
template: `
|
||||
<mat-form-field>
|
||||
<mat-label>Name</mat-label>
|
||||
<input matInput />
|
||||
</mat-form-field>
|
||||
`,
|
||||
})
|
||||
class WithoutDefaultsComponent {}
|
||||
|
||||
function formField(component: typeof WithDefaultsComponent): HTMLElement {
|
||||
const fixture = TestBed.createComponent(component);
|
||||
fixture.detectChanges();
|
||||
return fixture.nativeElement.querySelector('mat-form-field');
|
||||
}
|
||||
|
||||
describe('AppFormFieldModule', () => {
|
||||
it('gives a form field the outlined appearance and dynamic subscript', () => {
|
||||
const field = formField(WithDefaultsComponent);
|
||||
|
||||
expect(field.classList).toContain('mat-form-field-appearance-outline');
|
||||
expect(field.querySelector('.mdc-notched-outline')).not.toBeNull();
|
||||
// Dynamic sizing reserves no empty subscript row below the field.
|
||||
expect(
|
||||
field.querySelector('.mat-mdc-form-field-subscript-dynamic-size')
|
||||
).not.toBeNull();
|
||||
});
|
||||
|
||||
it('is the only source of those defaults: Material alone renders filled', () => {
|
||||
const field = formField(WithoutDefaultsComponent);
|
||||
|
||||
expect(field.classList).toContain('mat-form-field-appearance-fill');
|
||||
expect(field.querySelector('.mdc-notched-outline')).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Directive, NgModule } from '@angular/core';
|
||||
import {
|
||||
MAT_FORM_FIELD_DEFAULT_OPTIONS,
|
||||
MatFormFieldDefaultOptions,
|
||||
MatFormFieldModule,
|
||||
} from '@angular/material/form-field';
|
||||
|
||||
/** The app-wide form field: outlined, with hints and errors sized as needed. */
|
||||
export const APP_FORM_FIELD_DEFAULTS: MatFormFieldDefaultOptions = {
|
||||
appearance: 'outline',
|
||||
subscriptSizing: 'dynamic',
|
||||
};
|
||||
|
||||
/**
|
||||
* Gives every `<mat-form-field>` the app defaults through its own element
|
||||
* injector, which `MatFormField` reads before any environment injector.
|
||||
*
|
||||
* Provided at the root, the defaults were a static import of
|
||||
* `@angular/material/form-field` in `app.config.ts`, and one eager import of
|
||||
* any export puts a module's whole bundler chunk on the initial path: 54 KB
|
||||
* of form-field code for an app whose every form field renders in a lazy
|
||||
* route or dialog. A route-level provider would miss the dialogs that root
|
||||
* services open, so the defaults travel with the element instead.
|
||||
*/
|
||||
@Directive({
|
||||
// The directive must match the Material element itself: an attribute
|
||||
// would have to be added to every form field by hand.
|
||||
// eslint-disable-next-line @angular-eslint/directive-selector
|
||||
selector: 'mat-form-field',
|
||||
providers: [
|
||||
{
|
||||
provide: MAT_FORM_FIELD_DEFAULT_OPTIONS,
|
||||
useValue: APP_FORM_FIELD_DEFAULTS,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class AppFormFieldDefaultsDirective {}
|
||||
|
||||
/**
|
||||
* Import this, not `MatFormFieldModule`, wherever a template renders
|
||||
* `<mat-form-field>`; it exports Material's form field together with the
|
||||
* defaults directive. `pnpm run styles:form-field-defaults:validate` (CI)
|
||||
* rejects a component that renders a form field without it.
|
||||
*/
|
||||
@NgModule({
|
||||
imports: [MatFormFieldModule, AppFormFieldDefaultsDirective],
|
||||
exports: [MatFormFieldModule, AppFormFieldDefaultsDirective],
|
||||
})
|
||||
export class AppFormFieldModule {}
|
||||
+2
-2
@@ -16,10 +16,10 @@ import {
|
||||
MatDialogModule,
|
||||
MatDialogRef,
|
||||
} from '@angular/material/dialog';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { AppFormFieldModule } from '../form-field/app-form-field.module';
|
||||
import {
|
||||
createParentalLockPinThrottle,
|
||||
isValidParentalLockPin,
|
||||
@@ -66,7 +66,7 @@ export interface ParentalLockPinDialogData {
|
||||
FormsModule,
|
||||
MatButtonModule,
|
||||
MatDialogModule,
|
||||
MatFormFieldModule,
|
||||
AppFormFieldModule,
|
||||
MatIconModule,
|
||||
MatInputModule,
|
||||
TranslatePipe,
|
||||
|
||||
Reference in new issue
Block a user