or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

http-client.mdi18n.mdindex.mdlocale.mdmenu.mdpipes.mdsettings.mdui-helpers.mdutilities.md
tile.json

utilities.mddocs/

Utility Services

Additional utility services for responsive design, RTL support, title management, page preloading, and other common application utilities for Angular development.

Capabilities

ResponsiveService

Service for generating responsive CSS classes and handling responsive design utilities.

/**
 * Responsive design utility service
 * Provides helpers for responsive CSS class generation
 */
class ResponsiveService {
  /**
   * Generate responsive CSS classes based on count and default column size
   * @param count - Number of items or columns
   * @param defaultCol - Default column size (optional)
   * @returns Array of responsive CSS class names
   */
  genCls(count: number, defaultCol?: number): string[];
}

/** Maximum responsive breakpoint count */
const REP_MAX = 6;
/** Maximum span size for grid systems */
const SPAN_MAX = 24;
/** Responsive type definition */
type REP_TYPE = 1 | 2 | 3 | 4 | 5 | 6;

Usage Examples:

import { Component, inject } from "@angular/core";
import { ResponsiveService } from "@delon/theme";

@Component({
  selector: "responsive-grid",
  template: `
    <div class="grid-container">
      <!-- Dynamic responsive classes -->
      <div 
        *ngFor="let item of items; let i = index"
        [class]="getResponsiveClasses(items.length).join(' ')"
      >
        Item {{ i + 1 }}
      </div>
    </div>
    
    <!-- Card grid with responsive sizing -->
    <div class="card-grid">
      <div 
        *ngFor="let card of cards"
        [ngClass]="cardClasses"
      >
        <div class="card">
          <h3>{{ card.title }}</h3>
          <p>{{ card.content }}</p>
        </div>
      </div>
    </div>
    
    <!-- Dashboard widgets -->
    <div class="dashboard">
      <div 
        *ngFor="let widget of widgets"
        [ngClass]="getWidgetClasses(widget.size)"
      >
        <app-widget [data]="widget"></app-widget>
      </div>
    </div>
  `,
  styles: [`
    .grid-container {
      display: flex;
      flex-wrap: wrap;
      gap: 16px;
    }
    
    .card-grid {
      display: grid;
      gap: 16px;
    }
    
    .dashboard {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
      gap: 20px;
    }
    
    /* Responsive classes */
    .col-xs-12 { width: 100%; }
    .col-sm-6 { width: 50%; }
    .col-md-4 { width: 33.333%; }
    .col-lg-3 { width: 25%; }
    .col-xl-2 { width: 16.666%; }
  `]
})
export class ResponsiveGridComponent {
  responsiveService = inject(ResponsiveService);
  
  items = Array.from({ length: 8 }, (_, i) => ({ id: i + 1, name: `Item ${i + 1}` }));
  
  cards = [
    { title: 'Card 1', content: 'Content 1' },
    { title: 'Card 2', content: 'Content 2' },
    { title: 'Card 3', content: 'Content 3' },
    { title: 'Card 4', content: 'Content 4' }
  ];
  
  widgets = [
    { title: 'Sales', size: 'large', data: {} },
    { title: 'Users', size: 'medium', data: {} },
    { title: 'Orders', size: 'small', data: {} }
  ];
  
  // Generate responsive classes dynamically
  get cardClasses(): string[] {
    return this.responsiveService.genCls(this.cards.length, 4);
  }
  
  getResponsiveClasses(itemCount: number): string[] {
    return this.responsiveService.genCls(itemCount, 3);
  }
  
  getWidgetClasses(size: string): string[] {
    const sizeMap = { small: 6, medium: 4, large: 2 };
    const colSize = sizeMap[size] || 4;
    return this.responsiveService.genCls(1, colSize);
  }
  
  // Handle responsive breakpoint changes
  onResize() {
    const itemCount = this.getVisibleItemCount();
    const newClasses = this.responsiveService.genCls(itemCount);
    console.log('New responsive classes:', newClasses);
  }
  
  private getVisibleItemCount(): number {
    const width = window.innerWidth;
    if (width < 576) return 1;      // xs
    if (width < 768) return 2;      // sm
    if (width < 992) return 3;      // md
    if (width < 1200) return 4;     // lg
    return 6;                       // xl
  }
}

// Responsive utility functions
export class ResponsiveUtils {
  constructor(private responsiveService: ResponsiveService) {}
  
  // Generate classes for different layout patterns
  getListClasses(itemCount: number): string[] {
    return this.responsiveService.genCls(itemCount, 1); // Single column list
  }
  
  getGridClasses(itemCount: number): string[] {
    return this.responsiveService.genCls(itemCount, 4); // Grid layout
  }
  
  getCardClasses(itemCount: number): string[] {
    return this.responsiveService.genCls(itemCount, 3); // Card layout
  }
  
  // Adaptive class generation based on content
  getAdaptiveClasses(content: any[]): string[] {
    const count = content.length;
    const defaultCol = count <= 2 ? 2 : count <= 6 ? 3 : 4;
    return this.responsiveService.genCls(count, defaultCol);
  }
}

RTLService

Service for managing right-to-left text direction with reactive updates.

/**
 * Right-to-left text direction management service
 * Handles RTL/LTR switching with reactive notifications
 */
class RTLService {
  /** Current text direction */
  dir: Direction;
  /** Next direction (opposite of current) */
  readonly nextDir: Direction;
  /** Observable stream of direction changes */
  readonly change: Observable<Direction>;
  
  /** Toggle between LTR and RTL directions */
  toggle(): void;
}

type Direction = 'ltr' | 'rtl';

Usage Examples:

import { Component, inject, OnInit } from "@angular/core";
import { RTLService, Direction } from "@delon/theme";

@Component({
  selector: "rtl-controls",
  template: `
    <div class="rtl-controls" [dir]="currentDirection">
      <h2>RTL Support Demo</h2>
      
      <div class="direction-info">
        <p>Current Direction: {{ currentDirection }}</p>
        <p>Next Direction: {{ rtlService.nextDir }}</p>
      </div>
      
      <button (click)="toggleDirection()" class="toggle-btn">
        Switch to {{ rtlService.nextDir.toUpperCase() }}
      </button>
      
      <div class="content-demo">
        <div class="navigation">
          <a href="#" class="nav-item">Home</a>
          <a href="#" class="nav-item">About</a>
          <a href="#" class="nav-item">Contact</a>
        </div>
        
        <div class="text-content">
          <p>This content adapts to text direction changes.</p>
          <p>Notice how the layout flows differently in RTL mode.</p>
        </div>
        
        <div class="form-demo">
          <input type="text" placeholder="Enter text here" />
          <button type="submit">Submit</button>
        </div>
      </div>
    </div>
  `,
  styles: [`
    .rtl-controls {
      padding: 20px;
      max-width: 600px;
      margin: 0 auto;
    }
    
    .direction-info {
      background: #f5f5f5;
      padding: 10px;
      border-radius: 4px;
      margin: 10px 0;
    }
    
    .toggle-btn {
      background: #1890ff;
      color: white;
      border: none;
      padding: 8px 16px;
      border-radius: 4px;
      cursor: pointer;
    }
    
    .navigation {
      display: flex;
      gap: 20px;
      margin: 20px 0;
      padding: 10px;
      background: #f9f9f9;
    }
    
    .nav-item {
      text-decoration: none;
      color: #333;
      padding: 5px 10px;
      border-radius: 3px;
    }
    
    .nav-item:hover {
      background: #e6f7ff;
    }
    
    .text-content {
      margin: 20px 0;
      padding: 15px;
      border: 1px solid #ddd;
      border-radius: 4px;
    }
    
    .form-demo {
      display: flex;
      gap: 10px;
      margin: 20px 0;
    }
    
    .form-demo input {
      flex: 1;
      padding: 8px;
      border: 1px solid #ddd;
      border-radius: 4px;
    }
    
    .form-demo button {
      padding: 8px 16px;
      background: #52c41a;
      color: white;
      border: none;
      border-radius: 4px;
      cursor: pointer;
    }
    
    /* RTL-specific styles */
    [dir="rtl"] .navigation {
      flex-direction: row-reverse;
    }
    
    [dir="rtl"] .form-demo {
      flex-direction: row-reverse;
    }
  `]
})
export class RTLControlsComponent implements OnInit {
  rtlService = inject(RTLService);
  currentDirection: Direction = 'ltr';
  
  ngOnInit() {
    // Initialize with current direction
    this.currentDirection = this.rtlService.dir;
    
    // Listen to direction changes
    this.rtlService.change.subscribe(direction => {
      this.currentDirection = direction;
      this.updateDocumentDirection(direction);
      console.log('Direction changed to:', direction);
    });
  }
  
  toggleDirection() {
    this.rtlService.toggle();
  }
  
  private updateDocumentDirection(direction: Direction) {
    // Update document direction
    document.documentElement.dir = direction;
    
    // Update document class for styling
    document.body.classList.toggle('rtl-mode', direction === 'rtl');
    
    // Trigger layout recalculation if needed
    this.triggerLayoutUpdate();
  }
  
  private triggerLayoutUpdate() {
    // Force layout recalculation for components that need it
    setTimeout(() => {
      window.dispatchEvent(new Event('resize'));
    }, 0);
  }
}

// RTL-aware component base class
export abstract class RTLAwareComponent implements OnInit {
  protected rtlService = inject(RTLService);
  protected isRTL = false;
  
  ngOnInit() {
    this.isRTL = this.rtlService.dir === 'rtl';
    
    this.rtlService.change.subscribe(direction => {
      this.isRTL = direction === 'rtl';
      this.onDirectionChange(direction);
    });
  }
  
  protected abstract onDirectionChange(direction: Direction): void;
  
  protected getMarginStart(value: number): { [key: string]: string } {
    const property = this.isRTL ? 'margin-right' : 'margin-left';
    return { [property]: `${value}px` };
  }
  
  protected getMarginEnd(value: number): { [key: string]: string } {
    const property = this.isRTL ? 'margin-left' : 'margin-right';
    return { [property]: `${value}px` };
  }
}

TitleService

Service for managing document title with formatting options and i18n support.

/**
 * Document title management service
 * Provides title formatting and i18n integration
 */
class TitleService {
  /** Title separator character (setter only) */
  set separator(value: string);
  /** Title prefix text (setter only) */
  set prefix(value: string);
  /** Title suffix text (setter only) */
  set suffix(value: string);
  /** Whether to reverse title parts order (setter only) */
  set reverse(value: boolean);
  /** CSS selector for title element (optional) */
  selector?: string | null;
  /** Default title name */
  default: string;
  
  /** Set document title with optional formatting */
  setTitle(title?: string | string[]): void;
  /** Set title using i18n key with parameters */
  setTitleByI18n(key: string, params?: unknown): void;
}

Usage Examples:

import { Component, inject, OnInit } from "@angular/core";
import { Router, NavigationEnd } from "@angular/router";
import { TitleService } from "@delon/theme";
import { filter, map } from "rxjs/operators";

@Component({
  selector: "app-root",
  template: `
    <div class="app-container">
      <header>
        <h1>My Application</h1>
        <nav>
          <a routerLink="/dashboard" (click)="setPageTitle('Dashboard')">Dashboard</a>
          <a routerLink="/users" (click)="setPageTitle('Users')">Users</a>
          <a routerLink="/settings" (click)="setPageTitle('Settings')">Settings</a>
        </nav>
      </header>
      
      <main>
        <router-outlet></router-outlet>
      </main>
      
      <div class="title-controls">
        <h3>Title Configuration</h3>
        <label>
          Separator:
          <input 
            type="text" 
            [value]="currentSeparator"
            (input)="updateSeparator($event)"
          />
        </label>
        <label>
          Prefix:
          <input 
            type="text" 
            [value]="currentPrefix"
            (input)="updatePrefix($event)"
          />
        </label>
        <label>
          Suffix:
          <input 
            type="text" 
            [value]="currentSuffix"
            (input)="updateSuffix($event)"
          />
        </label>
        <label>
          <input 
            type="checkbox" 
            [checked]="isReversed"
            (change)="updateReverse($event)"
          />
          Reverse Order
        </label>
      </div>
    </div>
  `
})
export class AppComponent implements OnInit {
  titleService = inject(TitleService);
  router = inject(Router);
  
  currentSeparator = ' - ';
  currentPrefix = 'MyApp';
  currentSuffix = '';
  isReversed = false;
  
  ngOnInit() {
    // Configure title service
    this.titleService.separator = this.currentSeparator;
    this.titleService.prefix = this.currentPrefix;
    this.titleService.default = 'My Application';
    
    // Listen to route changes and update title
    this.router.events.pipe(
      filter(event => event instanceof NavigationEnd),
      map(() => this.getRouteTitle())
    ).subscribe(title => {
      if (title) {
        this.titleService.setTitle(title);
      }
    });
  }
  
  setPageTitle(title: string) {
    this.titleService.setTitle(title);
  }
  
  setMultiPartTitle() {
    // Set title with multiple parts
    this.titleService.setTitle(['Users', 'User Profile', 'John Doe']);
  }
  
  setI18nTitle() {
    // Set title using i18n key
    this.titleService.setTitleByI18n('page.dashboard.title');
  }
  
  setParameterizedI18nTitle() {
    // Set title with i18n parameters
    this.titleService.setTitleByI18n('page.user.title', { name: 'John Doe' });
  }
  
  updateSeparator(event: Event) {
    const value = (event.target as HTMLInputElement).value;
    this.currentSeparator = value;
    this.titleService.separator = value;
    this.refreshTitle();
  }
  
  updatePrefix(event: Event) {
    const value = (event.target as HTMLInputElement).value;
    this.currentPrefix = value;
    this.titleService.prefix = value;
    this.refreshTitle();
  }
  
  updateSuffix(event: Event) {
    const value = (event.target as HTMLInputElement).value;
    this.currentSuffix = value;
    this.titleService.suffix = value;
    this.refreshTitle();
  }
  
  updateReverse(event: Event) {
    const checked = (event.target as HTMLInputElement).checked;
    this.isReversed = checked;
    this.titleService.reverse = checked;
    this.refreshTitle();
  }
  
  private refreshTitle() {
    // Re-apply current title with new settings
    const currentTitle = this.getCurrentPageTitle();
    if (currentTitle) {
      this.titleService.setTitle(currentTitle);
    }
  }
  
  private getRouteTitle(): string | null {
    // Extract title from route data
    const route = this.router.routerState.root;
    let title: string | null = null;
    
    // Traverse route tree to find title
    let currentRoute = route;
    while (currentRoute) {
      if (currentRoute.snapshot.data['title']) {
        title = currentRoute.snapshot.data['title'];
      }
      currentRoute = currentRoute.firstChild;
    }
    
    return title;
  }
  
  private getCurrentPageTitle(): string | null {
    // Get current page title from various sources
    return this.getRouteTitle() || 'Default Page';
  }
}

// Title strategy for automatic title updates
@Injectable()
export class CustomTitleStrategy extends TitleStrategy {
  constructor(private titleService: TitleService) {
    super();
  }
  
  updateTitle(routerState: RouterStateSnapshot): void {
    const title = this.buildTitle(routerState);
    if (title !== undefined) {
      this.titleService.setTitle(title);
    }
  }
}

// Route configuration with titles
const routes: Routes = [
  {
    path: 'dashboard',
    component: DashboardComponent,
    data: { title: 'Dashboard' }
  },
  {
    path: 'users',
    data: { title: 'Users' },
    children: [
      {
        path: 'list',
        component: UserListComponent,
        data: { title: 'User List' }
      },
      {
        path: 'profile/:id',
        component: UserProfileComponent,
        data: { title: 'User Profile' }
      }
    ]
  }
];

stepPreloader

Function for hiding page preloader with smooth animation.

/**
 * Hide page preloader with animation
 * Returns cleanup function to stop the animation
 */
function stepPreloader(): () => void;

Usage Examples:

import { Component, OnInit } from "@angular/core";
import { stepPreloader } from "@delon/theme";

@Component({
  selector: "app-loading",
  template: `
    <div class="app-container">
      <div *ngIf="isLoading" class="loading-overlay">
        <div class="spinner"></div>
        <p>Loading application...</p>
      </div>
      
      <div class="main-content" [style.opacity]="isLoading ? 0 : 1">
        <h1>Application Loaded</h1>
        <p>Welcome to the application!</p>
      </div>
      
      <button (click)="simulateLoading()">Simulate Loading</button>
    </div>
  `,
  styles: [`
    .loading-overlay {
      position: fixed;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      background: rgba(255, 255, 255, 0.9);
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: center;
      z-index: 9999;
    }
    
    .spinner {
      width: 40px;
      height: 40px;
      border: 4px solid #f3f3f3;
      border-top: 4px solid #1890ff;
      border-radius: 50%;
      animation: spin 1s linear infinite;
    }
    
    @keyframes spin {
      0% { transform: rotate(0deg); }
      100% { transform: rotate(360deg); }
    }
    
    .main-content {
      transition: opacity 0.3s ease;
      padding: 20px;
    }
  `]
})
export class AppLoadingComponent implements OnInit {
  isLoading = true;
  private preloaderCleanup?: () => void;
  
  ngOnInit() {
    // Simulate app initialization
    this.initializeApp();
  }
  
  private async initializeApp() {
    try {
      // Simulate async operations (API calls, data loading, etc.)
      await this.loadInitialData();
      await this.setupServices();
      await this.preloadAssets();
      
      // Hide preloader with animation
      this.hidePreloader();
      
    } catch (error) {
      console.error('App initialization failed:', error);
      this.hidePreloader(); // Hide even on error
    }
  }
  
  private hidePreloader() {
    // Use stepPreloader to hide with animation
    this.preloaderCleanup = stepPreloader();
    
    // Update component state
    setTimeout(() => {
      this.isLoading = false;
    }, 300); // Allow animation to complete
  }
  
  simulateLoading() {
    this.isLoading = true;
    
    // Simulate loading process
    setTimeout(() => {
      this.hidePreloader();
    }, 2000);
  }
  
  ngOnDestroy() {
    // Cleanup preloader animation if needed
    if (this.preloaderCleanup) {
      this.preloaderCleanup();
    }
  }
  
  private async loadInitialData(): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, 800));
  }
  
  private async setupServices(): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, 500));
  }
  
  private async preloadAssets(): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, 300));
  }
}

// Application bootstrap with preloader
export class AppInitializer {
  static async bootstrap() {
    // Show initial preloader
    const cleanup = stepPreloader();
    
    try {
      // Bootstrap Angular application
      const app = await bootstrapApplication(AppComponent, {
        providers: [
          // ... providers
        ]
      });
      
      // Hide preloader after app is ready
      setTimeout(cleanup, 100);
      
      return app;
    } catch (error) {
      console.error('Bootstrap failed:', error);
      cleanup(); // Hide preloader even on error
      throw error;
    }
  }
}

// Custom preloader implementation
export class CustomPreloader {
  private static cleanupFn?: () => void;
  
  static show(): void {
    // Add preloader to DOM
    const preloader = document.createElement('div');
    preloader.id = 'custom-preloader';
    preloader.innerHTML = `
      <div class="preloader-content">
        <div class="spinner"></div>
        <p>Loading...</p>
      </div>
    `;
    document.body.appendChild(preloader);
  }
  
  static hide(): void {
    // Use stepPreloader for smooth animation
    this.cleanupFn = stepPreloader();
    
    // Remove preloader after animation
    setTimeout(() => {
      const preloader = document.getElementById('custom-preloader');
      if (preloader) {
        document.body.removeChild(preloader);
      }
    }, 500);
  }
  
  static cleanup(): void {
    if (this.cleanupFn) {
      this.cleanupFn();
      this.cleanupFn = undefined;
    }
  }
}