New Research: Supply Chain Attack on Axios Pulls Malicious Dependency from npm.Details
Socket
Book a DemoSign in
Socket

@mmstack/di

Package Overview
Dependencies
Maintainers
1
Versions
16
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@mmstack/di

A collection of dependency injection utilities for Angular that simplify working with InjectionTokens and provide type-safe patterns for creating injectable services.

latest
Source
npmnpm
Version
21.0.5
Version published
Maintainers
1
Created
Source

@mmstack/di

A collection of dependency injection utilities for Angular that simplify working with InjectionTokens and provide type-safe patterns for creating injectable services.

npm version License

Installation

npm install @mmstack/di

Utilities

This library provides the following utilities:

  • injectable - Creates a typed InjectionToken with inject and provide helper functions for type-safe dependency injection.
  • injectLazy - Defers the resolution and instantiation of a token until it is actually accessed.
  • createRunInInjectionContext - Captures an injection context securely and returns a runner function, useful for inject() inside async callbacks.
  • rootInjectable - Creates a lazily-initialized root-level injectable that maintains a singleton instance.
  • createScope - Creates a dependency injection scope that caches singletons based on the factory function.

injectable

Creates a typed InjectionToken with convenient, type-safe inject and provider functions, eliminating boilerplate and ensuring type safety throughout your dependency injection flow. It returns a tuple of [injectFn, provideFn] that work together seamlessly.

The injectable function supports three patterns:

  • Basic - Returns T | null when not provided
  • With Fallback - Returns a default value when not provided, can be either
  • With Lazy Fallback - Same as with fallback, but the fallback iz lazily evaluated, useful for expensive fallbacks or ones that require injection
  • With Error - Throws a custom error message when not provided

Basic Usage

import { Component, Injectable } from '@angular/core';
import { injectable } from '@mmstack/di';

// Create a typed injectable
const [injectLogger, provideLogger] = injectable<Logger>('Logger');

// Provide the value in a component or module
@Component({
  selector: 'app-root',
  providers: [
    provideLogger({
      log: (msg) => console.log(`[LOG]: ${msg}`),
      error: (msg) => console.error(`[ERROR]: ${msg}`),
    }),
  ],
})
export class AppComponent {}

// Inject it anywhere in the component tree
@Injectable()
export class DataService {
  private logger = injectLogger(); // Logger | null

  fetchData() {
    this.logger?.log('Fetching data...');
  }
}

With Factory Dependencies

import { HttpClient } from '@angular/common/http';
import { injectable } from '@mmstack/di';

interface ApiConfig {
  baseUrl: string;
  timeout: number;
}

const [injectApiConfig, provideApiConfig] = injectable<ApiConfig>('ApiConfig');

// Provide using a factory with dependencies
@Component({
  providers: [
    provideApiConfig(
      (http: HttpClient) => ({
        baseUrl: 'https://api.example.com',
        timeout: 5000,
      }),
      [HttpClient], // Dependencies array
    ),
  ],
})
export class AppComponent {}

With Fallback Value

When you want to provide a default value instead of returning null:

import { injectable } from '@mmstack/di';

interface Theme {
  primary: string;
  secondary: string;
}

const [injectTheme, provideTheme] = injectable<Theme>('Theme', {
  fallback: {
    primary: '#007bff',
    secondary: '#6c757d',
  },
});

// or if you need inject/lazy evaluation
const [injectTheme, provideTheme] = injectable<Theme>('Theme', {
  lazyFallback: () => {
    return {
      primary: inject(APP_PRIMARY),
      secondary: '#6c757d',
    },
  }
});

@Injectable()
export class ThemeService {
  // Always returns a Theme, never null
  private theme = injectTheme();

  getPrimaryColor() {
    return this.theme.primary; // Safe to access
  }
}

With Error Message

When you want to enforce that the value must be provided:

import { injectable } from '@mmstack/di';

const [injectApiKey, provideApiKey] = injectable<string>('ApiKey', {
  errorMessage: 'API Key is required! Please provide it using provideApiKey().',
});

@Injectable()
export class ApiService {
  // Throws error if not provided
  private apiKey = injectApiKey();

  makeRequest() {
    // apiKey is guaranteed to exist here
    return fetch(`https://api.example.com?key=${this.apiKey}`);
  }
}

Providing Functions as Values

The provideFn correctly handles functions as values (not factories):

import { injectable } from '@mmstack/di';

type Validator = (value: string) => boolean;

const [injectValidator, provideValidator] = injectable<Validator>('Validator');

@Component({
  providers: [
    // Providing a function as a value (not a factory)
    provideValidator((value: string) => value.length > 5),
  ],
})
export class FormComponent {
  private validator = injectValidator();

  validate(input: string) {
    return this.validator?.(input) ?? false;
  }
}

Advanced: Scoped Context Pattern

Create context-like dependency injection patterns:

import { Component, Injectable } from '@angular/core';
import { injectable } from '@mmstack/di';

interface FormContext {
  formId: string;
  isDirty: boolean;
  submit: () => void;
}

const [injectFormContext, provideFormContext] = injectable<FormContext>('FormContext', {
  errorMessage: 'FormContext must be provided by a parent form component',
});

@Component({
  selector: 'app-form',
  providers: [
    provideFormContext({
      formId: 'user-form',
      isDirty: false,
      submit: () => console.log('Submitting form...'),
    }),
  ],
  template: `
    <form>
      <app-form-field></app-form-field>
      <app-form-actions></app-form-actions>
    </form>
  `,
})
export class FormComponent {}

@Component({
  selector: 'app-form-field',
  template: `<input [id]="formContext.formId + '-input'" />`,
})
export class FormFieldComponent {
  // Automatically gets the context from parent
  formContext = injectFormContext();
}

@Component({
  selector: 'app-form-actions',
  template: `<button (click)="formContext.submit()">Submit</button>`,
})
export class FormActionsComponent {
  formContext = injectFormContext();
}

injectLazy

Defers the resolution and instantiation of an injection token until the returned getter function is actually called.

Angular's native inject() resolves and instantiates dependencies immediately during the construction phase. If a service is heavily resource-intensive but only needed conditionally (like an export service or a complex editor), injectLazy allows you to capture the injection context immediately while delaying instantiation. The resolved value is cached, acting as a standard scoped singleton on subsequent calls.

Basic Usage

import { Component, HostListener } from '@angular/core';
import { injectLazy } from '@mmstack/di';

@Component({
  selector: 'app-export-button',
  template: `<button>Export Data</button>`,
})
export class ExportButtonComponent {
  // Captures the Injector but does NOT instantiate HeavyExportService yet
  private getExportService = injectLazy(HeavyExportService);

  @HostListener('click')
  export() {
    // HeavyExportService is instantiated on the first click, then cached
    const service = this.getExportService();
    service.doExport();
  }
}

With Options

It fully supports Angular's InjectOptions and guarantees correct return types (e.g., returning T | null when optional: true):

const getOptionalDep = injectLazy(MyToken, { optional: true });

// Later...
const dep = getOptionalDep(); // MyToken | null

createRunInInjectionContext

Captures an injection context securely and returns a runner function.

This utility allows you to execute callbacks inside the captured context at a later time. It solves the common pain point of needing to use inject() inside asynchronous callbacks, RxJS streams, or external event listeners where the framework's implicit injection context has been lost.

Basic Usage

import { Component, OnInit, inject } from '@angular/core';
import { createRunInInjectionContext } from '@mmstack/di';

@Component({
  selector: 'app-dialog-trigger',
  template: `<button>Open Dialog</button>`,
})
export class DialogTriggerComponent implements OnInit {
  // Grabs the current injector during construction
  private runInContext = createRunInInjectionContext();

  ngOnInit() {
    // someExternalLibrary is out of Angular's zone/context
    someExternalLibrary.on('openEvent', () => {
      this.runInContext(() => {
        // We can safely use `inject()` here even though we are inside an async callback!
        const dialog = inject(DialogService);
        dialog.open();
      });
    });
  }
}

With Explicit Injector

You can also completely bypass the ambient capture and provide an Injector explicitly:

// Outside of normal injection context
const runner = createRunInInjectionContext(appRef.injector);

runner(() => {
  const router = inject(Router);
  router.navigate(['/home']);
});

rootInjectable

Creates a lazily-initialized root-level injectable that maintains a singleton instance across your entire application. The factory function runs in the root injection context on first access, allowing you to inject other dependencies.

Important: This should only be used for pure singletons. If you need scoped instances, use regular @Injectable services with providedIn or component-level providers.

Basic Usage

import { Injectable } from '@angular/core';
import { rootInjectable } from '@mmstack/di';

interface Logger {
  log: (message: string) => void;
}

// Create a root-level injectable
const injectLogger = rootInjectable<Logger>(() => ({
  log: (message) => console.log(`[${new Date().toISOString()}] ${message}`),
}));

@Injectable()
export class DataService {
  private logger = injectLogger();

  fetchData() {
    this.logger.log('Fetching data...');
  }
}

@Injectable()
export class UserService {
  private logger = injectLogger(); // Same instance as above

  saveUser() {
    this.logger.log('Saving user...');
  }
}

With Dependencies

The factory function receives the root injector, allowing you to inject other services:

import { HttpClient } from '@angular/common/http';
import { rootInjectable } from '@mmstack/di';

interface ApiClient {
  get: (url: string) => Promise<any>;
  post: (url: string, data: any) => Promise<any>;
}

const injectApiClient = rootInjectable<ApiClient>((injector) => {
  const http = injector.get(HttpClient); // or just inject(HttpClient)

  return {
    get: (url) => fetch(url).then((r) => r.json()),
    post: (url, data) =>
      fetch(url, {
        method: 'POST',
        body: JSON.stringify(data),
      }).then((r) => r.json()),
  };
});

@Injectable()
export class ProductService {
  private api = injectApiClient(); // Singleton instance

  loadProducts() {
    return this.api.get('/api/products');
  }
}

State Management Example

Create a simple global state manager:

import { signal, computed } from '@angular/core';
import { rootInjectable } from '@mmstack/di';

interface User {
  id: string;
  name: string;
  email: string;
}

interface AuthState {
  user: User | null;
  isAuthenticated: boolean;
}

const injectAuthStore = rootInjectable(() => {
  const user = signal<User | null>(null);
  const isAuthenticated = computed(() => user() !== null);

  return {
    user: user.asReadonly(),
    isAuthenticated,
    login: (userData: User) => user.set(userData),
    logout: () => user.set(null),
  };
});

@Injectable()
export class AuthService {
  private store = injectAuthStore(); // Singleton across app

  login(email: string, password: string) {
    // Perform authentication...
    this.store.login({
      id: '123',
      name: 'John Doe',
      email,
    });
  }

  logout() {
    this.store.logout();
  }
}

@Component({
  selector: 'app-navbar',
  template: `
    @if (authStore.isAuthenticated()) {
      <span>{{ authStore.user()?.name }}</span>
      <button (click)="logout()">Logout</button>
    }
  `,
})
export class NavbarComponent {
  authStore = injectAuthStore(); // Same instance

  logout() {
    this.authStore.logout();
  }
}

createScope

Creates a dependency injection scope using a dynamic InjectionToken representing a caching registry. Factories executed within the scope run in the Angular injection context and their results are cached, effectively creating scoped singletons, that are destroyed when the scoped provider is. It returns a tuple of [injectable, provider].

Basic Usage

import { Component, inject } from '@angular/core';
import { createScope } from '@mmstack/di';

// Create the scope
const [injectableFeatureItem, provideFeatureScope] = createScope('FeatureScope');

// Provide the scope at a specific component level boundary
@Component({
  selector: 'app-feature',
  providers: [provideFeatureScope()],
  template: `<app-child></app-child>`,
})
export class FeatureComponent {}

// Use the scope to register an item factory
// The factory will run in the injection context so you can use inject()
const useFeatureItem = injectableFeatureItem(() => {
  const someDep = inject(SomeDependency);
  return {
    id: Math.random(),
    doWork: () => someDep.work(),
  };
});

@Component({
  selector: 'app-child',
  template: `<div>Child Item ID: {{ item.id }}</div>`,
})
export class ChildComponent {
  // Always returns the exact same instance for this specific scope provider boundary
  item = useFeatureItem();
}

License

MIT © Miha Mulec

Keywords

angular

FAQs

Package last updated on 05 Apr 2026

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts