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

ng-adflowtail

v0.1.4

Published

A highly professional, developer-friendly, and lightweight Angular 22 component library built on top of **Tailwind CSS**, **Flowbite**, and modern **Angular Signals**.

Readme

ng-adflowtail 🚀

A highly professional, developer-friendly, and lightweight Angular 22 component library built on top of Tailwind CSS, Flowbite, and modern Angular Signals.

ng-adflowtail is architected with a secondary entry points design (similar to @angular/material). This enables absolute tree-shaking, meaning consumers only bundle the precise widgets and logic they import, leading to ultra-slim production bundles.


Key Features

  • Angular 22 Core: Leverages the absolute latest APIs including Standalone Components, Signal-based Inputs/Outputs, Computeds, and Host Directives.
  • Advanced State Management: Built natively around Signals to ensure highly performant and predictable change detection.
  • Signals-Based Forms: Clean integration with modern Angular Signal Forms (@angular/forms/signals).
  • Dynamic CSS Custom Properties (Theming): Easily brand components in real-time by passing a single hex/rgb color string. The built-in directive dynamically computes complementary brand shades using modern CSS color-mix().
  • Responsive & Accessible: Fully styled with Tailwind CSS and Flowbite, implementing modern accessibility requirements and responsive layouts.
  • Full Tree-Shaking: Secondary entry points keep your production bundles optimal.

Table of Contents

  1. Prerequisites & Installation
  2. Tailwind CSS Configuration
  3. Architecture & Imports
  4. Branding & Custom Theming
  5. Component: AdMultiSelect
  6. Component: AdDataTable
  7. Component: AdAutocomplete
  8. Adding New Components (Library Developers)
  9. Build & Publish Commands

Prerequisites & Installation

1. Install the NPM Package

Requires Angular 22+. Install ng-adflowtail and its peer dependencies:

npm install ng-adflowtail flowbite tslib @angular/aria @angular/cdk

| Package | Required for | | :--- | :--- | | flowbite, tailwindcss | All components (styling) | | @angular/forms | ad-multi-select, ad-autocomplete (Signal Forms) — usually already in your app | | @angular/aria, @angular/cdk | ad-autocomplete only — separate packages, not bundled with @angular/core |

If you use only ad-multi-select and ad-data-table, you can skip @angular/aria and @angular/cdk until you import ng-adflowtail/ad-autocomplete.

Note: @angular/aria is only published for Angular 22+. Consumer apps on Angular 21 must upgrade to Angular 22 before using ad-autocomplete.

2. Install Tailwind CSS

Ensure your Angular project has Tailwind CSS set up. If not, follow the official Tailwind CSS installation guide for Angular.


Tailwind CSS Configuration

Because ng-adflowtail relies on Tailwind classes, you must configure Tailwind to scan the library's compiled templates inside node_modules so that styles are not purged in production builds.

For Tailwind CSS v4 (Modern CSS Configuration)

Add the @source directive in your main CSS file (e.g., styles.css):

@import "tailwindcss";

/* Add the source path to scan ng-adflowtail templates */
@source "../../node_modules/ng-adflowtail";

For Tailwind CSS v3 (tailwind.config.js)

Add the ng-adflowtail path to your content array:

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./src/**/*.{html,ts}",
    "./node_modules/ng-adflowtail/**/*.{js,mjs,html}" // Add this line
  ],
  theme: {
    extend: {},
  },
  plugins: [
    require('flowbite/plugin') // Enable Flowbite utility classes
  ],
}

Architecture & Scaffolding

To import components, use their specialized entry points. Do not import everything from ng-adflowtail directly.

// Core Directives & Shared Interfaces (Primary Entry Point)
import { AdPrimaryColorDirective, AdColumn } from 'ng-adflowtail';

// Individual Secondary Entry Points
import { AdMultiSelect } from 'ng-adflowtail/ad-multi-select';
import { AdDataTable } from 'ng-adflowtail/ad-data-table';
import { AdAutocomplete } from 'ng-adflowtail/ad-autocomplete';

Branding & Custom Theming

AdPrimaryColorDirective ([adPrimaryColor])

Both AdMultiSelect, AdDataTable, and AdAutocomplete support dynamic theming via the primaryColor input. This input is automatically mapped on all components using Angular's robust Host Directives feature, delegating to the standalone AdPrimaryColorDirective.

This directive dynamically binds modern CSS Custom Properties (--color-brand, --color-fg-brand, etc.) to the host elements. Under the hood, it uses native CSS color-mix() to calculate brand states on-the-fly based on any valid CSS color input (hex, rgb, hsl, or web colors):

// Mapped custom properties on the host element style:
--color-brand: [primaryColor];
--color-brand-soft: color-mix(in srgb, [primaryColor] 10%, transparent);
--color-brand-softer: color-mix(in srgb, [primaryColor] 5%, transparent);
--color-brand-medium: color-mix(in srgb, [primaryColor] 20%, transparent);
--color-brand-strong: color-mix(in srgb, [primaryColor] 90%, black);

Example: Dynamic Color Picker Implementation

You can bind a color input or a signal directly to the widgets:

<!-- Fully rebranded multi-select -->
<ad-multi-select 
  [formField]="userForm.permissions" 
  [options]="permissionOptions" 
  primaryColor="#0d9488" 
/>

<!-- Rebranded data table -->
<ad-data-table 
  [data]="users" 
  [config]="tableConfig" 
  primaryColor="#7c3aed" 
/>

<!-- Rebranded autocomplete -->
<ad-autocomplete
  [formField]="userForm.selectedUser"
  [options]="userOptions()"
  primaryColor="#0284c7"
  (search)="onUserSearch($event)"
/>

Component: AdMultiSelect

The AdMultiSelect component (ng-adflowtail/ad-multi-select) is a highly interactive, accessible dropdown multi-select. It is integrated natively with the new Angular Signal-based Forms (@angular/forms/signals), displaying selection tokens as elegant, dismissible chips.

Key Features

  • Signal Forms: Directly binds to form fields via the formField input parameter.
  • Interactive Tokens: Selected values render as clean chips with a close button.
  • Global Actions: Includes fast-access "Select all" and "Clear" utilities.
  • Robust UX: Includes click-outside detection and Escape keyboard key handling to dismiss the dropdown panel safely.
  • Validation-Driven: Automatically applies red borders and error displays when the bound signal form is touched and invalid.

Multi-Select API Spec

@Input() Bindings

| Input | Type | Default | Description | | :--- | :--- | :--- | :--- | | formField (Required) | FieldTree<string[]> | — | The signal forms field node to bind (e.g. myForm.roles). | | options | AdMultiSelectOption[] | [] | List of items available in the dropdown selector. | | placeholder | string | 'Select options' | Label shown when no selection has been made yet. | | disabled | boolean | false | Disables user interaction and prevents opening the menu. | | errorMessage | string | '' | Custom message text shown underneath the input on validation failure. | | primaryColor | string | — | Overrides default branding colors with computed dynamic CSS colors. |

Model Interfaces

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

Complete Multi-Select Example

Component TS (app.component.ts)

import { Component, signal } from '@angular/core';
import { form, required } from '@angular/forms/signals';
import { AdMultiSelect, AdMultiSelectOption } from 'ng-adflowtail/ad-multi-select';

interface PermissionForm {
  permissions: string[];
}

@Component({
  selector: 'app-permission-manager',
  standalone: true,
  imports: [AdMultiSelect],
  templateUrl: './app.component.html'
})
export class AppPermissionManager {
  // Brand color customization
  protected readonly themeColor = signal<string>('#0d9488'); // Custom Teal

  // Local state container
  protected readonly formModel = signal<PermissionForm>({
    permissions: ['read']
  });

  // Signal forms validation & controller
  protected readonly userForm = form(this.formModel, (path) => {
    required(path.permissions, { message: 'Please select at least one permission.' });
  });

  // Available options
  protected readonly permissionOptions: AdMultiSelectOption[] = [
    { label: 'Read Access', value: 'read' },
    { label: 'Write Access', value: 'write' },
    { label: 'Delete Actions', value: 'delete' },
    { label: 'Super Admin', value: 'admin' }
  ];

  protected onSubmit(event: Event) {
    event.preventDefault();
    if (this.userForm.valid()) {
      console.log('Submitting verified model state:', this.formModel());
    } else {
      this.userForm.markAsTouched();
      console.warn('Form validation failed.');
    }
  }
}

Component HTML Template (app.component.html)

<div class="max-w-md p-6 bg-white dark:bg-gray-800 rounded-xl shadow border border-gray-200 dark:border-gray-700">
  <form (submit)="onSubmit($event)" class="space-y-4">
    <div>
      <label class="block mb-2 text-sm font-semibold text-gray-900 dark:text-white">
        Role Permissions
      </label>
      
      <!-- Multi Select Binding -->
      <ad-multi-select
        [formField]="userForm.permissions"
        [options]="permissionOptions"
        [primaryColor]="themeColor()"
        placeholder="Select key permissions"
        errorMessage="At least one permission is mandatory"
      />
    </div>

    <!-- Active State Inspector -->
    <div class="p-3 bg-gray-50 dark:bg-gray-900/50 rounded-lg text-xs">
      <p class="font-semibold text-gray-500">Live Form Value:</p>
      <code class="text-indigo-600 dark:text-indigo-400">{{ formModel() | json }}</code>
    </div>

    <button type="submit" class="w-full text-white bg-blue-600 hover:bg-blue-700 font-medium rounded-lg text-sm px-5 py-2.5">
      Save Rules
    </button>
  </form>
</div>

Component: AdDataTable

The AdDataTable component (ng-adflowtail/ad-data-table) is a high-performance, feature-rich responsive table widget that simplifies handling large datasets.

Key Features

  • Dynamic Columns: Configurable fields with automated rendering based on data types (text, number, date, currency pre-formatted for SAR, status, progress, action, template).
  • Flexible Row Actions: Built-in row buttons (EDIT, DELETE, VIEW) with custom text, colors, visibility conditions, and disable state callbacks per row.
  • Built-in Global Search: Instant master text filter across row records.
  • Advanced Column Filters: Supports field-by-field searching with specific widgets (text, select, multiSelect, date, dateRange, number, autocomplete).
  • Paging Controls: Custom pagination bar showing start/end ranges, previous/next controls, and customizable page size selections.

Data Table API Spec

@Input() Bindings

| Input | Type | Default | Description | | :--- | :--- | :--- | :--- | | title | string | '' | Headline shown above the table card. | | data | any[] | [] | Array of data records to display. | | tableStyle | string | — | Custom inline styling or CSS classes applied directly to the outer wrapper. | | config | TableConfig | See defaults below | Master schema and controls governing column capabilities and pagination. | | primaryColor | string | — | Overrides default branding colors with computed dynamic CSS colors. |

Default TableConfig Object:

{
  columns: [],
  itemsPerPage: 10,
  showPagination: true,
  showPageSize: true,
  pageSizeOptions: [5, 10, 25, 50],
  showSearch: true,
  searchPlaceholder: 'Search...',
  showActions: false,
  showColumnSearch: false,
  rowDensity: 'default'
}

@Output() Events

| Event | Payload Type | Description | | :--- | :--- | :--- | | pageChange | number | Emits the new page number selected by the user. | | pageSizeChange | number | Emits the new page size count chosen in the dropdown. | | rowClick | any | Emits the raw record object when a row is clicked. | | actionClick | { action: DataTableButtonAction; item: any } | Triggered when a predefined action button in a row is clicked. | | searchChange | string | Emits the global search string entered by the user. | | columnSearchChange | ColumnSearchEvent | Triggered when any column-specific search input values are modified. |


Data Table Types & Interfaces

export type DataTableButtonAction = 'EDIT' | 'DELETE' | 'VIEW';

export interface TableColumn {
  field: string;
  header: string;
  type?: 'text' | 'number' | 'date' | 'currency' | 'status' | 'action' | 'template' | 'progress';
  format?: string;
  actionTemplate?: (item: any) => string;
  statusColors?: { [key: string]: string };
  progressConfig?: {
    showPercentage?: boolean;
    color?: 'brand' | 'success' | 'danger' | 'warning' | 'dark';
    autoColor?: boolean;
    size?: 'sm' | 'md' | 'lg';
    width?: 'sm' | 'md' | 'lg' | 'xl' | 'full';
  };
  searchable?: boolean;
  searchPlaceholder?: string;
  searchType?: 'text' | 'select' | 'multiSelect' | 'date' | 'dateRange' | 'number' | 'autocomplete';
  searchOptions?: Array<{ value: any; label: string }>;
  multiSelectConfig?: {
    maxItems?: number;
    showSelectAll?: boolean;
  };
  autocompleteConfig?: {
    apiUrl?: string;
    debounceTime?: number;
    minChars?: number;
    valueField?: string;
    labelField?: string;
    customSearchFn?: (searchTerm: string) => Promise<Array<{ value: any; label: string }>>;
  };
  actions?: TableAction[];
}

export interface TableAction {
  id: DataTableButtonAction;
  label: string;
  icon?: string;
  color?: 'primary' | 'success' | 'danger' | 'warning' | 'info' | 'secondary';
  className?: string;
  visible?: (item: any) => boolean;
  disabled?: (item: any) => boolean;
  tooltip?: string;
}

export interface TableConfig {
  columns: TableColumn[];
  totalCount?: number;
  currentPage?: number;
  pageSize?: number;
  itemsPerPage?: number;
  showPagination?: boolean;
  showPageSize?: boolean;
  pageSizeOptions?: number[];
  showSearch?: boolean;
  searchPlaceholder?: string;
  showActions?: boolean;
  showColumnSearch?: boolean;
  rowDensity?: 'compact' | 'default' | 'comfortable';
}

export interface ColumnSearchEvent {
  column: string;
  value: any;
  field: string;
  searchType: string;
}

Progress Column Type

Use type: 'progress' for numeric fields (0–100) that should render as a Flowbite progress bar. The bar uses Angular property binding for width, so it works reliably unlike template columns which use [innerHTML] (Angular strips inline style attributes from sanitized HTML).

{
  field: 'completion',
  header: 'Completion',
  type: 'progress',
  progressConfig: {
    showPercentage: true,  // Show percentage before the bar (default: true)
    autoColor: true,       // Green >= 70%, amber >= 40%, red below (default: false)
    color: 'brand',        // Fixed color when autoColor is false
    size: 'md',            // Bar height: 'sm' | 'md' | 'lg'
    width: 'lg',           // Bar track width: 'sm' | 'md' | 'lg' | 'xl' | 'full' (default: 'full')
  },
}

Width presets (bar track only):

| Value | Approx. width | | :--- | :--- | | sm | 80px | | md | 128px | | lg | 192px | | xl | 256px | | full | Stretches to fill remaining cell space |

Note: Do not use type: 'template' with HTML strings for progress indicators — inline style="width: X%" is removed by Angular's HTML sanitizer.

Row Density

Control vertical and horizontal padding for header and body cells via rowDensity on TableConfig:

| Value | Cell padding | Use case | | :--- | :--- | :--- | | compact | px-4 py-2 | Dense data views, more rows on screen | | default | px-6 py-4 | Standard spacing (default) | | comfortable | px-8 py-6 | Easier scanning, touch-friendly layouts |

const tableConfig: TableConfig = {
  columns: [...],
  rowDensity: 'compact',
};

Complete Data Table Example

Component TS (app.component.ts)

import { Component, computed, OnInit, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
import { AdDataTable, TableConfig, ColumnSearchEvent } from 'ng-adflowtail/ad-data-table';

interface Employee {
  id: number;
  name: string;
  email: string;
  department: string;
  salary: number;
  hireDate: string;
  status: 'Active' | 'On Leave' | 'Terminated';
}

@Component({
  selector: 'app-dashboard-table',
  standalone: true,
  imports: [CommonModule, AdDataTable],
  templateUrl: './app.component.html'
})
export class AppDashboardTable implements OnInit {
  protected readonly brandColor = signal<string>('#7c3aed'); // Violet theme

  // Datastore
  protected readonly employees = signal<Employee[]>([]);

  // Filtering, Search and Pagination states
  protected readonly currentPage = signal<number>(1);
  protected readonly pageSize = signal<number>(5);
  protected readonly globalSearch = signal<string>('');
  protected readonly columnFilters = signal<Record<string, string>>({});

  ngOnInit() {
    // Bootstrap dummy data
    this.employees.set([
      { id: 101, name: 'Ahmad Al-Harbi', email: '[email protected]', department: 'Engineering', salary: 18500, hireDate: '2023-04-12', status: 'Active' },
      { id: 102, name: 'Sarah Connor', email: '[email protected]', department: 'Operations', salary: 14200, hireDate: '2021-08-19', status: 'Active' },
      { id: 103, name: 'John Doe', email: '[email protected]', department: 'Engineering', salary: 12000, hireDate: '2025-01-05', status: 'On Leave' },
      { id: 104, name: 'Lina Gamil', email: '[email protected]', department: 'Marketing', salary: 9800, hireDate: '2022-11-01', status: 'Active' },
      { id: 105, name: 'Michael Scott', email: '[email protected]', department: 'Sales', salary: 11500, hireDate: '2019-03-15', status: 'Terminated' }
    ]);
  }

  // Reactive computed: filters records
  protected readonly filteredEmployees = computed(() => {
    let list = this.employees();
    const search = this.globalSearch().toLowerCase();
    const colFilters = this.columnFilters();

    // 1. Process Global Master Search
    if (search) {
      list = list.filter(item => 
        item.name.toLowerCase().includes(search) || 
        item.email.toLowerCase().includes(search) ||
        item.department.toLowerCase().includes(search)
      );
    }

    // 2. Process Specific Column Searches
    for (const [key, value] of Object.entries(colFilters)) {
      if (value) {
        list = list.filter(item => {
          const itemVal = (item as any)[key]?.toString().toLowerCase();
          return itemVal?.includes(value.toLowerCase());
        });
      }
    }

    return list;
  });

  // Reactive computed: extracts sliced pagination view
  protected readonly paginatedEmployees = computed(() => {
    const list = this.filteredEmployees();
    const start = (this.currentPage() - 1) * this.pageSize();
    const end = start + this.pageSize();
    return list.slice(start, end);
  });

  protected readonly totalCount = computed(() => this.filteredEmployees().length);

  // Dynamic Reactive Table Config
  protected readonly tableConfig = computed<TableConfig>(() => ({
    columns: [
      { field: 'id', header: 'ID', type: 'number' },
      { field: 'name', header: 'Full Name', type: 'text', searchable: true },
      { field: 'email', header: 'Email Address', type: 'text', searchable: true },
      { 
        field: 'department', 
        header: 'Department', 
        type: 'text', 
        searchable: true,
        searchType: 'select',
        searchOptions: [
          { value: 'Engineering', label: 'Engineering' },
          { value: 'Operations', label: 'Operations' },
          { value: 'Marketing', label: 'Marketing' },
          { value: 'Sales', label: 'Sales' }
        ]
      },
      { field: 'salary', header: 'Base Salary', type: 'currency' },
      { field: 'hireDate', header: 'Hire Date', type: 'date' },
      { 
        field: 'status', 
        header: 'Status', 
        type: 'status', 
        searchable: true,
        searchType: 'select',
        searchOptions: [
          { value: 'Active', label: 'Active' },
          { value: 'On Leave', label: 'On Leave' },
          { value: 'Terminated', label: 'Terminated' }
        ],
        statusColors: {
          'Active': 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400',
          'On Leave': 'bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-400',
          'Terminated': 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400'
        }
      },
      {
        field: 'actions',
        header: 'Actions',
        type: 'action',
        actions: [
          { id: 'VIEW', label: 'View Profile', color: 'primary' },
          { 
            id: 'DELETE', 
            label: 'Remove', 
            color: 'danger', 
            disabled: (row) => row.status === 'Terminated' // Disable delete button for terminated staff
          }
        ]
      }
    ],
    showPagination: true,
    showPageSize: true,
    pageSizeOptions: [5, 10, 20],
    showSearch: true,
    searchPlaceholder: 'Search directory...',
    showActions: true,
    showColumnSearch: true,
    totalCount: this.totalCount(),
    currentPage: this.currentPage(),
    pageSize: this.pageSize()
  }));

  // Handle Reactive Handlers
  protected onPageChange(page: number) {
    this.currentPage.set(page);
  }

  protected onPageSizeChange(size: number) {
    this.pageSize.set(size);
    this.currentPage.set(1); // Reset to first page
  }

  protected onGlobalSearch(searchTerm: string) {
    this.globalSearch.set(searchTerm);
    this.currentPage.set(1);
  }

  protected onColumnSearch(event: ColumnSearchEvent) {
    this.columnFilters.update(filters => ({
      ...filters,
      [event.field]: event.value
    }));
    this.currentPage.set(1);
  }

  protected onActionTriggered(event: { action: string; item: any }) {
    console.log(`Action triggered: [${event.action}] on record ID:`, event.item.id);
  }

  protected onRowSelected(row: any) {
    console.log('Row record clicked:', row);
  }
}

Component HTML Template (app.component.html)

<div class="container mx-auto p-4">
  <ad-data-table
    title="Staff Directory"
    [data]="paginatedEmployees()"
    [config]="tableConfig()"
    [primaryColor]="brandColor()"
    (pageChange)="onPageChange($event)"
    (pageSizeChange)="onPageSizeChange($event)"
    (searchChange)="onGlobalSearch($event)"
    (columnSearchChange)="onColumnSearch($event)"
    (actionClick)="onActionTriggered($event)"
    (rowClick)="onRowSelected($event)"
  />
</div>

Component: AdAutocomplete

The AdAutocomplete component (ng-adflowtail/ad-autocomplete) is a searchable typeahead input built on Angular Aria (@angular/aria) combobox and listbox primitives. It integrates with Angular Signal Forms and delegates data fetching to the parent via a debounced (search) output — making it ideal for remote API lookups.

Key Features

  • Signal Forms: Binds to form fields via the formField input parameter.
  • Debounced Search: Emits (search) after 500ms of inactivity so parents can call APIs efficiently.
  • Angular Aria: Uses accessible ngCombobox, ngListbox, and ngOption directives for keyboard navigation and screen reader support.
  • Parent-Controlled Options: You supply [options] from any data source (REST API, GraphQL, local filter, etc.).
  • Rich Option Display: Each option shows a title and subtitle line; attach arbitrary payload via optional data.
  • Validation-Driven: Applies red borders and error messages when the bound signal form is touched and invalid.
  • Consistent Theming: Shares the same Tailwind design tokens and primaryColor host directive as other widgets.

Autocomplete API Spec

@Input() Bindings

| Input | Type | Default | Description | | :--- | :--- | :--- | :--- | | formField (Required) | FieldTree<string> | — | The signal forms field node to bind (e.g. myForm.selectedUser). | | options | AdAutocompleteOption<T>[] | [] | Suggestions rendered in the dropdown popup. | | placeholder | string | 'Search...' | Placeholder text shown in the search input. | | disabled | boolean | false | Disables user interaction with the input. | | errorMessage | string | '' | Custom message shown underneath the input on validation failure. | | primaryColor | string | — | Overrides default branding colors with computed dynamic CSS colors. |

@Output() Events

| Event | Payload Type | Description | | :--- | :--- | :--- | | search | string | Emits the debounced search query (min 1 character). Handle this in the parent to fetch or filter options. |

Model Interfaces

export interface AdAutocompleteOption<T> {
  id: string;
  title: string;
  subtitle: string;
  data?: T;
}

When a user selects an option, the component sets the bound form field value to option.title and closes the popup.


Complete Autocomplete Example (DummyJSON API)

This example searches users from DummyJSON and displays name, email, and job title in the dropdown.

Component TS (app.component.ts)

import { Component, computed, signal } from '@angular/core';
import { form, required, submit } from '@angular/forms/signals';
import { AdAutocomplete, AdAutocompleteOption } from 'ng-adflowtail/ad-autocomplete';

interface DummyJsonUser {
  id: number;
  firstName: string;
  lastName: string;
  email: string;
  company: { title: string; department: string };
}

interface UserSearchForm {
  selectedUser: string;
}

@Component({
  selector: 'app-user-search',
  standalone: true,
  imports: [AdAutocomplete],
  templateUrl: './app.component.html',
})
export class AppUserSearch {
  protected readonly themeColor = signal<string>('#0284c7');

  protected readonly formModel = signal<UserSearchForm>({ selectedUser: '' });

  protected readonly userForm = form(this.formModel, (path) => {
    required(path.selectedUser, { message: 'Please select a user from search results.' });
  });

  protected readonly autocompleteOptions = signal<AdAutocompleteOption<DummyJsonUser>[]>([]);
  protected readonly loading = signal(false);

  protected readonly selectedUser = computed(() => {
    const title = this.formModel().selectedUser;
    return this.autocompleteOptions().find((o) => o.title === title)?.data ?? null;
  });

  protected async onUserSearch(query: string): Promise<void> {
    this.loading.set(true);
    try {
      const res = await fetch(`https://dummyjson.com/users/search?q=${encodeURIComponent(query)}`);
      const data = await res.json();
      this.autocompleteOptions.set(
        data.users.map((user: DummyJsonUser) => ({
          id: String(user.id),
          title: `${user.firstName} ${user.lastName}`,
          subtitle: `${user.email} · ${user.company.title}`,
          data: user,
        })),
      );
    } catch {
      this.autocompleteOptions.set([]);
    } finally {
      this.loading.set(false);
    }
  }

  protected onSubmit(event: Event): void {
    event.preventDefault();
    submit(this.userForm, async () => {
      console.log('Selected user:', this.selectedUser());
    });
  }
}

Component HTML Template (app.component.html)

<div class="max-w-md p-6 bg-white dark:bg-gray-800 rounded-xl shadow border border-gray-200 dark:border-gray-700">
  <form (submit)="onSubmit($event)" class="space-y-4">
    <div>
      <label class="block mb-2 text-sm font-semibold text-gray-900 dark:text-white">
        Search Users
      </label>

      <ad-autocomplete
        [formField]="userForm.selectedUser"
        [options]="autocompleteOptions()"
        [primaryColor]="themeColor()"
        placeholder="Type to search users (e.g. ah)..."
        errorMessage="Please select a user from search results"
        (search)="onUserSearch($event)"
      />

      @if (loading()) {
        <p class="mt-2 text-xs text-gray-500">Searching...</p>
      }
    </div>

    @if (selectedUser(); as user) {
      <div class="p-3 bg-gray-50 dark:bg-gray-900/50 rounded-lg text-sm">
        <p class="font-semibold">{{ user.firstName }} {{ user.lastName }}</p>
        <p class="text-gray-500">{{ user.email }}</p>
        <p class="text-xs text-gray-400">{{ user.company.title }} · {{ user.company.department }}</p>
      </div>
    }

    <button type="submit" class="w-full text-white bg-sky-600 hover:bg-sky-700 font-medium rounded-lg text-sm px-5 py-2.5">
      Confirm Selection
    </button>
  </form>
</div>

Integration Notes

  • The component does not fetch data internally — listen to (search) and update [options] in the parent.
  • Debounce is fixed at 500ms inside the component; only non-empty queries trigger (search).
  • Store the full record in data on each AdAutocompleteOption to access rich details after selection without a second API call.
  • Requires @angular/aria and @angular/cdk as peer dependencies — install them separately; they are not bundled with @angular/core.

Adding New Components

If you are a library developer working on the ng-adflowtail monorepo workspace, you can easily add new components using ng-packagr's standard secondary entry points system.

Steps to scaffold a new widget ad-badge

1. Setup Folder Structure

Create a new folder alongside the main src directory in projects/ng-adflowtail/:

projects/ng-adflowtail/
├── src/                      # primary entry point
├── ad-multi-select/          # secondary entry point 1
├── ad-data-table/            # secondary entry point 2
├── ad-autocomplete/          # secondary entry point 3
└── ad-badge/                 # <--- Your new secondary entry point
    ├── ng-package.json
    └── src/
        ├── public-api.ts
        └── lib/
            ├── ad-badge.ts
            ├── ad-badge.html
            └── ad-badge.css

2. Create projects/ng-adflowtail/ad-badge/ng-package.json

Define the secondary packaging schema pointing back to the library:

{
  "lib": {
    "entryFile": "src/public-api.ts"
  }
}

3. Create projects/ng-adflowtail/ad-badge/src/public-api.ts

Export your exports correctly:

export * from './lib/ad-badge';

4. Configure compiler paths mapping in Root tsconfig.json

Allow main apps in the monorepo to resolve imports locally during development:

"paths": {
  "ng-adflowtail": ["./projects/ng-adflowtail/src/public-api.ts"],
  "ng-adflowtail/ad-data-table": ["./projects/ng-adflowtail/ad-data-table/src/public-api.ts"],
  "ng-adflowtail/ad-multi-select": ["./projects/ng-adflowtail/ad-multi-select/src/public-api.ts"],
  "ng-adflowtail/ad-autocomplete": ["./projects/ng-adflowtail/ad-autocomplete/src/public-api.ts"],
  "ng-adflowtail/ad-badge": ["./projects/ng-adflowtail/ad-badge/src/public-api.ts"]  // Add this
}

Build & Publish

Compile the entire package

Use the Angular CLI to run a production build. Under the hood, ng-packagr compiles all the secondary entry points in sequence:

ng build ng-adflowtail

The compiled distributable files are stored under /dist/ng-adflowtail.

Publish to NPM

Navigate to the build directory and run the publish command:

cd dist/ng-adflowtail
npm publish --access public

License

MIT © Ahmad Workspace. Fully custom-crafted components built for Angular 22 and styled with Tailwind CSS & Flowbite.