SolidX

Dynamic Selection Providers

Learn how to create dynamic selection providers to customize the selection options in your application.

Dynamic selection providers let you fetch options at runtime, rather than relying on static lists.
They are useful when options need to come from a database, an API, or some logic that changes based on context.

For example, you might want to populate a dropdown with stock symbols fetched from a live exchange API, or with filtered database values.

1. Example Field Metadata

Here’s an example field configuration using a custom provider named StockApiSelectionProvider.
The selectionDynamicProviderCtxt specifies which fields from the API response should be used as labels and values.

{
  "name": "preferredStock",
  "displayName": "Preferred Stock",
  "description": "Select a stock symbol from the live exchange",
  "type": "selectionDynamic",
  "ormType": "varchar",
  "isSystem": false,
  "selectionDynamicProvider": "StockApiSelectionProvider",
  "selectionDynamicProviderCtxt": "{\"labelField\": \"name\", \"valueField\": \"symbol\"}",
  "selectionValueType": "string",
  "required": true,
  "unique": false,
  "index": false,
  "private": false,
  "encrypt": false,
  "isUserKey": false,
  "enableAuditTracking": false,
  "isMultiSelect": true
}
  • Use the built-in ListOfValuesSelectionProvider when you just need to fetch options from your database.
  • Create a custom provider when the logic for fetching or filtering is more complex, or when data comes from an external source like an API.

2. Creating the Provider

Your provider class must implement the ISelectionProvider interface.
The most important method is values(), which fetches and returns the available options.

The value() method is deprecated. It can simply throw a NotImplementedException and is kept only for backward compatibility.

import { Injectable, Logger } from "@nestjs/common";
import { HttpService } from "@solidstarters/solid-core";
import { lastValueFrom } from "rxjs";
import { SelectionProvider } from "@solidstarters/solid-core";
import {
  ISelectionProvider,
  ISelectionProviderContext,
  ISelectionProviderValues,
} from "../interfaces";

interface StockApiSelectionProviderContext extends ISelectionProviderContext {
  labelField: string; // Field to use as label
  valueField: string; // Field to use as value
}

@SelectionProvider()
@Injectable()
export class StockApiSelectionProvider
  implements ISelectionProvider<StockApiSelectionProviderContext>
{
  private readonly logger = new Logger(this.constructor.name);
  private readonly url = "https://api.example.com/stocks"; // Example API endpoint

  constructor(private readonly httpService: HttpService) {}

  name(): string {
    return "StockApiSelectionProvider";
  }

  help(): string {
    return "# Fetches options dynamically from an external API.\n" +
           "Context requires:\n" +
           "- url: API endpoint\n" +
           "- labelField: field to use for label\n" +
           "- valueField: field to use for value";
  }

  async value(): Promise<ISelectionProviderValues | null> {
    throw new Error("Not implemented (deprecated).");
  }

  async values(
    query: string,
    ctxt: StockApiSelectionProviderContext
  ): Promise<readonly ISelectionProviderValues[]> {
    if (!ctxt.labelField || !ctxt.valueField) {
      this.logger.error("Invalid context");
      return [];
    }

    try {
      const response$ = this.httpService.get(this.url);
      const response = await lastValueFrom(response$);

      if (!Array.isArray(response.data)) {
        this.logger.warn("API response is not an array");
        return [];
      }

      return response.data.map((item: any) => ({
        label: item[ctxt.labelField],
        value: item[ctxt.valueField],
      }));
    } catch (err) {
      this.logger.error(`Failed to fetch values from API: ${err.message}`);
      return [];
    }
  }
}

3. Registering the Provider

Since providers are standard NestJS providers, register them in the module where they should be available.

// fees-portal.module.ts
@Module({
  ...
  providers: [StockApiSelectionProvider],
  ...
})

4. Interfaces

Below are the core interfaces used when implementing a dynamic selection provider.

export interface ISelectionProvider<T extends ISelectionProviderContext> {
  // Description of the provider and expected context
  help(): string;

  // Unique name of the provider
  name(): string;

  // Deprecated method — throw NotImplementedException
  value(optionValue: string, ctxt: T): Promise<ISelectionProviderValues | any>;

  // Fetch selection options dynamically
  values(query: any, ctxt: T): Promise<readonly ISelectionProviderValues[]>;
}

export interface ISelectionProviderContext {}

export interface ISelectionProviderValues {
  label: string;
  value: string;
}