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
- Prerequisites & Installation
- Tailwind CSS Configuration
- Architecture & Imports
- Branding & Custom Theming
- Component: AdMultiSelect
- Component: AdDataTable
- Component: AdAutocomplete
- Adding New Components (Library Developers)
- 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/ariais only published for Angular 22+. Consumer apps on Angular 21 must upgrade to Angular 22 before usingad-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
formFieldinput 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
Escapekeyboard key handling to dismiss the dropdown panel safely. - Validation-Driven: Automatically applies red borders and error displays when the bound signal form is
touchedandinvalid.
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,currencypre-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
formFieldinput parameter. - Debounced Search: Emits
(search)after 500ms of inactivity so parents can call APIs efficiently. - Angular Aria: Uses accessible
ngCombobox,ngListbox, andngOptiondirectives 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
titleandsubtitleline; attach arbitrary payload via optionaldata. - Validation-Driven: Applies red borders and error messages when the bound signal form is
touchedandinvalid. - Consistent Theming: Shares the same Tailwind design tokens and
primaryColorhost 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
dataon eachAdAutocompleteOptionto access rich details after selection without a second API call. - Requires
@angular/ariaand@angular/cdkas 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.css2. 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-adflowtailThe 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 publicLicense
MIT © Ahmad Workspace. Fully custom-crafted components built for Angular 22 and styled with Tailwind CSS & Flowbite.
