Skip to main content
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' },
);

Form Handling

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

  1. Private Fields: Use # prefix for private fields
  2. Readonly Injections: Mark injected dependencies as readonly
  3. Signal-Based State: Use signals instead of traditional observables for component state
  4. Typed Forms: Always type FormGroups and FormControls
  5. Error Handling: Wrap async operations in try-catch blocks
  6. Loading States: Disable forms during async operations
  7. Cleanup: Use takeUntilDestroyed() for automatic subscription cleanup

Build docs developers (and LLMs) love