Toolbar
Beta

Overlay

The FktOverlay service provides a powerful and flexible system for creating positioned overlays in your Angular applications. Built with modern Angular signals and reactive patterns, it supports dynamic positioning, intelligent repositioning, and seamless data binding between parent and overlay components.

Import

import {FktOverlayService} from "frakton-ng/overlay";

Simple

Basic overlay implementation with simple content positioning.

Dropdown-style overlay with menu items and selection functionality.

Form

Overlay containing form elements for input and data collection.

Custom tooltip

Custom tooltip implementation using overlay service with rich content.

Interactive

Interactive overlay with complex content and user interactions.

Shared State


Counter: Items:
Changes made in overlays will update this shared state automatically.

Overlay

Key Features

  • Advanced TypeScript Inference: Automatic type inference for overlay data based on component signal inputs, outputs, and models
  • Signal-Based Architecture: Built for Angular's new signal APIs with full reactivity support
  • Component Agnostic Design: Overlay components work anywhere - in overlays, dialogs, or standalone
  • Intelligent Positioning: Automatic positioning with 16 different anchor positions and smart fallbacks
  • Dynamic Styling: Real-time style updates and theme-aware overlays
  • Memory Safe: Automatic lifecycle management with proper cleanup and destroy callbacks
  • Performance Optimized: Efficient DOM management and minimal re-renders
  • Advanced Control: Outside click handling, auto-repositioning, and custom overlay IDs
  • Responsive Design: Smart viewport detection and mobile-friendly positioning

Types

import {ComponentRef, ElementRef, Type} from "@angular/core";
import {FktGeometryPosition} from "frakton-ng/core";


export interface FktOverlayOptions<T> {
    anchorElementRef?: ElementRef<HTMLElement>;
    component: Type<T>;
    data: FktReactiveComponentData<T>;
    panelOptions?: {
        overflow?: 'hidden' | 'visible' | 'scroll' | 'auto';
        id?: string;
        maxHeight?: string;
        minWidth?: string;
        borderRadius?: string;
        backgroundColor?: string;
        width?: string;
        padding?: string;
        boxShadow?: string;
        position?: FktGeometryPosition;
        disableAutoReposition?: boolean;
        disableAutoClose?: boolean;
        onOutsideClick?: (element: HTMLElement) => void;
    }
}

export interface FktOverlayRef<T> {
    componentRef: ComponentRef<T>;              // Reference to the overlay component
    close: () => void;                          // Method to close the overlay
}

Positioning Options

The overlay supports intelligent positioning with the following options:

  • top-start: Above anchor, aligned to the left
  • top-center: Above anchor, centered
  • top-end: Above anchor, aligned to the right
  • bottom-start: Below anchor, aligned to the left
  • bottom-center: Below anchor, centered (default)
  • bottom-end: Below anchor, aligned to the right
  • left-start: Left of anchor, aligned to the top
  • left-center: Left of anchor, centered
  • left-end: Left of anchor, aligned to the bottom
  • right-start: Right of anchor, aligned to the top
  • right-center: Right of anchor, centered
  • right-end: Right of anchor, aligned to the bottom
  • top-left: Top left corner
  • top-right: Top right corner
  • bottom-left: Bottom left corner
  • bottom-right: Bottom right corner

TypeScript Inference & Component Design

Required: Signal-Based Component APIs

Important: The FktOverlay service requires components to use Angular's signal-based APIs (input(), output(), model()) for proper TypeScript inference. Decorator-based components (@Input, @Output) are not supported due to TypeScript compilation limitations.

    // ✅ REQUIRED: Signal-based component (fully supported)
@Component({
    selector: 'my-overlay-component',
    template: `...`
})
export class MyOverlayComponent {
    // Signal inputs - Required for TypeScript inference
    title = input('Default Title');
    userData = input<User>();
    config = input<OverlayConfig>({theme: 'light'});

    // Signal outputs - Required for callback type inference
    onSave = output<FormData>();
    onCancel = output<void>();
    onAction = output<{ type: string, payload: any }>();

    // Signal models - Required for two-way binding
    counter = model(0);
    items = model<string[]>([]);
}

// Perfect TypeScript inference when using signal-based components:
const overlayRef = this.overlayService.open({
    anchorElementRef: anchor,
    component: MyOverlayComponent,
    data: {
        // ✅ Full IntelliSense for all inputs
        title: 'Custom Title',          // string - auto-completed
        userData: this.currentUser,     // User type enforced
        config: {theme: 'dark'},      // OverlayConfig type enforced

        // ✅ TypeScript automatically infers callback parameter types
        onSave: (data) => {             // data is automatically typed as FormData
            console.log('Form saved:', data);
            overlayRef.close();
        },
        onCancel: () => {               // void - no parameters
            overlayRef.close();
        },
        onAction: (event) => {          // event is automatically typed as { type: string, payload: any }
            this.handleAction(event.type, event.payload);
        },

        // ✅ Two-way signal binding with type safety
        counter: this.sharedCounter,    // WritableSignal<number>
        items: this.sharedItems,        // WritableSignal<string[]>
    }
});

// ❌ NOT SUPPORTED: Decorator-based components won't compile
@Component({
    selector: 'decorator-component',
    template: `...`
})
export class DecoratorComponent {
    @Input() title: string = 'Default';         // ❌ Won't work
    @Input() userData?: User;                   // ❌ Won't work
    @Output() onSave = new EventEmitter<FormData>(); // ❌ Won't work
}

// This will result in TypeScript compilation errors:
const willNotCompile = this.overlayService.open({
    component: DecoratorComponent,  // ❌ TypeScript error
    data: { /* ... */}             // ❌ No inference available
});

Component Agnostic Design Benefits

Using signal-based APIs makes your overlay components truly reusable across different contexts:


@Component({
    selector: 'user-profile-form',
    template: `
			<div class="container">
				<h3>{{ title() }}</h3>
				<form (ngSubmit)="handleSubmit()">
					<input [(ngModel)]="name" placeholder="Name" [readonly]="readonly()" />
					<input [(ngModel)]="email" placeholder="Email" [readonly]="readonly()" />
					<div class="container__actions">
						@if (!readonly()) {
							<button type="submit" [disabled]="!isValid()">Save</button>
							<button type="button" (click)="handleCancel()">Cancel</button>
						}
					</div>
				</form>
			</div>
		`,
})
export class UserProfileFormComponent {
    // Signal inputs - work everywhere
    title = input('Edit Profile');
    initialName = input('');
    initialEmail = input('');
    readonly = input(false);

    // Signal outputs - work everywhere
    onSubmit = output<{ name: string, email: string }>();
    onCancel = output<void>();

    // Internal component state
    name = signal('');
    email = signal('');

    ngOnInit() {
        this.name.set(this.initialName());
        this.email.set(this.initialEmail());
    }

    isValid = computed(() =>
        this.name().trim().length > 0 &&
        this.email().includes('@')
    );

    handleSubmit() {
        if (this.isValid()) {
            this.onSubmit.emit({
                name: this.name(),
                email: this.email()
            });
        }
    }

    handleCancel() {
        this.onCancel.emit();
    }
}

// This component works seamlessly in ALL contexts:

// 1. ✅ In FktOverlay (with automatic type inference)
this.overlayService.open({
    component: UserProfileFormComponent,
    data: {
        title: 'Create New User',
        initialName: '',
        initialEmail: '',
        readonly: false,
        onSubmit: (userData) => {
            // userData is automatically typed as { name: string, email: string }
            this.userService.create(userData);
            overlayRef.close();
        },
        onCancel: () => overlayRef.close()
    }
});

// 2. ✅ In other overlay systems (like CDK Dialog)
this.dialog.open(UserProfileFormComponent, {
    data: {
        title: 'Edit User',
        initialName: user.name,
        initialEmail: user.email,
        readonly: !canEdit,
        onSubmit: (userData) => this.updateUser(userData),
        onCancel: () => dialogRef.close()
    }
});

// 3. ✅ As standalone component in templates
@Component({
    template: `
			<user-profile-form
				[title]="'User Settings'"
				[initialName]="currentUser.name"
				[initialEmail]="currentUser.email"
				[readonly]="!canEditProfile"
				(onSubmit)="updateProfile($event)"
				(onCancel)="navigateBack()"
			/>
		`
})
export class ProfilePageComponent { /* ... */
}

Migration from Decorator Components

If you have existing decorator-based components, here's how to migrate them:

    // Before: Decorator-based (won't work with FktOverlay)
@Component({ /* ... */})
export class OldComponent {
    @Input() title: string = '';
    @Input() data?: MyData;
    @Input() config: Config = {theme: 'light'};
    @Output() save = new EventEmitter<FormData>();
    @Output() cancel = new EventEmitter<void>();
}

// After: Signal-based (works perfectly with FktOverlay)
@Component({ /* ... */})
export class NewComponent {
    title = input('');
    data = input<MyData>();
    config = input<Config>({theme: 'light'});
    save = output<FormData>();
    cancel = output<void>();

    // Migration tip: Keep the same logic, just change the API
    handleSave() {
        const formData = this.buildFormData();
        this.save.emit(formData); // Same emit pattern
    }
}

Advanced Features & Tips

Reactive Data Binding (Optional)

While not required, signals enable powerful reactive patterns when you need real-time updates:

    // Option 1: Static data (simple and fine for most cases)
const staticOptions = {
    data: {
        title: 'Static Title',
        message: `This won't change after overlay opens`
    }
}

// Option 2: Reactive data (when you need live updates)
const reactiveOptions = {
    data: {
        title: this.dynamicTitle, // Signal that can update
        message: this.liveMessage, // Updates reflect in overlay
        counter: this.sharedCounter // Two-way binding with models
    }
}

Outside Click Customization

Customize behavior when users click outside the overlay:

    this.open({
    panelOptions: {
        outsideClick: (clickedElement: HTMLElement) => {
            console.log('Clicked outside:', clickedElement);
            // Add custom logic before closing
            if (this.hasUnsavedChanges()) {
                if (confirm('Discard changes?')) {
                    overlayRef.close();
                }
            } else {
                overlayRef.close();
            }
        }
    }
})

Positioning Control

Fine-tune overlay positioning behavior:

const options = {
    panelOptions: {
        position: 'bottom-start',
        disableAutoReposition: true, // Stays in position even if off-screen
        width: 'fit-content', // Adapts to content size
        maxHeight: '400px', // Prevents oversized overlays
        overflow: 'auto' // Adds scrolling when needed
    }
}

Managing Multiple Overlays

Track and control multiple overlays independently:

    class MyComponent {
    private activeOverlays = new Map<string, FktOverlayRef<any>>();

    openNamedOverlay(id: string, config: any) {
        // Close existing overlay with same ID
        this.closeOverlay(id);

        const overlayRef = this.overlayService.open({
            ...config,
            panelOptions: {...config.panelOptions, id}
        });

        this.activeOverlays.set(id, overlayRef);
        return overlayRef;
    }

    closeOverlay(id: string) {
        const overlay = this.activeOverlays.get(id);
        if (overlay) {
            overlay.close();
            this.activeOverlays.delete(id);
        }
    }

    closeAllOverlays() {
        this.activeOverlays.forEach(overlay => overlay.close());
        this.activeOverlays.clear();
    }
}

Best Practices

Data Binding Approaches

  • Static data: Perfect for simple overlays that don't need updates after opening
  • Reactive data: Use signals/models when you need live updates between parent and overlay
  • Callback functions: Handle overlay outputs by passing callback functions in the data object
  • Mixed approach: Combine static and reactive data as needed for your use case

Lifecycle Management

  • Store references: Keep overlay references when you need programmatic control
  • Clean up properly: Close overlays in component destruction to prevent memory leaks
  • Use unique IDs: Assign custom IDs when managing multiple overlays independently
  • Handle edge cases: Consider what happens when users navigate away or refresh

Positioning & Responsiveness

  • Test different viewports: Ensure overlays work well on various screen sizes
  • Consider anchor positioning: Choose appropriate positions based on content and layout
  • Use auto-repositioning wisely: Enable by default, disable only when you need fixed positioning
  • Handle overflow: Set appropriate maxHeight and overflow for content that might be large

Performance Considerations

  • Limit concurrent overlays: Avoid opening too many overlays simultaneously
  • Optimize component templates: Keep overlay component templates lightweight
  • Use appropriate sizing: Prefer 'fit-content' and specific sizes over 'auto' when possible
  • Consider lazy loading: For complex overlay content, consider loading data only when needed

Accessibility

  • Overlays are properly positioned to avoid viewport edges
  • Keyboard navigation is supported within overlay content
  • Focus management is handled automatically
  • ARIA attributes should be added to overlay content as needed

Performance

  • Overlays use efficient positioning calculations
  • Components are properly destroyed when overlays close
  • Memory leaks are prevented through proper cleanup
  • Positioning updates only when necessary

Common Use Cases

  • Tooltips: Perfect for contextual help and information displays.
  • Dropdown Menus: Ideal for action menus and option selectors.
  • Popovers: Great for detailed information without navigation.
  • Form Helpers: Useful for inline form assistance and validation messages.
  • Context Menus: Right-click context menus and contextual actions.
  • Date Pickers: Custom date selection overlays.
  • Color Pickers: Color selection interfaces.
  • Search Results: Autocomplete and search suggestion displays.