npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

sds-drag-drop

v1.0.0

Published

A lightweight decorator-based request/response registry for Angular component libraries.

Readme

Angular Function Registry

A lightweight decorator-based request/response registry for Angular component libraries.

It supports:

  • Dispatching typed payloads from components.
  • Handling dispatched operations in one registered handler.
  • Returning handler results to selectors.
  • Routing results to the source component, all selectors, other selectors, the first selector, or matching repeated components.
  • Synchronous and asynchronous handler methods.
  • Automatic component registration and cleanup through Angular lifecycle hooks.

Installation

npm install your-package-name

Import the registry APIs from your package entry point:

import {
    Dispatch,
    Handler,
    RegistryDispatchMode,
    RegistryFunctionType,
    RegistrySelectorContext,
    Selector,
} from 'your-package-name';

Replace your-package-name with the published npm package name.


Required Initialisation

FunctionRegistryService uses a static runtime instance. Angular services provided with providedIn: 'root' are created only when first injected.

Inject the service once in the top-level component of the library so it exists before decorated child components initialise:

import {
    ChangeDetectionStrategy,
    Component,
    inject,
} from '@angular/core';
import { FunctionRegistryService } from 'your-package-name';

@Component({
    selector: 'lib-audit-builder',
    standalone: true,
    templateUrl: './audit-builder.component.html',
    styleUrl: './audit-builder.component.scss',
    changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AuditBuilderComponent {
    private readonly registry = inject(FunctionRegistryService);
}

No registration code is required in child components. Components using @Handler() or @Selector() are registered automatically during ngOnInit and removed during ngOnDestroy.


Registry Function Types

The included registry types are:

RegistryFunctionType.PAGE.add
RegistryFunctionType.PAGE.delete
RegistryFunctionType.PAGE.save
RegistryFunctionType.PAGE.sort

RegistryFunctionType.QUESTION.add
RegistryFunctionType.QUESTION.delete
RegistryFunctionType.QUESTION.save
RegistryFunctionType.QUESTION.sort

RegistryFunctionType.LOGIC.add
RegistryFunctionType.LOGIC.delete
RegistryFunctionType.LOGIC.save

Example:

RegistryFunctionType.QUESTION.add

resolves to:

'ADD_QUESTION'

Basic Request and Response Flow

The registry flow is:

@Dispatch method
    ↓ returns payload
@Handler method
    ↓ returns result
@Selector method
    ↓ receives result and original dispatch information

Shared models:

export interface AddQuestionPayload {
    pageId: string;
    parentId: string | null;
    position: number;
}

export interface Question {
    id: string;
    pageId: string;
    parentId: string | null;
    position: number;
    title: string;
}

Dispatch component

import {
    ChangeDetectionStrategy,
    Component,
    input,
} from '@angular/core';
import {
    Dispatch,
    RegistryFunctionType,
} from 'your-package-name';

@Component({
    selector: 'lib-add-question-button',
    standalone: true,
    template: `
        <button
            type="button"
            (click)="addQuestion()">
            Add question
        </button>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AddQuestionButtonComponent {
    readonly pageId = input.required<string>();
    readonly parentId = input<string | null>(null);
    readonly position = input.required<number>();

    @Dispatch(RegistryFunctionType.QUESTION.add)
    addQuestion(): AddQuestionPayload {
        return {
            pageId: this.pageId(),
            parentId: this.parentId(),
            position: this.position(),
        };
    }
}

Handler component

import {
    ChangeDetectionStrategy,
    Component,
    inject,
} from '@angular/core';
import {
    Handler,
    RegistryFunctionType,
} from 'your-package-name';

@Component({
    selector: 'lib-question-handler',
    standalone: true,
    template: '',
    changeDetection: ChangeDetectionStrategy.OnPush,
})
export class QuestionHandlerComponent {
    private readonly questionApi = inject(QuestionApiService);

    @Handler(RegistryFunctionType.QUESTION.add)
    addQuestion(
        payload: AddQuestionPayload,
    ): Promise<Question> {
        return this.questionApi.addQuestion(payload);
    }
}

Selector component

import {
    ChangeDetectionStrategy,
    Component,
    signal,
} from '@angular/core';
import {
    RegistryFunctionType,
    RegistrySelectorContext,
    Selector,
} from 'your-package-name';

@Component({
    selector: 'lib-question-list',
    standalone: true,
    template: `
        @for (question of questions(); track question.id) {
            <article>
                {{ question.title }}
            </article>
        }
    `,
    changeDetection: ChangeDetectionStrategy.OnPush,
})
export class QuestionListComponent {
    readonly questions = signal<readonly Question[]>([]);

    @Selector(RegistryFunctionType.QUESTION.add)
    onQuestionAdded(
        context: RegistrySelectorContext<Question>,
    ): void {
        this.questions.update(questions => [
            ...questions,
            context.result,
        ]);
    }
}

Selector Context

Every selector receives a RegistrySelectorContext<TResult>:

export interface RegistrySelectorContext<TResult = unknown> {
    type: RegistryFunctionType;
    result: TResult;
    payload: unknown;
    args: unknown[];
    source?: object;
}

Usage:

@Selector(RegistryFunctionType.QUESTION.add)
onQuestionAdded(
    context: RegistrySelectorContext<Question>,
): void {
    console.log(context.type);
    console.log(context.result);
    console.log(context.payload);
    console.log(context.args);
    console.log(context.source);
}

type

The registry operation that was dispatched:

context.type === RegistryFunctionType.QUESTION.add

result

The value returned by the registered handler:

const question = context.result;

payload

The value returned by the original @Dispatch() method:

const payload =
    context.payload as AddQuestionPayload;

args

The original arguments passed to the decorated dispatch method:

@Dispatch(RegistryFunctionType.QUESTION.delete)
deleteQuestion(
    questionId: string,
    event: MouseEvent,
): DeleteQuestionPayload {
    return {
        questionId,
    };
}

The selector can access them through:

const [questionId, event] = context.args;

source

The component instance that called the dispatch method:

const dispatchingComponent = context.source;

Dispatch Modes

The available modes are:

export type RegistryDispatchMode =
    | 'SOURCE_OR_FIRST'
    | 'SOURCE'
    | 'ALL'
    | 'OTHERS'
    | 'FIRST'
    | 'MATCH';

Set the mode in @Dispatch():

@Dispatch(
    RegistryFunctionType.QUESTION.add,
    {
        mode: 'ALL',
    },
)

SOURCE_OR_FIRST

This is the default mode.

@Dispatch(RegistryFunctionType.QUESTION.add)
addQuestion(): AddQuestionPayload {
    return this.createPayload();
}

It behaves as follows:

  1. When the dispatching component has a matching selector, that selector receives the result.
  2. Otherwise, the first matching selector registered for the operation receives the result.

Example with dispatch and selector in the same component:

@Component({
    selector: 'lib-question-editor',
    standalone: true,
    template: `
        <button
            type="button"
            (click)="addQuestion()">
            Add question
        </button>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush,
})
export class QuestionEditorComponent {
    readonly lastAddedQuestion =
        signal<Question | null>(null);

    @Dispatch(RegistryFunctionType.QUESTION.add)
    addQuestion(): AddQuestionPayload {
        return {
            pageId: 'page-1',
            parentId: null,
            position: 1,
        };
    }

    @Selector(RegistryFunctionType.QUESTION.add)
    onQuestionAdded(
        context: RegistrySelectorContext<Question>,
    ): void {
        this.lastAddedQuestion.set(context.result);
    }
}

Only the selector owned by the dispatching QuestionEditorComponent instance receives the result.


SOURCE

Only selectors declared on the component instance that dispatched the operation receive the result.

@Dispatch(
    RegistryFunctionType.QUESTION.save,
    {
        mode: 'SOURCE',
    },
)
saveQuestion(): SaveQuestionPayload {
    return {
        question: this.question(),
    };
}

Selector in the same component:

@Selector(RegistryFunctionType.QUESTION.save)
onQuestionSaved(
    context: RegistrySelectorContext<Question>,
): void {
    this.question.set(context.result);
}

When the source component has no selector for that operation, no selector is called.


ALL

Every selector registered for the operation receives the result, subject to any selector matcher.

@Dispatch(
    RegistryFunctionType.QUESTION.add,
    {
        mode: 'ALL',
    },
)
addQuestion(): AddQuestionPayload {
    return {
        pageId: this.pageId(),
        parentId: null,
        position: this.position(),
    };
}

Example selectors:

@Selector(RegistryFunctionType.QUESTION.add)
refreshQuestionList(
    context: RegistrySelectorContext<Question>,
): void {
    this.questions.update(items => [
        ...items,
        context.result,
    ]);
}
@Selector(RegistryFunctionType.QUESTION.add)
updateQuestionCount(
    context: RegistrySelectorContext<Question>,
): void {
    this.questionCount.update(count => count + 1);
}

Both selectors receive the same result.


OTHERS

Every matching selector except selectors owned by the dispatching component receives the result.

@Dispatch(
    RegistryFunctionType.QUESTION.save,
    {
        mode: 'OTHERS',
    },
)
saveQuestion(): SaveQuestionPayload {
    return {
        question: this.question(),
    };
}

This is useful when the source component already updates itself directly and only other components need notification.


FIRST

Only the first matching selector registered for the operation receives the result.

@Dispatch(
    RegistryFunctionType.QUESTION.delete,
    {
        mode: 'FIRST',
    },
)
deleteQuestion(
    questionId: string,
): DeleteQuestionPayload {
    return {
        questionId,
    };
}

Selector registration order follows component creation and ngOnInit order. Do not use FIRST when application behaviour depends on a specific component instance.


MATCH

MATCH sends the result only to selectors that:

  1. Define a match callback.
  2. Return true from that callback.

This is used for repeated components where only one or selected component instances should receive the result.

Shared types:

export interface SaveQuestionPayload {
    questionId: string;
    title: string;
}

export interface Question {
    id: string;
    title: string;
}

Dispatch component

The dispatch payload must contain the value used by the selector matcher.

import {
    ChangeDetectionStrategy,
    Component,
    input,
} from '@angular/core';
import {
    Dispatch,
    RegistryFunctionType,
} from 'your-package-name';

@Component({
    selector: 'lib-save-question-button',
    standalone: true,
    template: `
        <button
            type="button"
            (click)="saveQuestion()">
            Save
        </button>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SaveQuestionButtonComponent {
    readonly questionId = input.required<string>();
    readonly title = input.required<string>();

    @Dispatch(
        RegistryFunctionType.QUESTION.save,
        {
            mode: 'MATCH',
        },
    )
    saveQuestion(): SaveQuestionPayload {
        return {
            questionId: this.questionId(),
            title: this.title(),
        };
    }
}

Repeated selector component

import {
    ChangeDetectionStrategy,
    Component,
    input,
    signal,
} from '@angular/core';
import {
    RegistryFunctionType,
    RegistrySelectorContext,
    Selector,
} from 'your-package-name';

@Component({
    selector: 'lib-question-row',
    standalone: true,
    template: `
        @if (question(); as item) {
            <article>
                {{ item.title }}
            </article>
        }
    `,
    changeDetection: ChangeDetectionStrategy.OnPush,
})
export class QuestionRowComponent {
    readonly questionId = input.required<string>();

    readonly question = signal<Question | null>(null);

    @Selector<QuestionRowComponent>(
        RegistryFunctionType.QUESTION.save,
        {
            match: (component, context) =>
                component.questionId() ===
                (
                    context.payload as SaveQuestionPayload
                ).questionId,
        },
    )
    onQuestionSaved(
        context: RegistrySelectorContext<Question>,
    ): void {
        this.question.set(context.result);
    }
}

Loop template

@for (question of questions(); track question.id) {
    <lib-question-row
        [questionId]="question.id"
    />
}

When the dispatch payload contains:

{
    questionId: 'question-2',
    title: 'Updated question',
}

the matcher checks each row:

'question-1' === 'question-2' // false
'question-2' === 'question-2' // true
'question-3' === 'question-2' // false

Only the row with questionId() === 'question-2' receives the handler result.

Where component.questionId() comes from

The selector generic specifies the owner component type:

@Selector<QuestionRowComponent>(...)

The registry invokes the matcher with:

selector.match?.(
    selector.owner,
    context,
)

selector.owner is the actual registered QuestionRowComponent instance.

The component declares:

readonly questionId = input.required<string>();

Angular signal inputs are read by calling them:

this.questionId()

Inside the matcher, the owner instance is named component, so the same input is read as:

component.questionId()

The dispatch component does not call component.questionId(). It only places its target identifier into the payload:

return {
    questionId: this.questionId(),
    title: this.title(),
};

The selector compares:

selector component questionId
    ===
dispatch payload questionId

Matching Multiple Selectors

A matcher does not have to identify only one component. Every selector whose matcher returns true receives the result in MATCH mode.

Example matching every row on the same page:

export interface SavePagePayload {
    pageId: string;
}
@Selector<QuestionRowComponent>(
    RegistryFunctionType.PAGE.save,
    {
        match: (component, context) =>
            component.pageId() ===
            (
                context.payload as SavePagePayload
            ).pageId,
    },
)
onPageSaved(
    context: RegistrySelectorContext<AuditPage>,
): void {
    this.refreshFromPage(context.result);
}

All question rows belonging to the saved page receive the result.


Matchers with Other Dispatch Modes

Matchers are evaluated before dispatch mode routing.

This means selector matchers also filter selectors when using:

'SOURCE_OR_FIRST'
'SOURCE'
'ALL'
'OTHERS'
'FIRST'

Example:

@Dispatch(
    RegistryFunctionType.QUESTION.save,
    {
        mode: 'ALL',
    },
)

Only selectors with no matcher or selectors whose matcher returns true are included.

Selectors without a matcher always pass the initial matching filter:

selector.match?.(
    selector.owner,
    context,
) ?? true

In MATCH mode, selectors without a matcher are then excluded.


Async Dispatch Methods

A dispatch method can return a value or a promise:

@Dispatch(RegistryFunctionType.QUESTION.save)
async prepareQuestionSave(): Promise<SaveQuestionPayload> {
    const valid = await this.validateQuestion();

    if (!valid) {
        return undefined as never;
    }

    return {
        question: this.question(),
    };
}

When a dispatch method returns undefined, registry execution stops:

@Dispatch(RegistryFunctionType.QUESTION.save)
saveQuestion(): SaveQuestionPayload | undefined {
    if (this.form.invalid) {
        return undefined;
    }

    return {
        question: this.form.getRawValue(),
    };
}

No handler or selector is called.


Async Handlers

Handlers can return either a direct result or a promise:

@Handler(RegistryFunctionType.QUESTION.save)
saveQuestion(
    payload: SaveQuestionPayload,
): Promise<Question> {
    return this.questionApi.save(payload);
}

The registry waits for the handler before notifying selectors.


Handler Returning undefined

When a handler returns undefined, selectors are not notified:

@Handler(RegistryFunctionType.QUESTION.save)
async saveQuestion(
    payload: SaveQuestionPayload,
): Promise<Question | undefined> {
    const saved = await this.questionApi.save(payload);

    if (!saved) {
        return undefined;
    }

    return saved;
}

Handler Errors

Errors thrown by a handler reject the dispatch promise:

try {
    await this.saveQuestion();
} catch (error) {
    console.error(error);
}

A decorated dispatch method always returns a promise because the decorator wrapper is asynchronous:

const result = await this.saveQuestion();

Directly Receiving the Dispatch Result

The dispatch method resolves with the handler result:

@Dispatch(RegistryFunctionType.QUESTION.add)
addQuestion(): AddQuestionPayload {
    return {
        pageId: this.pageId(),
        parentId: null,
        position: this.position(),
    };
}

Call it with:

const question = await this.addQuestion();

For strong typing at the calling location, define the decorated method return according to the original payload contract and cast the resolved handler result only where required by the current decorator typing.

Selectors remain the primary typed response mechanism.


Accessing Original Dispatch Arguments

@Dispatch(RegistryFunctionType.QUESTION.sort)
sortQuestion(
    questionId: string,
    previousIndex: number,
    currentIndex: number,
): SortQuestionPayload {
    return {
        questionId,
        previousIndex,
        currentIndex,
    };
}

Selector:

@Selector(RegistryFunctionType.QUESTION.sort)
onQuestionSorted(
    context: RegistrySelectorContext<SortQuestionResult>,
): void {
    const [
        questionId,
        previousIndex,
        currentIndex,
    ] = context.args;

    console.log({
        questionId,
        previousIndex,
        currentIndex,
    });
}

Use context.payload for the normalized request sent to the handler. Use context.args only when the original method arguments are required.


Multiple Selectors in One Component

A component can declare multiple selectors for different registry operations:

export class QuestionListComponent {
    @Selector(RegistryFunctionType.QUESTION.add)
    onQuestionAdded(
        context: RegistrySelectorContext<Question>,
    ): void {
        // Add question to local state.
    }

    @Selector(RegistryFunctionType.QUESTION.save)
    onQuestionSaved(
        context: RegistrySelectorContext<Question>,
    ): void {
        // Replace question in local state.
    }

    @Selector(RegistryFunctionType.QUESTION.delete)
    onQuestionDeleted(
        context: RegistrySelectorContext<string>,
    ): void {
        // Remove question from local state.
    }

    @Selector(RegistryFunctionType.QUESTION.sort)
    onQuestionSorted(
        context: RegistrySelectorContext<SortQuestionResult>,
    ): void {
        // Apply returned ordering.
    }
}

A component can also declare multiple selector methods for the same operation. Each method is registered separately.


One Handler Per Registry Type

The current registry stores handlers in:

Map<RegistryFunctionType, RegistryStoredFn>

Only one handler can be active for each registry type.

When another component registers a handler for the same type, it replaces the previously registered handler:

@Handler(RegistryFunctionType.QUESTION.add)

Do not register the same handler type in multiple active components.

Use one handler owner for each operation:

ADD_QUESTION    → one handler
SAVE_QUESTION   → one handler
DELETE_QUESTION → one handler
SORT_QUESTION   → one handler

Multiple selectors are supported.


Component Lifecycle

Components using @Handler() or @Selector() are automatically registered by patched lifecycle methods:

ngOnInit    → registerHost(component)
ngOnDestroy → unregisterHost(component)

Existing lifecycle methods continue to execute:

export class QuestionRowComponent {
    ngOnInit(): void {
        // Existing component setup still runs.
    }

    ngOnDestroy(): void {
        // Existing component cleanup still runs.
    }

    @Selector(RegistryFunctionType.QUESTION.save)
    onQuestionSaved(
        context: RegistrySelectorContext<Question>,
    ): void {
        // Selector logic.
    }
}

Do not replace ngOnInit or ngOnDestroy dynamically after decorators have been evaluated.


No Registered Handler

When no handler is registered for a dispatched type:

const handler = this.handlers.get(type);

the dispatch resolves with:

undefined

No selector is notified.

Ensure the component containing the required handler exists in the rendered component tree before dispatching.


No Matching Selector

When the selected dispatch mode produces no selectors, the handler still executes and returns its result. No selector callback runs.

Example:

@Dispatch(
    RegistryFunctionType.QUESTION.save,
    {
        mode: 'SOURCE',
    },
)

When the source component has no QUESTION.save selector:

handler executes
result is returned
no selector is called

Complete Feature Example

Parent component

import {
    ChangeDetectionStrategy,
    Component,
    inject,
    signal,
} from '@angular/core';
import {
    FunctionRegistryService,
} from 'your-package-name';

@Component({
    selector: 'lib-question-feature',
    standalone: true,
    imports: [
        AddQuestionButtonComponent,
        QuestionHandlerComponent,
        QuestionListComponent,
    ],
    templateUrl: './question-feature.component.html',
    changeDetection: ChangeDetectionStrategy.OnPush,
})
export class QuestionFeatureComponent {
    private readonly registry = inject(FunctionRegistryService);

    readonly questions = signal<readonly Question[]>([]);
}

Parent template

<lib-question-handler />

<lib-question-list />

<lib-add-question-button
    [pageId]="'page-1'"
    [parentId]="null"
    [position]="1"
/>

Execution flow

User clicks Add question
        ↓
AddQuestionButtonComponent.addQuestion()
        ↓
@Dispatch returns AddQuestionPayload
        ↓
FunctionRegistryService.execute()
        ↓
QuestionHandlerComponent.addQuestion(payload)
        ↓
@Handler returns Question
        ↓
FunctionRegistryService.notifySelectors()
        ↓
QuestionListComponent.onQuestionAdded(context)

Public Package Exports

Export the registry APIs from the library public entry point:

export {
    Dispatch,
    FunctionRegistryService,
    Handler,
    RegistryFunctionType,
    Selector,
} from './lib/function-registry';

export type {
    RegistryDispatchMode,
    RegistryDispatchOptions,
    RegistrySelectorContext,
    RegistrySelectorOptions,
} from './lib/function-registry';

Consumers can then import from the package root:

import {
    Dispatch,
    Handler,
    RegistryFunctionType,
    RegistrySelectorContext,
    Selector,
} from 'your-package-name';

Mode Reference

| Mode | Selector delivery | |---|---| | SOURCE_OR_FIRST | Source selector when present; otherwise the first matching selector | | SOURCE | Selectors owned by the dispatching component | | ALL | Every matching selector | | OTHERS | Every matching selector except selectors owned by the source component | | FIRST | First matching selector | | MATCH | Every selector that defines a matcher and returns true |


Usage Rules

  1. Inject FunctionRegistryService once in the library feature root.
  2. Register only one active handler per registry function type.
  3. Return undefined from a dispatch method to cancel execution.
  4. Return undefined from a handler to prevent selector notification.
  5. Use SOURCE_OR_FIRST for the default source-aware behaviour.
  6. Use SOURCE when only the dispatching component should receive the result.
  7. Use ALL when every relevant selector should receive the result.
  8. Use OTHERS when the source should be excluded.
  9. Use FIRST only when registration order is acceptable.
  10. Use MATCH for repeated components that must be selected using payload data.
  11. Include matching identifiers in the dispatch payload when using MATCH.
  12. Ensure the handler component is rendered before dispatching.