Jet’s components follow modern Angular patterns with standalone components, signal-based reactivity, and composition over inheritance.
Component Structure
All components in Jet follow a consistent structure with private fields prefixed with #:
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
DatePipe,
NgOptimizedImage,
ReactiveFormsModule,
MatButtonModule,
// ... other imports
PageComponent,
],
selector: 'jet-profile-page',
styleUrl: './profile-page.component.scss',
templateUrl: './profile-page.component.html',
})
export class ProfilePageComponent implements CanComponentDeactivate, OnInit {
readonly #formBuilder = inject(FormBuilder);
readonly #alertService = inject(AlertService);
readonly #loggerService = inject(LoggerService);
readonly #profileService = inject(ProfileService);
readonly #progressBarService = inject(ProgressBarService);
readonly #userService = inject(UserService);
readonly #translocoService = inject(TranslocoService);
#isLoading: boolean;
readonly #user: null | User;
protected readonly profile: WritableSignal<Profile | undefined>;
protected readonly profileFormGroup: FormGroup<{
full_name: FormControl<null | string>;
username: FormControl<null | string>;
}>;
public constructor() {
this.#isLoading = false;
this.#user = this.#userService.user();
this.profile = signal(undefined);
// ... initialization
}
}
Dependency Injection Pattern
Jet uses the modern inject() function for dependency injection:
readonly #breakpointObserver = inject(BreakpointObserver);
readonly #document = inject(DOCUMENT);
readonly #destroyRef = inject(DestroyRef);
readonly #router = inject(Router);
readonly #alertService = inject(AlertService);
readonly #analyticsService = inject(AnalyticsService);
readonly #settingsService = inject(SettingsService);
All injected dependencies are marked as readonly to prevent accidental reassignment.
Signal-Based Reactivity
Components use signals for reactive state management:
Writable Signals
protected readonly isMatSidenavOpen: WritableSignal<boolean>;
protected readonly profile: WritableSignal<Profile | undefined>;
public constructor() {
this.isMatSidenavOpen = signal(false);
this.profile = signal(undefined);
}
Computed Signals
this.#colorSchemeOption = computed(() => this.#settingsService.settings().colorSchemeOption);
this.#languageOption = computed(() => this.#settingsService.settings().languageOption);
this.matSidenavMode = computed(() => (this.isLargeViewport() ? 'side' : 'over'));
this.shouldAddSafeArea = computed(() =>
this.matSidenavMode() === 'over' ? true : !this.isMatSidenavOpen(),
);
Effects for Side Effects
effect(
() => {
this.#loggerService.logEffectRun('languageOption');
const languageOption: LanguageOption = this.#languageOption();
untracked(() => {
this.#loadFontPair(languageOption.fontPairUrl);
this.#setFontPair(languageOption.fontPair);
this.#setLanguage(languageOption.value);
});
},
{ debugName: 'languageOption' },
);
Components use reactive forms with typed form groups:
protected readonly profileFormGroup: FormGroup<{
full_name: FormControl<null | string>;
username: FormControl<null | string>;
}>;
public constructor() {
this.profileFormGroup = this.#formBuilder.group({
full_name: this.#formBuilder.control<null | string>(null, [Validators.maxLength(60)]),
username: this.#formBuilder.control<null | string>(null, [
Validators.maxLength(36),
Validators.minLength(3),
Validators.pattern(/^[a-z0-9_]+$/),
Validators.required,
]),
});
}
ViewChild Queries
Use the modern viewChild() function for template queries:
protected readonly avatarFileInputRef =
viewChild<ElementRef<HTMLInputElement>>('avatarFileInput');
Async Operations
Components handle async operations with proper loading states:
protected async updateProfile(partialProfile: Partial<Profile>): Promise<void> {
if (this.#isLoading) {
return;
}
this.#isLoading = true;
this.profileFormGroup.disable();
this.#progressBarService.showIndeterminateProgressBar();
try {
const { data } = await this.#profileService.updateAndSelectProfile(partialProfile);
this.profile.set(data);
this.#patchProfileFormGroup(data);
this.profileFormGroup.markAsPristine();
this.#alertService.showAlert(this.#translocoService.translate('alerts.profile-updated'));
} catch (exception: unknown) {
if (exception instanceof Error) {
this.#loggerService.logError(exception);
this.#alertService.showErrorAlert(exception.message);
} else {
this.#loggerService.logException(exception);
}
} finally {
this.#isLoading = false;
this.profileFormGroup.enable();
this.#progressBarService.hideProgressBar();
}
}
Router Event Handling
Components can subscribe to router events with automatic cleanup:
public ngOnInit(): void {
this.#router.events
.pipe(
filter(
(event: Event) =>
event instanceof NavigationStart ||
event instanceof NavigationCancel ||
event instanceof NavigationEnd ||
event instanceof NavigationError,
),
takeUntilDestroyed(this.#destroyRef),
)
.subscribe((event: Event) => {
if (event instanceof NavigationStart) {
this.#progressBarService.showQueryProgressBar();
return;
}
if (event instanceof NavigationEnd) {
this.activeNavigationMenuItemPath = event.url.split('?')[0];
}
if (event instanceof NavigationError) {
const error = event.error;
const message: string | undefined = error instanceof Error ? error.message : undefined;
this.#loggerService.logError(error);
this.#alertService.showErrorAlert(message);
}
this.#progressBarService.hideProgressBar();
});
}
Breakpoint Observation
Responsive behavior is implemented using Angular CDK’s BreakpointObserver:
this.isLargeViewport = signal(this.#breakpointObserver.isMatched(Breakpoints.Web));
this.matSidenavMode = computed(() => (this.isLargeViewport() ? 'side' : 'over'));
Component Interfaces
Components can implement custom interfaces for type safety:
export class ProfilePageComponent implements CanComponentDeactivate, OnInit {
public hasUnsavedChanges(): boolean {
return this.profileFormGroup.dirty;
}
}
Logging and Debugging
All components log their initialization for debugging:
public constructor() {
this.#loggerService.logComponentInitialization('HomePageComponent');
}
Always use OnPush change detection strategy combined with signals for optimal performance.
Best Practices
- Private Fields: Use
# prefix for private fields
- Readonly Injections: Mark injected dependencies as
readonly
- Signal-Based State: Use signals instead of traditional observables for component state
- Typed Forms: Always type FormGroups and FormControls
- Error Handling: Wrap async operations in try-catch blocks
- Loading States: Disable forms during async operations
- Cleanup: Use
takeUntilDestroyed() for automatic subscription cleanup