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
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 lefttop-center: Above anchor, centeredtop-end: Above anchor, aligned to the rightbottom-start: Below anchor, aligned to the leftbottom-center: Below anchor, centered (default)bottom-end: Below anchor, aligned to the rightleft-start: Left of anchor, aligned to the topleft-center: Left of anchor, centeredleft-end: Left of anchor, aligned to the bottomright-start: Right of anchor, aligned to the topright-center: Right of anchor, centeredright-end: Right of anchor, aligned to the bottomtop-left: Top left cornertop-right: Top right cornerbottom-left: Bottom left cornerbottom-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.