@molecule/app-angular

Framework · framework · App (browser) · v1.0.2 · Apache-2.0

Angular framework bindings for molecule.dev

npm install @molecule/app-angular

npm · Source on GitHub

How it works

@molecule/app-angular adapts the @molecule/* cores to the framework framework on the app (browser) side.

// main.ts — wire concrete providers into Angular DI:
import { bootstrapApplication } from '@angular/platform-browser'
import { provideMolecule } from '@molecule/app-angular'
import { createJWTAuthClient } from '@molecule/app-auth'
import { provider as stateProvider } from '@molecule/app-state-zustand'
import { provider as themeProvider } from '@molecule/app-theme-css-variables'

const authClient = createJWTAuthClient({ baseURL: '/api' })

bootstrapApplication(AppComponent, {
  providers: [
    provideMolecule({
      state: stateProvider,
      auth: authClient,
      theme: themeProvider,
    }),
  ],
})

// dashboard.component.ts — inject the Molecule services:
import { Component, inject } from '@angular/core'
import { MoleculeAuthService, MoleculeThemeService, t } from '@molecule/app-angular'

@Component({
  selector: 'app-dashboard',
  template: `
    <div [style.background]="(theme$ | async)?.colors.background">
      <h1>{{ t('dashboard.welcome', {}, { defaultValue: 'Welcome!' }) }}</h1>
      <p>{{ (user$ | async)?.name }}</p>
      <button (click)="logout()">
        {{ t('auth.logout', {}, { defaultValue: 'Log out' }) }}
      </button>
    </div>
  `,
})
class DashboardComponent {
  // Expose the reactive t() so template bindings re-evaluate on locale change.
  protected readonly t = t

  private authService = inject(MoleculeAuthService)
  private themeService = inject(MoleculeThemeService)

  user$ = this.authService.user$ // Observable<UserProfile | null>
  theme$ = this.themeService.theme$ // Observable<Theme>

  logout(): void {
    void this.authService.logout()
  }
}

Works with: @molecule/app-auth, @molecule/app-device, @molecule/app-forms, @molecule/app-http, @molecule/app-i18n, @molecule/app-logger, @molecule/app-platform, @molecule/app-push, @molecule/app-routing, @molecule/app-state, @molecule/app-storage, @molecule/app-theme, @molecule/app-ui, @molecule/app-utilities, @molecule/app-version

Reference

Angular framework bindings for molecule.dev.

Provides Angular-specific services and providers for all molecule core interfaces. This package enables the use of molecule's framework-agnostic interfaces with Angular's idioms (services, DI, RxJS observables, etc.).

Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit src/index.ts JSDoc, not this file.

Quick Start

// main.ts — wire concrete providers into Angular DI:
import { bootstrapApplication } from '@angular/platform-browser'
import { provideMolecule } from '@molecule/app-angular'
import { createJWTAuthClient } from '@molecule/app-auth'
import { provider as stateProvider } from '@molecule/app-state-zustand'
import { provider as themeProvider } from '@molecule/app-theme-css-variables'

const authClient = createJWTAuthClient({ baseURL: '/api' })

bootstrapApplication(AppComponent, {
  providers: [
    provideMolecule({
      state: stateProvider,
      auth: authClient,
      theme: themeProvider,
    }),
  ],
})

// dashboard.component.ts — inject the Molecule services:
import { Component, inject } from '@angular/core'
import { MoleculeAuthService, MoleculeThemeService, t } from '@molecule/app-angular'

@Component({
  selector: 'app-dashboard',
  template: `
    <div [style.background]="(theme$ | async)?.colors.background">
      <h1>{{ t('dashboard.welcome', {}, { defaultValue: 'Welcome!' }) }}</h1>
      <p>{{ (user$ | async)?.name }}</p>
      <button (click)="logout()">
        {{ t('auth.logout', {}, { defaultValue: 'Log out' }) }}
      </button>
    </div>
  `,
})
class DashboardComponent {
  // Expose the reactive t() so template bindings re-evaluate on locale change.
  protected readonly t = t

  private authService = inject(MoleculeAuthService)
  private themeService = inject(MoleculeThemeService)

  user$ = this.authService.user$ // Observable<UserProfile | null>
  theme$ = this.themeService.theme$ // Observable<Theme>

  logout(): void {
    void this.authService.logout()
  }
}

Type

framework

Installation

npm install @molecule/app-angular @angular/core @molecule/app-auth @molecule/app-device @molecule/app-forms @molecule/app-http @molecule/app-i18n @molecule/app-logger @molecule/app-platform @molecule/app-push @molecule/app-routing @molecule/app-state @molecule/app-storage @molecule/app-theme @molecule/app-ui @molecule/app-utilities @molecule/app-version rxjs

API

Interfaces

AuthState

Reactive authentication state snapshot (initialized, authenticated, user, loading, and error).

interface AuthState<T = UserProfile> {
  /**
   * The session's state as one value — render from this instead of combining
   * `initialized`/`authenticated`/`loading`. See `AuthStatus`.
   */
  status: AuthStatus
  /**
   * Whether auth state has been initialized.
   */
  initialized: boolean
  /**
   * Whether the user is authenticated.
   */
  authenticated: boolean
  /**
   * Current user (if authenticated).
   */
  user: T | null
  /**
   * Whether an auth operation is in progress.
   */
  loading: boolean
  /**
   * Last auth error (if any).
   */
  error: string | null
}

AuthClient

Auth client interface that all auth bond packages must implement. Provides login/logout/register flows, token management, profile updates, and auth state subscription.

interface AuthClient<T = UserProfile> {
  /**
   * Returns the current authentication state snapshot.
   */
  getState(): AuthState<T>
  /**
   * Returns whether the user is currently authenticated.
   */
  isAuthenticated(): boolean
  /**
   * Gets the current user.
   */
  getUser(): T | null
  /**
   * Updates the cached user object (state + persistent storage) without
   * hitting the network. Intended for local refreshes after a per-app
   * mutation (e.g., the user just PATCHed their own profile and the
   * server returned the canonical row). Does NOT change tokens.
   */
  setUser(user: T | null): void
  /**
   * Gets the current access token.
   */
  getAccessToken(): string | null
  /**
   * Stores the access token in the configured token storage adapter (in-memory
   * by default). Use this to seed the token after an out-of-band exchange (e.g.
   * the OAuth code→token redirect) instead of writing to `localStorage` directly,
   * which would violate the in-memory-default storage contract and make the bearer
   * token JS-readable (XSS-exfiltratable). Pass `null` to clear it.
   */
  setAccessToken(token: string | null): void
  /**
   * Gets the refresh token.
   */
  getRefreshToken(): string | null
  /**
   * Logs in with credentials.
   */
  login(credentials: LoginCredentials): Promise<AuthResult<T>>
  /**
   * Logs out the current user.
   */
  logout(): Promise<void>
  /**
   * Registers a new user.
   */
  register(data: RegisterData): Promise<AuthResult<T>>
  /**
   * Refreshes the access token.
   */
  refresh(): Promise<AuthResult<T>>
  /**
   * Requests a password reset.
   */
  requestPasswordReset(data: PasswordResetRequest): Promise<void>
  /**
   * Confirms a password reset.
   */
  confirmPasswordReset(data: PasswordResetConfirm): Promise<void>
  /**
   * Updates the current user's profile.
   */
  updateProfile(data: Partial<T>): Promise<T>
  /**
   * Changes the current user's password.
   */
  changePassword(oldPassword: string, newPassword: string): Promise<void>
  /**
   * Initializes auth state (e.g., from stored tokens).
   */
  initialize(): Promise<void>
  /**
   * Re-checks a restored session against the server, in the background — the
   * boot-time validation an app with a stored token needs when a revoked or
   * expired token must sign the user out. Call it AFTER the first render
   * (`initialize()` restores synchronously from storage; this costs one request).
   *
   * - The server confirms the user: the session is refreshed and `AuthState.status`
   *   stays `'authenticated'`.
   * - The server answers 4xx: the token is dead — the stored session is cleared and
   *   the status becomes `'unauthenticated'`.
   * - The server cannot answer (network failure or 5xx): the session is KEPT and the
   *   status becomes `'unavailable'`, so the app can retry instead of signing the
   *   user out over a bad connection.
   *
   * Do not hand-write this beside the client: a parallel token/user store for a
   * "validating"/"unavailable" flag is the race this method removes.
   *
   * Optional: a provider with no restorable session has nothing to validate.
   */
  validateSession?(): Promise<void>
  /**
   * Resolves when the session restore started by `initialize()` has settled — the point
   * after which the client knows whether this visitor is signed in.
   *
   * Resolves IMMEDIATELY when no restore is in flight: before `initialize()` is ever
   * called, and after it has finished. A restore that failed resolves too (it settled the
   * question), so an HTTP client can await this before every request without risking one
   * held forever. That is what makes the fetch-before-restore race impossible: a page that
   * fetches viewer-dependent data on mount waits here instead of going out anonymously
   * (see `setAuthGate` in `@molecule/app-http`).
   *
   * Optional: a provider that restores no session has nothing to wait for. Without it, an
   * HTTP client bonded to that provider sends first-paint requests unauthenticated.
   *
   * A provider whose restore sends a request THROUGH the gated HTTP client must send that
   * request with `skipAuthGate: true` (`RequestConfig` in `@molecule/app-http`): the gate
   * waits on this promise, so holding the restore's own request makes the gate wait on
   * itself and stalls every request it holds until it times out. `createJWTAuthClient`'s
   * restore does not need it — it uses `fetch`, not the bonded client.
   */
  whenRestored?(): Promise<void>
  /**
   * Subscribes to auth state changes.
   */
  subscribe(callback: (state: AuthState<T>) => void): () => void
  /**
   * Subscribes to auth state changes (alias for subscribe).
   */
  onAuthChange(callback: (state: AuthState<T>) => void): () => void
  /**
   * Gets the current access token (alias for getAccessToken).
   */
  getToken?(): string | null
  /**
   * Adds an auth event listener.
   */
  addEventListener(listener: AuthEventListener): () => void
  /**
   * Destroys the auth client.
   */
  destroy(): void
}

FormController

Form controller interface.

All form providers must implement this interface.

interface FormController<T extends Record<string, unknown> = Record<string, unknown>> {
  /**
   * Gets the current form state.
   */
  getState(): FormState<T>
  /**
   * Gets the value of a specific field.
   */
  getValue(name: string): unknown
  getValue<K extends keyof T>(name: K): T[K]
  /**
   * Gets all form values.
   */
  getValues(): T
  /**
   * Sets the value of a specific field.
   */
  setValue(
    name: string,
    value: unknown,
    options?: {
      shouldValidate?: boolean
      shouldDirty?: boolean
      shouldTouch?: boolean
    },
  ): void
  /**
   * Sets multiple values at once.
   */
  setValues(
    values: Partial<T>,
    options?: {
      shouldValidate?: boolean
    },
  ): void
  /**
   * Gets the error for a specific field.
   */
  getError(name: string): string | undefined
  /**
   * Sets the error for a specific field.
   */
  setError(name: string, error: string | undefined): void
  /**
   * Clears the error for a specific field.
   */
  clearError<K extends keyof T>(name: K): void
  /**
   * Clears all errors.
   */
  clearErrors(): void
  /**
   * Gets the field state for a specific field.
   */
  getFieldState<K extends keyof T>(name: K): FieldState<T[K]>
  /**
   * Registers a field for form management.
   */
  register(nameOrOptions: string | RegisterOptions, options?: RegisterOptions): FieldRegistration
  /**
   * Unregisters a field.
   */
  unregister(name: string): void
  /**
   * Validates a specific field.
   */
  validateField<K extends keyof T>(name: K): Promise<boolean>
  /**
   * Validates all fields.
   */
  validate(): Promise<boolean>
  /**
   * Resets the form to initial values.
   */
  reset(values?: Partial<T>): void
  /**
   * Handles form submission.
   */
  handleSubmit(
    onSubmit: (values: T) => void | Promise<void>,
    onError?: (errors: Partial<Record<keyof T, string>>) => void,
  ): (event?: { preventDefault?: () => void }) => Promise<void>
  /**
   * Sets focus to a field.
   */
  setFocus(name: keyof T): void
  /**
   * Subscribes to form state changes.
   */
  subscribe(callback: (state: FormState<T>) => void): () => void
  /**
   * Destroys the form controller.
   */
  destroy(): void
}

FormOptions

Form creation options.

interface FormOptions<T extends Record<string, unknown>> {
  /**
   * Default values.
   */
  defaultValues?: Partial<T>
  /**
   * Validation mode.
   */
  mode?: 'onSubmit' | 'onChange' | 'onBlur' | 'all'
  /**
   * Revalidation mode.
   */
  reValidateMode?: 'onChange' | 'onBlur' | 'onSubmit'
  /**
   * Whether to focus the first error field on submit.
   */
  shouldFocusError?: boolean
  /**
   * Form-level validation function.
   */
  validate?: (
    values: T,
  ) => Partial<Record<keyof T, string>> | Promise<Partial<Record<keyof T, string>>>
}

HttpClient

HTTP client interface.

All HTTP providers must implement this interface.

interface HttpClient {
  /**
   * Base URL for all requests.
   */
  baseURL: string
  /**
   * Default headers for all requests.
   */
  defaultHeaders: Record<string, string>
  /**
   * Makes a generic HTTP request. `data` is a field of THIS config object — the same body
   * `post(url, data, config)` takes as its second argument.
   */
  request<T = unknown>(config: FullRequestConfig): Promise<HttpResponse<T>>
  /**
   * Makes a GET request.
   */
  get<T = unknown>(url: string, config?: RequestConfig): Promise<HttpResponse<T>>
  /**
   * Makes a POST request. `data` is the request BODY, not a config object: the wire body is
   * `data` itself, so `post(url, { data: body })` sends `{"data":body}` — pass `body` directly.
   * A config (headers, params) is the THIRD argument.
   */
  post<T = unknown>(url: string, data?: unknown, config?: RequestConfig): Promise<HttpResponse<T>>
  /**
   * Makes a PUT request. `data` is the request BODY (second argument) — never `{ data: body }`,
   * which sends `{"data":body}`; a config is the THIRD argument.
   */
  put<T = unknown>(url: string, data?: unknown, config?: RequestConfig): Promise<HttpResponse<T>>
  /**
   * Makes a PATCH request. `data` is the request BODY (second argument) — never `{ data: body }`,
   * which sends `{"data":body}`; a config is the THIRD argument.
   */
  patch<T = unknown>(url: string, data?: unknown, config?: RequestConfig): Promise<HttpResponse<T>>
  /**
   * Makes a DELETE request.
   */
  delete<T = unknown>(url: string, config?: RequestConfig): Promise<HttpResponse<T>>
  /**
   * Adds a request interceptor.
   * Returns a function to remove the interceptor.
   */
  addRequestInterceptor(interceptor: RequestInterceptor): () => void
  /**
   * Adds a response interceptor.
   * Returns a function to remove the interceptor.
   */
  addResponseInterceptor(interceptor: ResponseInterceptor): () => void
  /**
   * Adds an error interceptor.
   * Returns a function to remove the interceptor.
   */
  addErrorInterceptor(interceptor: ErrorInterceptor): () => void
  /**
   * Sets the authorization token.
   */
  setAuthToken(token: string | null): void
  /**
   * Returns the current authorization token, or `null` if not set.
   */
  getAuthToken(): string | null
  /**
   * Registers a handler for authentication errors (401).
   *
   * @returns An unsubscribe function.
   */
  onAuthError(handler: () => void): () => void
}

I18nProvider

i18n provider interface.

All i18n providers must implement this interface.

interface I18nProvider {
  /**
   * Gets the current locale.
   */
  getLocale(): string
  /**
   * Sets the current locale.
   *
   * **Fleet contract:** every conformant provider (the core simple provider,
   * `@molecule/api-i18n-simple`, `@molecule/app-i18n-i18next`, and
   * `@molecule/app-i18n-react-i18next`) MUST throw `Error('Locale "<code>"
   * not found')` when `locale` is not registered — via the constructor's
   * `initialLocales`/`locales` config, `addLocale()`, or `addTranslations()`
   * (all three register a locale). It must NOT silently degrade to
   * fallback-locale text while `getLocale()` reports the unregistered code —
   * that divergence makes a misconfigured locale switch indistinguishable
   * from a working one until a user notices the wrong language on screen.
   */
  setLocale(locale: string): Promise<void>
  /**
   * Gets all available locales.
   */
  getLocales(): LocaleConfig[]
  /**
   * Adds a locale.
   */
  addLocale(config: LocaleConfig): void
  /**
   * Removes a locale by code, notifying subscribers so language pickers
   * built on `onLocaleChange` re-render their list. If the removed locale
   * is currently active, the caller is responsible for switching to a
   * fallback (e.g. `'en'`) BEFORE calling this — the provider will not
   * auto-fall-back on its own.
   *
   * Returns `true` if the locale was registered and removed, `false`
   * otherwise.
   */
  removeLocale(code: string): boolean
  /**
   * Adds translations to a locale. Auto-creates the locale if it doesn't exist.
   *
   * **Fleet contract:** merges are DEEP, not a shallow spread — registering
   * two calls (e.g. two modules) that share a top-level namespace key merges
   * their subtrees instead of the second call clobbering the first's nested
   * translations wholesale. `@molecule/api-i18n-simple` implements the same
   * contract on the API side.
   */
  addTranslations(locale: string, translations: Translations, namespace?: string): void
  /**
   * Translates a key with optional interpolation values and pluralization.
   *
   * **Fleet plural contract (matches i18next's own key resolution order):**
   * when `options.count` is provided, the plural-suffixed key
   * (`` `${key}_${pluralForm}` ``, e.g. `item_one`/`item_few`/…, falling back
   * to `` `${key}_other` ``) is looked up FIRST and wins over the base `key`
   * if BOTH are registered. Only when no plural-suffixed key exists at all
   * does resolution fall back to the base key. A catalog that ships both
   * `item` and `item_one`/`item_other` therefore pluralizes identically
   * whichever provider is bonded.
   *
   * @returns The translated string, or the default value / key if not found.
   */
  t(
    key: string,
    values?: InterpolationValues,
    options?: {
      defaultValue?: string
      count?: number
    },
  ): string
  /**
   * Checks if a translation key exists.
   *
   * **Fleet contract:** follows the SAME locale-resolution chain as `t()` —
   * the active locale, then the English fallback — so `exists(key) === true`
   * whenever `t(key)` would render real translated text (not the raw key or
   * an inline `defaultValue`). Do not narrow this to "only the active
   * locale's own catalog"; that made `exists()` return `false` for keys `t()`
   * happily rendered via the English fallback, and the answer differed by
   * provider.
   *
   * @returns `true` if the key has a translation.
   */
  exists(key: string): boolean
  /**
   * Formats a number according to the current locale.
   *
   * @returns The locale-formatted number string.
   */
  formatNumber(value: number, options?: NumberFormatOptions): string
  /**
   * Formats a date according to the current locale.
   *
   * @returns The locale-formatted date string.
   */
  formatDate(value: Date | number | string, options?: DateFormatOptions): string
  /**
   * Formats a relative time (e.g. "2 hours ago").
   *
   * @returns The locale-formatted relative time string.
   */
  formatRelativeTime(
    value: Date | number,
    options?: {
      unit?: Intl.RelativeTimeFormatUnit
    },
  ): string
  /**
   * Formats a list (e.g. "A, B, and C").
   *
   * @returns The locale-formatted list string.
   */
  formatList(
    values: string[],
    options?: {
      type?: 'conjunction' | 'disjunction' | 'unit'
    },
  ): string
  /**
   * Subscribes to locale changes.
   *
   * @returns An unsubscribe function.
   */
  onLocaleChange(listener: (locale: string) => void): () => void
  /**
   * Gets the text direction for the current locale.
   *
   * @returns `'ltr'` or `'rtl'`.
   */
  getDirection(): 'ltr' | 'rtl'
  /**
   * Checks if a translation key exists (alias for exists).
   */
  hasKey?(key: string): boolean
  /**
   * Checks if the provider is ready.
   */
  isReady?(): boolean
  /**
   * Registers a callback for when the provider is ready.
   */
  onReady?(callback: () => void): () => void
  /**
   * Registers a lazily-loaded content module for automatic reload on locale changes.
   * All registered content is reloaded during `setLocale()` before listeners fire,
   * ensuring content is available on the first re-render with no flash.
   *
   * Idempotent: registering the same module name twice is a no-op.
   */
  registerContent?(module: string, loader: (locale: string) => Promise<void>): void
}

Logger

Logger instance with leveled logging methods, child logger creation, and transport management.

interface Logger {
  /**
   * Logs a trace message.
   */
  trace(message: string, ...args: unknown[]): void
  /**
   * Logs a debug message.
   */
  debug(message: string, ...args: unknown[]): void
  /**
   * Logs an info message.
   */
  info(message: string, ...args: unknown[]): void
  /**
   * Logs a warning message.
   */
  warn(message: string, ...args: unknown[]): void
  /**
   * Logs an error message.
   */
  error(message: string | Error, ...args: unknown[]): void
  /**
   * Sets the log level.
   */
  setLevel(level: LogLevel): void
  /**
   * Gets the current log level.
   */
  getLevel(): LogLevel
  /**
   * Creates a child logger with a namespace.
   */
  child(name: string, context?: Record<string, unknown>): Logger
  /**
   * Adds additional context to the logger.
   */
  withContext(context: Record<string, unknown>): Logger
  /**
   * Adds a transport.
   */
  addTransport(transport: LogTransport): () => void
  /**
   * Removes a transport.
   */
  removeTransport(transport: LogTransport): void
}

LoggerProvider

Logger provider interface that all logger bond packages must implement. Creates and manages logger instances and global log configuration.

interface LoggerProvider {
  /**
   * Gets a logger by name, or the root logger if no name given.
   */
  getLogger(name?: string): Logger
  /**
   * Creates a named logger.
   */
  createLogger(nameOrConfig: string | LoggerConfig, config?: LoggerConfig): Logger
  /**
   * Sets the global log level.
   */
  setLevel(level: LogLevel): void
  /**
   * Gets the global log level.
   */
  getLevel(): LogLevel
  /**
   * Adds a global transport.
   */
  addTransport(transport: LogTransport): () => void
  /**
   * Enables logging.
   */
  enable(): void
  /**
   * Disables logging.
   */
  disable(): void
  /**
   * Checks if logging is enabled.
   *
   * @returns `true` if logging is currently enabled.
   */
  isEnabled(): boolean
}

RouteLocation

Current URL decomposed into pathname, search string, hash, navigation state, and unique key.

interface RouteLocation {
  /**
   * Current pathname.
   */
  pathname: string
  /**
   * Query string (including leading ?).
   */
  search: string
  /**
   * Hash (including leading #).
   */
  hash: string
  /**
   * State data passed with navigation.
   */
  state?: unknown
  /**
   * Unique key for this location.
   */
  key?: string
}

Router

Client-side router providing navigation, guards, route matching, and history control.

All routing providers must implement this interface.

interface Router {
  /**
   * Returns the current route location (pathname, search, hash, state).
   */
  getLocation(): RouteLocation
  /**
   * Gets the current route params.
   */
  getParams<T extends RouteParams = RouteParams>(): T
  /**
   * Gets the current query params.
   */
  getQuery(): QueryParams
  /**
   * Gets a specific query parameter.
   */
  getQueryParam(key: string): string | undefined
  /**
   * Gets the current hash.
   */
  getHash(): string
  /**
   * Navigates to a path.
   */
  navigate(path: string, options?: NavigateOptions): void
  /**
   * Navigates to a named route.
   */
  navigateTo(
    name: string,
    params?: RouteParams,
    query?: QueryParams,
    options?: NavigateOptions,
  ): void
  /**
   * Goes back in history.
   */
  back(): void
  /**
   * Goes forward in history.
   */
  forward(): void
  /**
   * Goes to a specific point in history.
   */
  go(delta: number): void
  /**
   * Updates the current query params.
   */
  setQuery(params: QueryParams, options?: NavigateOptions): void
  /**
   * Updates a specific query parameter.
   */
  setQueryParam(key: string, value: string | undefined, options?: NavigateOptions): void
  /**
   * Updates the current hash.
   */
  setHash(hash: string, options?: NavigateOptions): void
  /**
   * Checks if a path matches the current location.
   *
   * @returns `true` if the path matches the current route.
   */
  isActive(path: string, exact?: boolean): boolean
  /**
   * Matches a path pattern against a pathname.
   */
  matchPath<Params extends RouteParams = RouteParams>(
    pattern: string,
    pathname: string,
  ): RouteMatch<Params> | null
  /**
   * Generates a URL from a named route.
   */
  generatePath(name: string, params?: RouteParams, query?: QueryParams): string
  /**
   * Subscribes to route changes.
   */
  subscribe(listener: RouteChangeListener): () => void
  /**
   * Adds a navigation guard.
   */
  addGuard(guard: NavigationGuard): () => void
  /**
   * Registers route definitions.
   */
  registerRoutes(routes: RouteDefinition[]): void
  /**
   * Gets all registered routes.
   */
  getRoutes(): RouteDefinition[]
  /**
   * Destroys the router.
   */
  destroy(): void
}

StoreConfig

Configuration for creating a store (initial state, optional name, and middleware chain).

interface StoreConfig<T> {
  /**
   * Initial state value.
   */
  initialState: T
  /**
   * Optional name for debugging.
   */
  name?: string
  /**
   * Optional middleware functions.
   */
  middleware?: StoreMiddleware<T>[]
}

Store

Reactive state container with getState, setState, subscribe, and destroy.

All state management providers must implement this interface.

interface Store<T> {
  /**
   * Gets the current state.
   */
  getState(): T
  /**
   * Sets the state (partial or via updater function).
   */
  setState(partial: Partial<T> | ((state: T) => Partial<T>)): void
  /**
   * Subscribes to state changes.
   * Returns an unsubscribe function.
   */
  subscribe(listener: StateListener<T>): () => void
  /**
   * Destroys the store and cleans up subscriptions.
   */
  destroy(): void
}

StateProvider

State provider interface that all state management bond packages must implement. Provides the store creation factory.

interface StateProvider {
  /**
   * Creates a new store.
   */
  createStore<T>(config: StoreConfig<T>): Store<T>
}

StorageProvider

Storage provider interface.

All storage providers must implement this interface.

interface StorageProvider {
  /**
   * Gets a value from storage.
   */
  get<T = unknown>(key: string): Promise<T | null>
  /**
   * Sets a value in storage.
   */
  set<T = unknown>(key: string, value: T): Promise<void>
  /**
   * Removes a value from storage.
   */
  remove(key: string): Promise<void>
  /**
   * Clears all values from storage.
   */
  clear(): Promise<void>
  /**
   * Gets all keys in storage.
   */
  keys(): Promise<string[]>
  /**
   * Gets multiple values from storage.
   */
  getMany?<T = unknown>(keys: string[]): Promise<Map<string, T | null>>
  /**
   * Sets multiple values in storage.
   */
  setMany?<T = unknown>(entries: Array<[string, T]>): Promise<void>
  /**
   * Removes multiple values from storage.
   */
  removeMany?(keys: string[]): Promise<void>
}

ThemeProvider

Manages theme state including the active theme, mode toggling, and change subscriptions.

interface ThemeProvider {
  /**
   * Returns the currently active theme.
   */
  getTheme(): Theme
  /**
   * Sets the active theme by reference or by name.
   *
   * @param theme - A `Theme` object or a theme name string to activate.
   */
  setTheme(theme: Theme | string): void
  /**
   * Toggles between light and dark mode for the active theme.
   */
  toggleMode(): void
  /**
   * Subscribes to theme changes. The callback fires whenever
   * `setTheme()` or `toggleMode()` is called.
   *
   * @param callback - Invoked with the new theme after each change.
   * @returns An unsubscribe function.
   */
  subscribe(callback: (theme: Theme) => void): () => void
  /**
   * Returns all registered themes. Optional — not all providers
   * support multiple themes.
   */
  getThemes?(): Theme[]
  /**
   * Returns the theme a render WITHOUT a browser produces — a server or
   * build-time render, which has no storage and no `prefers-color-scheme`
   * to read. A hydrating client renders this theme first, so its markup
   * matches what the server sent, and switches to `getTheme()` right
   * after. Optional — a provider whose theme cannot differ between the two
   * need not implement it.
   */
  getServerTheme?(): Theme
}

Theme

Complete theme definition.

interface Theme {
  name: string
  mode: 'light' | 'dark'
  colors: ThemeColors
  breakpoints: ThemeBreakpoints
  spacing: ThemeSpacing
  typography: ThemeTypography
  borderRadius: ThemeBorderRadius
  shadows: ThemeShadows
  transitions: ThemeTransitions
  zIndex: ThemeZIndex
}

AsyncStateManager

Async state manager.

interface AsyncStateManager<T> {
  state$: Observable<T>
  getState: () => T
  setState: (value: T | ((prev: T) => T) | Promise<T | ((prev: T) => T)>) => void
  extendState: (
    partial:
      Partial<T> | ((prev: T) => Partial<T>) | Promise<Partial<T> | ((prev: T) => Partial<T>)>,
  ) => void
  destroy: () => void
}

CapacitorAppManager

Capacitor app manager interface.

interface CapacitorAppManager {
  state$: Observable<CapacitorAppState>
  ready$: Observable<boolean>
  initialize: () => Promise<void>
  destroy: () => void
}

ChangePasswordStateManager

Change password state manager.

interface ChangePasswordStateManager {
  state$: Observable<PromiseState<void>>
  getState: () => PromiseState<void>
  changePassword: (oldPassword: string, newPassword: string) => Promise<void>
  reset: () => void
  destroy: () => void
}

DeviceService

Device service interface.

interface DeviceService {
  deviceInfo: DeviceInfo
  screenInfo: ScreenInfo
  hardwareInfo: HardwareInfo
  featureSupport: FeatureSupport
  supports: (feature: keyof FeatureSupport) => boolean
  isOnline: () => boolean
  isStandalone: () => boolean
  language: string
  languages: string[]
}

HttpState

State for HTTP requests.

interface HttpState<T> {
  data: T | null
  loading: boolean
  error: Error | null
}

LoginStateManager

Login state manager.

interface LoginStateManager<T = unknown> {
  state$: Observable<PromiseState<AuthResult<T>>>
  getState: () => PromiseState<AuthResult<T>>
  login: (credentials: LoginCredentials) => Promise<AuthResult<T>>
  reset: () => void
  destroy: () => void
}

OAuthOptions

OAuth configuration options.

interface OAuthOptions {
  /** Base URL for the API server (e.g. `https://api.example.com`). */
  baseURL?: string
  /** List of supported OAuth provider names (e.g. `['github', 'google']`). */
  oauthProviders?: string[]
  /** Path prefix for OAuth initiation routes. Defaults to `/oauth`. */
  oauthEndpoint?: string
  /** Path for the OAuth login POST endpoint. Defaults to `/users/log-in/oauth`. */
  loginEndpoint?: string
  /** Called after a successful OAuth login (session established). */
  onSuccess?: () => void
  /**
   * Called with a failure message when the OAuth login fails. Failures are
   * also emitted on `OAuthStateManager.error$`.
   */
  onError?: (error: string) => void
  /**
   * Auth client used to establish the session after the code exchange. This
   * is THE way to wire session establishment in Angular: inject the
   * `AUTH_CLIENT` token and pass the client here. When omitted, the helper
   * falls back to the server-established httpOnly-cookie session (see
   * `OAuthStateManager.handleCallback`).
   */
  authClient?: AuthClient<unknown>
}

OAuthStateManager

OAuth state manager.

interface OAuthStateManager {
  /** Observable of the configured OAuth provider names. */
  providers$: Observable<string[]>
  /**
   * Observable of OAuth failure messages (from the callback code exchange).
   * Completed by `OAuthStateManager.destroy`.
   */
  error$: Observable<string>
  /** Returns the configured OAuth provider names. */
  getProviders: () => string[]
  /** Builds the OAuth initiation URL for a provider. */
  getOAuthUrl: (provider: string) => string
  /** Starts the full-page redirect flow for a provider. */
  redirect: (provider: string) => void
  /**
   * Handles the OAuth callback: exchanges the `code` URL parameter for a
   * session. Invoked automatically at creation (see `createOAuthState`);
   * exposed for callers that need to re-run it manually. No-ops unless
   * running in a browser with a `code` URL parameter and a stashed provider.
   *
   * When an auth client was provided, the session is established locally
   * (`setAccessToken` + `setUser` + `initialize`). When no client is
   * available, the server has already established the httpOnly-cookie
   * session during the exchange, so a user-carrying response still counts
   * as success.
   */
  handleCallback: () => Promise<void>
  /** Completes the `providers$` and `error$` observables. */
  destroy: () => void
}

PasswordResetStateManager

Password reset state manager.

interface PasswordResetStateManager {
  requestState$: Observable<PromiseState<void>>
  confirmState$: Observable<PromiseState<void>>
  getRequestState: () => PromiseState<void>
  getConfirmState: () => PromiseState<void>
  requestReset: (data: PasswordResetRequest) => Promise<void>
  confirmReset: (data: PasswordResetConfirm) => Promise<void>
  reset: () => void
  destroy: () => void
}

PlatformService

Platform service interface.

interface PlatformService {
  platform: Platform
  isNative: boolean
  isMobile: boolean
  isDesktop: boolean
  isWeb: boolean
  isDevelopment: boolean
  isProduction: boolean
  isPlatform: (...platforms: Platform[]) => boolean
}

PromiseStateManager

Promise state manager.

interface PromiseStateManager<T> {
  state$: Observable<PromiseState<T>>
  getState: () => PromiseState<T>
  call: (...args: any[]) => Promise<T>
  cancel: (message?: string) => void
  reset: () => void
  destroy: () => void
}

Returns: The result.

PushService

Push service interface.

interface PushService {
  permission$: Observable<PermissionStatus | null>
  token$: Observable<PushToken | null>
  getPermission: () => PermissionStatus | null
  getToken: () => PushToken | null
  checkPermission: () => Promise<PermissionStatus>
  requestPermission: () => Promise<PermissionStatus>
  register: (options?: PushRegisterOptions) => Promise<PushToken>
  unregister: () => Promise<void>
  onNotificationReceived: (listener: NotificationReceivedListener) => () => void
  onNotificationAction: (listener: NotificationActionListener) => () => void
  onTokenChange: (listener: TokenChangeListener) => () => void
  setBadge: (count: number) => Promise<void>
  clearBadge: () => Promise<void>
  destroy: () => void
}

SignupStateManager

Signup state manager.

interface SignupStateManager<T = unknown> {
  state$: Observable<PromiseState<AuthResult<T>>>
  getState: () => PromiseState<AuthResult<T>>
  signup: (data: RegisterData) => Promise<AuthResult<T>>
  reset: () => void
  destroy: () => void
}

StorageValueState

State for a storage value.

interface StorageValueState<T> {
  value: T | undefined
  loading: boolean
  error: Error | null
}

VersionService

Version service interface.

interface VersionService {
  state$: Observable<VersionState>
  isUpdateAvailable$: Observable<boolean>
  isChecking$: Observable<boolean>
  isServiceWorkerWaiting$: Observable<boolean>
  newVersion$: Observable<string | undefined>
  getState: () => VersionState
  checkForUpdates: () => Promise<boolean>
  applyUpdate: (options?: { force?: boolean }) => void
  dismissUpdate: () => void
  startPeriodicChecks: (options?: UpdateCheckOptions) => void
  stopPeriodicChecks: () => void
  destroy: () => void
}

MoleculeModuleConfig

Configuration for molecule Angular module.

interface MoleculeModuleConfig {
  state?: StateProvider
  auth?: AuthClient<unknown>
  theme?: ThemeProvider
  router?: Router
  i18n?: I18nProvider
  http?: HttpClient
  storage?: StorageProvider
  logger?: LoggerProvider
}

MoleculeTokens

Injection tokens for molecule services.

interface MoleculeTokens {
  STATE_PROVIDER: symbol
  AUTH_CLIENT: symbol
  THEME_PROVIDER: symbol
  ROUTER: symbol
  I18N_PROVIDER: symbol
  HTTP_CLIENT: symbol
  STORAGE_PROVIDER: symbol
  LOGGER_PROVIDER: symbol
}

Types

RouteParams

URL path parameter key-value map extracted from dynamic route segments (e.g. { id: '123' }).

type RouteParams = Record<string, string>

QueryParams

URL query string parameter map (single values or arrays for repeated keys).

type QueryParams = Record<string, string | string[] | undefined>

Classes

MoleculeAuthService

Angular service for authentication.

Wraps molecule auth client and exposes state as RxJS observables.

@Injectable({
  providedIn: 'root',
})
export class MoleculeAuthService<T = unknown> implements OnDestroy {
  private stateSubject: BehaviorSubject<AuthState<T>>
  private unsubscribe: (() => void) | null = null

  /**
   * Observable of the full auth state.
   */
  state$: Observable<AuthState<T>>

  /**
   * Observable of the authenticated user.
   */
  user$: Observable<T | null>

  /**
   * Observable of authentication status.
   */
  isAuthenticated$: Observable<boolean>

  /**
   * Observable of loading status.
   */
  isLoading$: Observable<boolean>

  constructor(@Inject(AUTH_CLIENT) private client: AuthClient<T>) {
    this.stateSubject = new BehaviorSubject<AuthState<T>>(client.getState())

    this.unsubscribe = client.onAuthChange(() => {
      this.stateSubject.next(client.getState())
    })

    this.state$ = this.stateSubject.asObservable()

    this.user$ = this.state$.pipe(
      map((state) => state.user),
      distinctUntilChanged(),
    )

    this.isAuthenticated$ = this.state$.pipe(
      map((state) => state.authenticated),
      distinctUntilChanged(),
    )

    this.isLoading$ = this.state$.pipe(
      map((state) => state.loading),
      distinctUntilChanged(),
    )
  }

  /**
   * Get the current auth state snapshot.
   * @returns The current authentication state including user, loading, and authenticated flags.
   */
  get state(): AuthState<T> {
    return this.client.getState()
  }

  /**
   * Get the current user snapshot.
   * @returns The authenticated user object, or `null` if not authenticated.
   */
  get user(): T | null {
    return this.state.user
  }

  /**
   * Check if user is currently authenticated.
   * @returns `true` if the user is authenticated, `false` otherwise.
   */
  get isAuthenticated(): boolean {
    return this.state.authenticated
  }

  /**
   * Login with credentials.
   *
   * @param credentials - The email/password or provider-specific login credentials.
   * @returns A promise that resolves to the authentication result with user data and tokens.
   */
  login(credentials: LoginCredentials): Promise<AuthResult<T>> {
    return this.client.login(credentials)
  }

  /**
   * Logout the current user.
   * @returns A promise that resolves when the user has been logged out.
   */
  logout(): Promise<void> {
    return this.client.logout()
  }

  /**
   * Register a new user.
   *
   * @param data - The registration payload including email, password, and profile fields.
   * @returns A promise that resolves to the authentication result for the newly registered user.
   */
  register(data: RegisterData): Promise<AuthResult<T>> {
    return this.client.register(data)
  }

  /**
   * Refresh the authentication token.
   * @returns A promise that resolves to the authentication result with refreshed tokens.
   */
  refresh(): Promise<AuthResult<T>> {
    return this.client.refresh()
  }

  /**
   * Clean up auth state subscription on service destruction.
   */
  ngOnDestroy(): void {
    this.unsubscribe?.()
    this.stateSubject.complete()
  }
}

MoleculeFormInstance

Wrapper around a FormController that provides RxJS observables and synchronous accessors for use in Angular components.

class MoleculeFormInstance<T extends Record<string, unknown>> {
  private stateSubject: BehaviorSubject<FormState<T>>
  private unsubscribe: (() => void) | null = null

  /**
   * Observable of the full form state.
   */
  state$: Observable<FormState<T>>

  /**
   * Observable of form validity.
   */
  isValid$: Observable<boolean>

  /**
   * Observable of form dirty state.
   */
  isDirty$: Observable<boolean>

  /**
   * Observable of form submitting state.
   */
  isSubmitting$: Observable<boolean>

  /**
   * Observable of field errors.
   */
  errors$: Observable<Partial<Record<keyof T, string>>>

  constructor(private controller: FormController<T>) {
    this.stateSubject = new BehaviorSubject<FormState<T>>(controller.getState())

    this.unsubscribe = controller.subscribe((state: FormState<T>) => {
      this.stateSubject.next(state)
    })

    this.state$ = this.stateSubject.asObservable()

    this.isValid$ = this.state$.pipe(
      map((state) => state.isValid),
      distinctUntilChanged(),
    )

    this.isDirty$ = this.state$.pipe(
      map((state) => state.isDirty),
      distinctUntilChanged(),
    )

    this.isSubmitting$ = this.state$.pipe(
      map((state) => state.isSubmitting),
      distinctUntilChanged(),
    )

    this.errors$ = this.state$.pipe(
      map((state) => state.errors),
      distinctUntilChanged(),
    )
  }

  /**
   * Get the current form state snapshot.
   * @returns The current form state including values, errors, validity, and submission status.
   */
  get state(): FormState<T> {
    return this.controller.getState()
  }

  /**
   * Get the current form validity snapshot.
   * @returns `true` if all fields pass validation, `false` otherwise.
   */
  get isValid(): boolean {
    return this.controller.getState().isValid
  }

  /**
   * Get the current form dirty state snapshot.
   * @returns `true` if any field has been modified from its default value, `false` otherwise.
   */
  get isDirty(): boolean {
    return this.controller.getState().isDirty
  }

  /**
   * Get the current form submitting state snapshot.
   * @returns `true` if the form is currently being submitted, `false` otherwise.
   */
  get isSubmitting(): boolean {
    return this.controller.getState().isSubmitting
  }

  /**
   * Register a field for form management.
   *
   * @param name - The field name corresponding to a key in the form values type.
   * @param options - Registration options including validation rules and default value.
   * @returns The field registration with value, onChange, onBlur, and ref bindings.
   */
  register(name: keyof T, options?: Omit<RegisterOptions, 'name'>): FieldRegistration {
    return this.controller.register({ ...options, name: name as string } as RegisterOptions)
  }

  /**
   * Get the value of a specific field.
   *
   * @param name - The field name corresponding to a key in the form values type.
   * @returns The current value of the specified field.
   */
  getValue<K extends keyof T>(name: K): T[K] {
    return this.controller.getValue(name)
  }

  /**
   * Set the value of a specific field.
   *
   * @param name - The field name corresponding to a key in the form values type.
   * @param value - The new value to set for the field.
   */
  setValue<K extends keyof T>(name: K, value: T[K]): void {
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    this.controller.setValue(name as any, value as any)
  }

  /**
   * Get the error for a specific field.
   *
   * @param name - The field name to retrieve the error for.
   * @returns The error message for the field, or `undefined` if the field has no error.
   */
  getError(name: keyof T): string | undefined {
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    return this.controller.getError(name as any)
  }

  /**
   * Set the error for a specific field.
   *
   * @param name - The field name to set or clear the error for.
   * @param error - The error message to set, or `undefined` to clear the error.
   */
  setError(name: keyof T, error: string | undefined): void {
    if (error !== undefined) {
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      this.controller.setError(name as any, error)
    } else {
      this.controller.clearError(name)
    }
  }

  /**
   * Clear all field errors.
   */
  clearErrors(): void {
    this.controller.clearErrors()
  }

  /**
   * Create a submit handler function.
   *
   * @param onSubmit - Callback invoked with validated form values on successful submission.
   * @param onError - Optional callback invoked with field errors when validation fails.
   * @returns A submit handler function that can be called from form submit events.
   */
  handleSubmit(
    onSubmit: (values: T) => void | Promise<void>,
    onError?: (errors: Partial<Record<keyof T, string>>) => void,
  ): (event?: { preventDefault?: () => void }) => Promise<void> {
    return this.controller.handleSubmit(onSubmit, onError)
  }

  /**
   * Reset the form to initial values.
   *
   * @param values - Optional partial values to use as the new initial state.
   */
  reset(values?: Partial<T>): void {
    this.controller.reset(values)
  }

  /**
   * Validate all fields.
   *
   * @returns A promise that resolves to `true` if all fields are valid, `false` otherwise.
   */
  validate(): Promise<boolean> {
    return this.controller.validate()
  }

  /**
   * Destroy the form instance and clean up subscriptions.
   */
  destroy(): void {
    this.unsubscribe?.()
    this.unsubscribe = null
    this.stateSubject.complete()
    this.controller.destroy()
  }
}

MoleculeFormsService

Angular service for form handling.

Wraps molecule form providers and creates form instances that expose state as RxJS observables.

@Injectable({
  providedIn: 'root',
})
export class MoleculeFormsService implements OnDestroy {
  private instances: MoleculeFormInstance<Record<string, unknown>>[] = []

  /**
   * Create a form instance from a provider.
   *
   * @param provider - The form provider that creates form controllers.
   * @param options - Form configuration including default values and validation mode.
   * @returns A new form instance with RxJS observables for reactive state tracking.
   */
  createForm<T extends Record<string, unknown>>(
    provider: FormProvider,
    options: FormOptions<T>,
  ): MoleculeFormInstance<T> {
    const controller = provider.createForm(options)
    const instance = new MoleculeFormInstance<T>(controller)
    this.instances.push(instance as MoleculeFormInstance<Record<string, unknown>>)
    return instance
  }

  /**
   * Destroy all managed form instances on service destruction.
   */
  ngOnDestroy(): void {
    for (const instance of this.instances) {
      instance.destroy()
    }
    this.instances = []
  }
}

MoleculeHttpService

Angular service for HTTP requests.

Wraps molecule HTTP client and returns RxJS observables.

@Injectable({
  providedIn: 'root',
})
/**
 * Molecule Http Service class.
 */
export class MoleculeHttpService {
  constructor(@Inject(HTTP_CLIENT) private client: HttpClient) {}

  /**
   * Make a GET request.
   *
   * @param url - Request URL
   * @param config - Request configuration
   * @returns Observable of response data
   */
  get<T>(url: string, config?: RequestConfig): Observable<T> {
    return from(this.client.get<T>(url, config)).pipe(map((res) => res.data))
  }

  /**
   * Make a POST request.
   *
   * @param url - Request URL
   * @param data - Request body
   * @param config - Request configuration
   * @returns Observable of response data
   */
  post<T>(url: string, data?: unknown, config?: RequestConfig): Observable<T> {
    return from(this.client.post<T>(url, data, config)).pipe(map((res) => res.data))
  }

  /**
   * Make a PUT request.
   *
   * @param url - Request URL
   * @param data - Request body
   * @param config - Request configuration
   * @returns Observable of response data
   */
  put<T>(url: string, data?: unknown, config?: RequestConfig): Observable<T> {
    return from(this.client.put<T>(url, data, config)).pipe(map((res) => res.data))
  }

  /**
   * Make a PATCH request.
   *
   * @param url - Request URL
   * @param data - Request body
   * @param config - Request configuration
   * @returns Observable of response data
   */
  patch<T>(url: string, data?: unknown, config?: RequestConfig): Observable<T> {
    return from(this.client.patch<T>(url, data, config)).pipe(map((res) => res.data))
  }

  /**
   * Make a DELETE request.
   *
   * @param url - Request URL
   * @param config - Request configuration
   * @returns Observable of response data
   */
  delete<T>(url: string, config?: RequestConfig): Observable<T> {
    return from(this.client.delete<T>(url, config)).pipe(map((res) => res.data))
  }

  /**
   * Get the full response (including headers, status, etc.).
   *
   * @param method - HTTP method
   * @param url - Request URL
   * @param config - Request configuration
   * @returns Observable of full response
   */
  request<T>(
    method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE',
    url: string,
    config?: RequestConfig,
  ): Observable<HttpResponse<T>> {
    const promise = (async () => {
      switch (method) {
        case 'GET':
          return this.client.get<T>(url, config)
        case 'POST':
          return this.client.post<T>(
            url,
            (config as RequestConfig & { data?: unknown })?.data,
            config,
          )
        case 'PUT':
          return this.client.put<T>(
            url,
            (config as RequestConfig & { data?: unknown })?.data,
            config,
          )
        case 'PATCH':
          return this.client.patch<T>(
            url,
            (config as RequestConfig & { data?: unknown })?.data,
            config,
          )
        case 'DELETE':
          return this.client.delete<T>(url, config)
      }
    })()

    return from(promise)
  }

  /**
   * Create a state subject for managing request state.
   *
   * @returns The created instance.
   */
  createState<T>(): BehaviorSubject<HttpState<T>> {
    return new BehaviorSubject<HttpState<T>>({
      data: null,
      loading: false,
      error: null,
    })
  }

  /**
   * Make a GET request with state management.
   *
   * @param url - Request URL
   * @param state - State subject to update
   * @param config - Request configuration
   */
  getWithState<T>(url: string, state: BehaviorSubject<HttpState<T>>, config?: RequestConfig): void {
    state.next({ ...state.value, loading: true, error: null })

    this.get<T>(url, config)
      .pipe(
        tap((data) => state.next({ data, loading: false, error: null })),
        catchError((error) => {
          state.next({ data: null, loading: false, error })
          throw error
        }),
      )
      .subscribe()
  }
}

MoleculeI18nService

Angular service for internationalization.

Wraps molecule i18n provider and exposes state as RxJS observables.

@Injectable({
  providedIn: 'root',
})
export class MoleculeI18nService implements OnDestroy {
  private localeSubject: BehaviorSubject<string>
  private directionSubject: BehaviorSubject<'ltr' | 'rtl'>
  private localesSubject: BehaviorSubject<LocaleConfig[]>
  private unsubscribe: (() => void) | null = null

  /**
   * Observable of the current locale code.
   */
  locale$: Observable<string>

  /**
   * Observable of the text direction.
   */
  direction$: Observable<'ltr' | 'rtl'>

  /**
   * Observable of available locales.
   */
  locales$: Observable<LocaleConfig[]>

  constructor(@Inject(I18N_PROVIDER) private provider: I18nProvider) {
    this.localeSubject = new BehaviorSubject<string>(provider.getLocale())
    this.directionSubject = new BehaviorSubject<'ltr' | 'rtl'>(provider.getDirection())
    this.localesSubject = new BehaviorSubject<LocaleConfig[]>(provider.getLocales())

    this.unsubscribe = provider.onLocaleChange(() => {
      this.localeSubject.next(provider.getLocale())
      this.directionSubject.next(provider.getDirection())
      this.localesSubject.next(provider.getLocales())
    })

    this.locale$ = this.localeSubject.asObservable()
    this.direction$ = this.directionSubject.asObservable()
    this.locales$ = this.localesSubject.asObservable()
  }

  /**
   * Get the current locale code snapshot.
   * @returns The active locale code (e.g. `'en'`, `'fr'`).
   */
  get locale(): string {
    return this.provider.getLocale()
  }

  /**
   * Get the current text direction snapshot.
   * @returns The text direction for the active locale, either `'ltr'` or `'rtl'`.
   */
  get direction(): 'ltr' | 'rtl' {
    return this.provider.getDirection()
  }

  /**
   * Translate a key.
   *
   * @param key - The dot-notation translation key to look up.
   * @param values - Optional interpolation values to substitute into the translated string.
   * @returns The translated and interpolated string for the current locale.
   */
  t(key: string, values?: InterpolationValues): string {
    return this.provider.t(key, values)
  }

  /**
   * Set the current locale.
   *
   * @param code - The locale code to switch to (e.g. `'en'`, `'fr'`).
   */
  setLocale(code: string): void {
    this.provider.setLocale(code)
  }

  /**
   * Format a number.
   *
   * @param value - The number to format.
   * @param options - Locale-aware formatting options such as style, currency, and precision.
   * @returns The locale-formatted number string.
   */
  formatNumber(value: number, options?: NumberFormatOptions): string {
    return this.provider.formatNumber(value, options)
  }

  /**
   * Format a date.
   *
   * @param value - The date to format, as a Date object, timestamp, or ISO string.
   * @param options - Locale-aware formatting options such as date style and time zone.
   * @returns The locale-formatted date string.
   */
  formatDate(value: Date | number | string, options?: DateFormatOptions): string {
    return this.provider.formatDate(value, options)
  }

  /**
   * Clean up locale change subscription and complete all subjects on service destruction.
   */
  ngOnDestroy(): void {
    this.unsubscribe?.()
    this.localeSubject.complete()
    this.directionSubject.complete()
    this.localesSubject.complete()
  }
}

MoleculeLoggerService

Angular service for logging.

Wraps molecule logger provider.

@Injectable({
  providedIn: 'root',
})
export class MoleculeLoggerService {
  constructor(@Inject(LOGGER_PROVIDER) private provider: LoggerProvider) {}

  /**
   * Get a logger instance.
   *
   * @param name - Logger name (usually component/service name)
   * @returns The result.
   */
  getLogger(name?: string): Logger {
    return this.provider.getLogger(name)
  }

  /**
   * Create a logger with custom configuration.
   *
   * @param config - Logger configuration
   * @returns The created instance.
   */
  createLogger(config: LoggerConfig): Logger {
    return this.provider.createLogger(config)
  }

  /**
   * Set the global log level.
   *
   * @param level - Log level
   */
  setLevel(level: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'silent'): void {
    this.provider.setLevel(level)
  }

  /**
   * Get the current global log level.
   *
   * @returns The result.
   */
  getLevel(): string {
    return this.provider.getLevel()
  }

  /**
   * Enable logging.
   */
  enable(): void {
    this.provider.enable()
  }

  /**
   * Disable logging.
   */
  disable(): void {
    this.provider.disable()
  }

  /**
   * Check if logging is enabled.
   *
   * @returns Whether logging is enabled
   */
  isEnabled(): boolean {
    return this.provider.isEnabled()
  }
}

MoleculeRouterService

Angular service for routing.

Wraps molecule router and exposes state as RxJS observables.

@Injectable({
  providedIn: 'root',
})
/**
 * Molecule Router Service class.
 */
export class MoleculeRouterService implements OnDestroy {
  private locationSubject: BehaviorSubject<RouteLocation>
  private paramsSubject: BehaviorSubject<RouteParams>
  private querySubject: BehaviorSubject<QueryParams>
  private unsubscribe: (() => void) | null = null

  /**
   * Observable of the current location.
   */
  location$: Observable<RouteLocation>

  /**
   * Observable of route parameters.
   */
  params$: Observable<RouteParams>

  /**
   * Observable of query parameters.
   */
  query$: Observable<QueryParams>

  /**
   * Observable of the current pathname.
   */
  pathname$: Observable<string>

  constructor(@Inject(ROUTER) private router: Router) {
    this.locationSubject = new BehaviorSubject<RouteLocation>(router.getLocation())
    this.paramsSubject = new BehaviorSubject<RouteParams>(router.getParams())
    this.querySubject = new BehaviorSubject<QueryParams>(router.getQuery())

    this.unsubscribe = router.subscribe(() => {
      this.locationSubject.next(router.getLocation())
      this.paramsSubject.next(router.getParams())
      this.querySubject.next(router.getQuery())
    })

    this.location$ = this.locationSubject.asObservable()
    this.params$ = this.paramsSubject.asObservable()
    this.query$ = this.querySubject.asObservable()

    this.pathname$ = this.location$.pipe(
      map((loc) => loc.pathname),
      distinctUntilChanged(),
    )
  }

  /**
   * Get the current location snapshot.
   * @returns The result.
   */
  get location(): RouteLocation {
    return this.router.getLocation()
  }

  /**
   * Get the current params snapshot.
   * @returns The result.
   */
  get params(): RouteParams {
    return this.router.getParams()
  }

  /**
   * Get the current query snapshot.
   * @returns The result.
   */
  get query(): QueryParams {
    return this.router.getQuery()
  }

  /**
   * Navigate to a path.
   *
   * @param path - Target path
   * @param options - Navigation options
   */
  navigate(path: string, options?: NavigateOptions): void {
    this.router.navigate(path, options)
  }

  /**
   * Navigate to a named route.
   *
   * @param name - Route name
   * @param params - Route parameters
   * @param query - Query parameters
   * @param options - Navigation options
   */
  navigateTo(
    name: string,
    params?: RouteParams,
    query?: QueryParams,
    options?: NavigateOptions,
  ): void {
    this.router.navigateTo(name, params, query, options)
  }

  /**
   * Go back in history.
   */
  back(): void {
    this.router.back()
  }

  /**
   * Go forward in history.
   */
  forward(): void {
    this.router.forward()
  }

  /**
   * Check if a path is currently active.
   *
   * @param path - Path to check
   * @param exact - Whether to match exactly
   * @returns The result.
   */
  isActive(path: string, exact = false): boolean {
    return this.router.isActive(path, exact)
  }

  /**
   * Cleans up subscriptions and completes subjects on service destruction.
   */
  ngOnDestroy(): void {
    this.unsubscribe?.()
    this.locationSubject.complete()
    this.paramsSubject.complete()
    this.querySubject.complete()
  }
}

MoleculeStateService

Angular service for state management.

Wraps molecule state stores and exposes them as RxJS observables.

@Injectable({
  providedIn: 'root',
})
/**
 * Molecule State Service class.
 */
export class MoleculeStateService implements OnDestroy {
  private subscriptions = new Map<Store<unknown>, () => void>()
  private subjects = new Map<Store<unknown>, BehaviorSubject<unknown>>()

  constructor(@Inject(STATE_PROVIDER) private provider: StateProvider) {}

  /**
   * Get an observable of the full store state.
   *
   * @param store - The store to observe
   * @returns Observable of the store state
   */
  observe<T>(store: Store<T>): Observable<T> {
    return this.getOrCreateSubject(store).asObservable() as Observable<T>
  }

  /**
   * Select a piece of state from a store.
   *
   * @param store - The store to select from
   * @param selector - Selector function
   * @returns Observable of the selected state
   */
  select<T, S>(store: Store<T>, selector: (state: T) => S): Observable<S> {
    return this.observe(store).pipe(map(selector), distinctUntilChanged())
  }

  /**
   * Get the current snapshot of a store's state.
   *
   * @param store - The store
   * @returns The result.
   */
  snapshot<T>(store: Store<T>): T {
    return store.getState()
  }

  /**
   * Update a store's state.
   *
   * @param store - The store to update
   * @param partial - Partial state or updater function
   */
  update<T>(store: Store<T>, partial: Partial<T> | ((state: T) => Partial<T>)): void {
    store.setState(partial)
  }

  /**
   * Gets or creates a BehaviorSubject for the given store.
   * @param store - The data store to observe.
   * @returns The BehaviorSubject tracking the store state.
   */
  private getOrCreateSubject<T>(store: Store<T>): BehaviorSubject<T> {
    let subject = this.subjects.get(store as Store<unknown>) as BehaviorSubject<T> | undefined

    if (!subject) {
      subject = new BehaviorSubject<T>(store.getState())
      this.subjects.set(store as Store<unknown>, subject as BehaviorSubject<unknown>)

      const unsubscribe = store.subscribe(() => {
        subject!.next(store.getState())
      })
      this.subscriptions.set(store as Store<unknown>, unsubscribe)
    }

    return subject
  }

  /**
   * Cleans up all store subscriptions and completes subjects on service destruction.
   */
  ngOnDestroy(): void {
    // Clean up all subscriptions
    this.subscriptions.forEach((unsubscribe) => unsubscribe())
    this.subscriptions.clear()

    // Complete all subjects
    this.subjects.forEach((subject) => subject.complete())
    this.subjects.clear()
  }
}

MoleculeStorageService

Angular service for storage.

Wraps molecule storage provider and returns RxJS observables.

@Injectable({
  providedIn: 'root',
})
/**
 * Molecule Storage Service class.
 */
export class MoleculeStorageService {
  constructor(@Inject(STORAGE_PROVIDER) private provider: StorageProvider) {}

  /**
   * Get a value from storage.
   *
   * @param key - Storage key
   * @returns Observable of the value
   */
  get<T>(key: string): Observable<T | null> {
    return from(this.provider.get<T>(key))
  }

  /**
   * Set a value in storage.
   *
   * @param key - Storage key
   * @param value - Value to store
   * @returns Observable that completes when done
   */
  set<T>(key: string, value: T): Observable<void> {
    return from(this.provider.set(key, value))
  }

  /**
   * Remove a value from storage.
   *
   * @param key - Storage key
   * @returns Observable that completes when done
   */
  remove(key: string): Observable<void> {
    return from(this.provider.remove(key))
  }

  /**
   * Clear all values from storage.
   *
   * @returns Observable that completes when done
   */
  clear(): Observable<void> {
    return from(this.provider.clear())
  }

  /**
   * Get all keys in storage.
   *
   * @returns Observable of key array
   */
  keys(): Observable<string[]> {
    return from(this.provider.keys())
  }

  /**
   * Create a reactive storage value with auto-loading.
   *
   * @param key - Storage key
   * @param defaultValue - Default value if not found
   * @returns BehaviorSubject that syncs with storage
   */
  createValue<T>(key: string, defaultValue: T): BehaviorSubject<StorageValueState<T>> {
    const state = new BehaviorSubject<StorageValueState<T>>({
      value: defaultValue,
      loading: true,
      error: null,
    })

    // Load initial value
    this.get<T>(key).subscribe({
      next: (value) => {
        state.next({
          value: value ?? defaultValue,
          loading: false,
          error: null,
        })
      },
      error: (error) => {
        state.next({
          value: defaultValue,
          loading: false,
          error,
        })
      },
    })

    return state
  }

  /**
   * Update a reactive storage value.
   *
   * @param key - Storage key
   * @param value - New value
   * @param state - State subject to update
   */
  updateValue<T>(key: string, value: T, state: BehaviorSubject<StorageValueState<T>>): void {
    state.next({ ...state.value, loading: true })

    this.set(key, value).subscribe({
      next: () => {
        state.next({ value, loading: false, error: null })
      },
      error: (error) => {
        state.next({ ...state.value, loading: false, error })
      },
    })
  }
}

MoleculeThemeService

Angular service for theming.

Wraps molecule theme provider and exposes state as RxJS observables.

@Injectable({
  providedIn: 'root',
})
/**
 * Molecule Theme Service class.
 */
export class MoleculeThemeService implements OnDestroy {
  private themeSubject: BehaviorSubject<Theme>
  private themeNameSubject: BehaviorSubject<string>
  private unsubscribe: (() => void) | null = null

  /**
   * Observable of the current theme.
   */
  theme$: Observable<Theme>

  /**
   * Observable of the current theme name.
   */
  themeName$: Observable<string>

  /**
   * Observable of the theme mode (light/dark).
   */
  mode$: Observable<'light' | 'dark'>

  constructor(@Inject(THEME_PROVIDER) private provider: ThemeProvider) {
    this.themeSubject = new BehaviorSubject<Theme>(provider.getTheme())
    this.themeNameSubject = new BehaviorSubject<string>(provider.getTheme().name)

    this.unsubscribe = provider.subscribe(() => {
      this.themeSubject.next(provider.getTheme())
      this.themeNameSubject.next(provider.getTheme().name)
    })

    this.theme$ = this.themeSubject.asObservable()
    this.themeName$ = this.themeNameSubject.asObservable()

    this.mode$ = this.theme$.pipe(
      map((theme: Theme) => theme.mode),
      distinctUntilChanged(),
    )
  }

  /**
   * Get the current theme snapshot.
   * @returns The result.
   */
  get theme(): Theme {
    return this.provider.getTheme()
  }

  /**
   * Get the current theme name snapshot.
   * @returns The result.
   */
  get themeName(): string {
    return this.provider.getTheme().name
  }

  /**
   * Get available themes.
   * @returns The result.
   */
  get themes(): Theme[] {
    return this.provider.getThemes?.() ?? []
  }

  /**
   * Set the current theme.
   *
   * @param name - Theme name
   */
  setTheme(name: string): void {
    this.provider.setTheme(name)
  }

  /**
   * Toggle to the next theme.
   */
  toggleTheme(): void {
    const themes = this.themes
    const currentName = this.themeName
    const currentIndex = themes.findIndex((t: Theme) => t.name === currentName)
    const nextIndex = (currentIndex + 1) % themes.length
    this.provider.setTheme(themes[nextIndex])
  }

  /**
   * Cleans up subscriptions and completes subjects on service destruction.
   */
  ngOnDestroy(): void {
    this.unsubscribe?.()
    this.themeSubject.complete()
    this.themeNameSubject.complete()
  }
}

Functions

bumpLocaleVersion()

Bump the locale version signal, causing all template bindings that use the reactive t() to re-evaluate on the next change detection cycle.

Called automatically by the ENVIRONMENT_INITIALIZER in provideMolecule.

function bumpLocaleVersion(): void

t(key, values, options)

Translate a key using the current locale.

This is a signal-aware wrapper around @molecule/app-i18n's t(). Reading the internal locale signal establishes an Angular reactivity dependency, so template bindings that call this function will be re-evaluated when the locale changes.

function t(
  key: string,
  values?: InterpolationValues,
  options?: { defaultValue?: string; count?: number },
): string
  • key — Translation key
  • values — Interpolation values
  • options — Options (defaultValue, count).
  • options.defaultValue — Fallback string when no translation is found.
  • options.count — Pluralization count.

Returns: Translated string

provideState(provider)

Provide state management.

function provideState(provider: StateProvider): EnvironmentProviders
  • provider — State provider

Returns: Environment providers

provideAuth(client)

Registers an AuthClient as an Angular environment provider for dependency injection.

function provideAuth(client: AuthClient<T>): EnvironmentProviders
  • client — Auth client

Returns: Environment providers

provideTheme(provider)

Registers a ThemeProvider as an Angular environment provider for dependency injection.

function provideTheme(provider: ThemeProvider): EnvironmentProviders
  • provider — Theme provider

Returns: Environment providers

provideRouter(router)

Registers a Router as an Angular environment provider for dependency injection.

function provideRouter(router: Router): EnvironmentProviders
  • router — Router instance

Returns: Environment providers

provideI18n(provider)

Registers an I18nProvider as an Angular environment provider for dependency injection.

function provideI18n(provider: I18nProvider): EnvironmentProviders
  • provider — I18n provider

Returns: Environment providers

provideHttp(client)

Provide HTTP client.

function provideHttp(client: HttpClient): EnvironmentProviders
  • client — HTTP client

Returns: Environment providers

provideStorage(provider)

Registers a StorageProvider as an Angular environment provider for dependency injection.

function provideStorage(provider: StorageProvider): EnvironmentProviders
  • provider — Storage provider

Returns: Environment providers

provideLogger(provider)

Registers a LoggerProvider as an Angular environment provider for dependency injection.

function provideLogger(provider: LoggerProvider): EnvironmentProviders
  • provider — Logger provider

Returns: Environment providers

provideMolecule(config)

Provide all molecule services at once.

function provideMolecule(config: MoleculeModuleConfig): EnvironmentProviders
  • config — Configuration with all providers

Returns: Environment providers

createAsyncState(initialState)

Creates an async-capable state manager.

function createAsyncState(initialState: T): AsyncStateManager<T>
  • initialState — Initial state value

Returns: The created instance.

createCapacitorAppState(options)

Creates an Angular Capacitor app service with reactive state.

Wraps createCapacitorApp from @molecule/app-platform and exposes state changes as RxJS observables.

function createCapacitorAppState(options?: CapacitorAppOptions): CapacitorAppManager
  • options — Capacitor app configuration options

Returns: Capacitor app manager with observables and action methods

createChangePasswordState(client)

Creates a change password state manager with async state tracking.

function createChangePasswordState(client: AuthClient<UserProfile>): ChangePasswordStateManager
  • client — Auth client

Returns: Change password state manager

createDeviceService()

Creates an Angular device service with static device information.

function createDeviceService(): DeviceService

Returns: Device service with device, screen, hardware, and feature info

createLoginState(client)

Creates a login state manager with async state tracking.

function createLoginState(client: AuthClient<T>): LoginStateManager<T>
  • client — Auth client

Returns: Login state manager

createOAuthState(options)

Creates an OAuth state manager.

Automatically handles OAuth callbacks: when created in a browser, it invokes OAuthStateManager.handleCallback immediately (a guarded no-op when the URL carries no code parameter).

function createOAuthState(options?: OAuthOptions): OAuthStateManager
  • options — OAuth configuration

Returns: The created instance.

createPasswordResetState(client)

Creates a password reset state manager with async state tracking.

function createPasswordResetState(client: AuthClient<UserProfile>): PasswordResetStateManager
  • client — Auth client

Returns: Password reset state manager

createPlatformService()

Creates an Angular platform service with static platform information.

function createPlatformService(): PlatformService

Returns: Platform service with platform flags and isPlatform check

createPromiseState(asyncFn)

Creates a promise state manager for tracking async function state.

function createPromiseState(asyncFn: T): PromiseStateManager<Awaited<ReturnType<T>>>
  • asyncFn — The async function to track

Returns: Promise state manager with observable state

createPushService()

Creates an Angular push notifications service with reactive state.

function createPushService(): PushService

Returns: Push service with observables and action methods

createSignupState(client)

Creates a signup state manager with async state tracking.

function createSignupState(client: AuthClient<T>): SignupStateManager<T>
  • client — Auth client

Returns: Signup state manager

createVersionService()

Creates an Angular version service with reactive state.

function createVersionService(): VersionService

Returns: The created instance.

Constants

STATE_PROVIDER

Injection token for state provider.

const STATE_PROVIDER: InjectionToken<StateProvider>

AUTH_CLIENT

Injection token for auth client.

const AUTH_CLIENT: InjectionToken<AuthClient<unknown>>

THEME_PROVIDER

Injection token for theme provider.

const THEME_PROVIDER: InjectionToken<ThemeProvider>

ROUTER

Injection token for router.

const ROUTER: InjectionToken<Router>

I18N_PROVIDER

Injection token for i18n provider.

const I18N_PROVIDER: InjectionToken<I18nProvider>

HTTP_CLIENT

Injection token for HTTP client.

const HTTP_CLIENT: InjectionToken<HttpClient>

STORAGE_PROVIDER

Injection token for storage provider.

const STORAGE_PROVIDER: InjectionToken<StorageProvider>

LOGGER_PROVIDER

Injection token for logger provider.

const LOGGER_PROVIDER: InjectionToken<LoggerProvider>

Injection Notes

Requirements

Peer dependencies:

  • @angular/core 22.0.0
  • @molecule/app-auth ^1.0.1
  • @molecule/app-device ^1.0.1
  • @molecule/app-forms ^1.0.1
  • @molecule/app-http ^1.0.1
  • @molecule/app-i18n ^1.0.1
  • @molecule/app-logger ^1.0.1
  • @molecule/app-platform ^1.0.1
  • @molecule/app-push ^1.0.1
  • @molecule/app-routing ^1.0.1
  • @molecule/app-state ^1.0.1
  • @molecule/app-storage ^1.0.1
  • @molecule/app-theme ^1.0.1
  • @molecule/app-ui ^1.0.1
  • @molecule/app-utilities ^1.0.1
  • @molecule/app-version ^1.0.1
  • rxjs ^7.8.0

Runtime Dependencies

  • @angular/core

  • @molecule/app-auth

  • @molecule/app-device

  • @molecule/app-forms

  • @molecule/app-http

  • @molecule/app-i18n

  • @molecule/app-logger

  • @molecule/app-platform

  • @molecule/app-push

  • @molecule/app-routing

  • @molecule/app-state

  • @molecule/app-storage

  • @molecule/app-theme

  • @molecule/app-ui

  • @molecule/app-utilities

  • @molecule/app-version

  • rxjs

  • Peer requirements: Angular 22 (@angular/core is pinned to 22.0.0) and rxjs 7.8+.

  • Translations: import t from THIS package, not from @molecule/app-i18n. The re-exported t reads an internal Angular signal, so template bindings that CALL it (expose it on the component as above) re-evaluate automatically when the locale changes; the plain app-i18n t — or a one-time field assignment like title = t(...) — renders once and goes stale. The signal is bumped by provideMolecule — pass your i18n provider there (or call bumpLocaleVersion() from your own locale-change hook) for the reactivity to fire.

  • provideMolecule only registers the providers you pass; injecting a Molecule service whose token was never provided fails at DI time. Per-concern helpers (provideAuth, provideTheme, ...) exist for piecemeal setup.