or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

accessibility-rules.mdangular-framework.mdcomponent-rules.mddecorator-metadata-rules.mddirective-rules.mdindex.mdinput-output-rules.mdlifecycle-rules.mdpipe-rules.mdstyle-architecture-rules.mdtemplate-rules.md
tile.json

decorator-metadata-rules.mddocs/

Decorator and Metadata Rules

Angular decorator and metadata validation rules that prevent problematic decorator usage and enforce proper metadata configuration patterns.

Capabilities

No Attribute Decorator Rule

Prevents use of @Attribute decorator which has specific limitations and use cases.

/**
 * Prevents use of @Attribute decorator
 * @Attribute decorator has limitations and should generally be avoided in favor of @Input
 */
export class NoAttributeDecoratorRule extends Lint.Rules.AbstractRule {
  static readonly metadata: Lint.IRuleMetadata;
  static readonly FAILURE_STRING: string;
  apply(sourceFile: ts.SourceFile): Lint.RuleFailure[];
}

Usage Examples:

Bad usage with @Attribute:

import { Component, Attribute } from '@angular/core';

@Component({
  selector: 'app-user',
  template: '...'
})
export class UserComponent {
  constructor(
    @Attribute('data-id') private dataId: string  // ❌ @Attribute decorator
  ) {}
}

Good usage with @Input:

import { Component, Input } from '@angular/core';

@Component({
  selector: 'app-user',
  template: '...'
})
export class UserComponent {
  @Input() dataId: string;  // ✅ @Input decorator instead
}

No Forward Ref Rule

Prevents use of forwardRef which can indicate circular dependency issues.

/**
 * Prevents use of forwardRef
 * forwardRef often indicates circular dependencies that should be resolved through refactoring
 */
export class NoForwardRefRule extends Lint.Rules.AbstractRule {
  static readonly metadata: Lint.IRuleMetadata;
  static readonly FAILURE_STRING: string;
  apply(sourceFile: ts.SourceFile): Lint.RuleFailure[];
}

Usage Examples:

Bad usage with forwardRef:

import { Component, forwardRef, Inject } from '@angular/core';

@Component({
  selector: 'app-child',
  template: '...'
})
export class ChildComponent {
  constructor(
    @Inject(forwardRef(() => ParentComponent)) private parent: ParentComponent  // ❌ forwardRef usage
  ) {}
}

Better approaches:

// ✅ Use service for communication
@Injectable()
export class CommunicationService {
  // Shared communication logic
}

@Component({
  selector: 'app-child',
  template: '...'
})
export class ChildComponent {
  constructor(private commService: CommunicationService) {}
}

// ✅ Use ViewChild or ContentChild for parent-child communication
@Component({
  selector: 'app-parent',
  template: '<app-child #childRef></app-child>'
})
export class ParentComponent {
  @ViewChild('childRef') child: ChildComponent;
}

No Host Metadata Property Rule

Prevents defining host properties in component/directive metadata instead of using @HostBinding and @HostListener.

/**
 * Prevents defining host properties in component metadata
 * Enforces use of @HostBinding and @HostListener decorators for better type safety
 */
export class NoHostMetadataPropertyRule extends Lint.Rules.AbstractRule {
  static readonly metadata: Lint.IRuleMetadata;
  static readonly FAILURE_STRING: string;
  apply(sourceFile: ts.SourceFile): Lint.RuleFailure[];
}

Usage Examples:

Bad usage in metadata:

@Component({
  selector: 'app-button',
  template: '...',
  host: {  // ❌ Host in metadata
    '[class.active]': 'isActive',
    '(click)': 'onClick()',
    '[attr.disabled]': 'disabled'
  }
})
export class ButtonComponent {
  isActive = false;
  disabled = false;
  
  onClick() {
    this.isActive = !this.isActive;
  }
}

Good usage with decorators:

@Component({
  selector: 'app-button',
  template: '...'
})
export class ButtonComponent {
  @HostBinding('class.active') isActive = false;  // ✅ @HostBinding decorator
  @HostBinding('attr.disabled') disabled = false;  // ✅ @HostBinding decorator
  
  @HostListener('click')  // ✅ @HostListener decorator
  onClick() {
    this.isActive = !this.isActive;
  }
}

No Queries Metadata Property Rule

Prevents defining queries (ViewChild, ContentChild, etc.) in component metadata instead of using decorators.

/**
 * Prevents defining queries in component metadata
 * Enforces use of @ViewChild, @ContentChild, etc. decorators for better type safety
 */
export class NoQueriesMetadataPropertyRule extends Lint.Rules.AbstractRule {
  static readonly metadata: Lint.IRuleMetadata;
  static readonly FAILURE_STRING: string;
  apply(sourceFile: ts.SourceFile): Lint.RuleFailure[];
}

Usage Examples:

Bad usage in metadata:

@Component({
  selector: 'app-parent',
  template: '<app-child #childRef></app-child>',
  queries: {  // ❌ Queries in metadata
    child: new ViewChild('childRef'),
    children: new ViewChildren(ChildComponent)
  }
})
export class ParentComponent {
  child: ChildComponent;
  children: QueryList<ChildComponent>;
}

Good usage with decorators:

@Component({
  selector: 'app-parent',
  template: '<app-child #childRef></app-child>'
})
export class ParentComponent {
  @ViewChild('childRef') child: ChildComponent;  // ✅ @ViewChild decorator
  @ViewChildren(ChildComponent) children: QueryList<ChildComponent>;  // ✅ @ViewChildren decorator
}

Advanced Decorator Patterns

Host Binding Examples

@Component({
  selector: 'app-card',
  template: '...'
})
export class CardComponent {
  @Input() elevation: number = 1;
  @Input() disabled: boolean = false;
  
  // Dynamic class binding
  @HostBinding('class') 
  get cssClasses() {
    return {
      [`elevation-${this.elevation}`]: true,
      'disabled': this.disabled
    };
  }
  
  // Attribute binding
  @HostBinding('attr.aria-disabled')
  get ariaDisabled() {
    return this.disabled ? 'true' : null;
  }
  
  // Style binding
  @HostBinding('style.opacity')
  get opacity() {
    return this.disabled ? 0.5 : 1;
  }
}

Host Listener Examples

@Directive({
  selector: '[appClickOutside]'
})
export class ClickOutsideDirective {
  @Output() clickOutside = new EventEmitter<void>();
  
  @HostListener('document:click', ['$event'])
  onDocumentClick(event: MouseEvent) {
    if (!this.elementRef.nativeElement.contains(event.target)) {
      this.clickOutside.emit();
    }
  }
  
  @HostListener('keydown.escape')
  onEscapeKey() {
    this.clickOutside.emit();
  }
  
  constructor(private elementRef: ElementRef) {}
}

Query Examples

@Component({
  selector: 'app-tabs',
  template: `
    <div class="tab-headers">
      <ng-content select="[slot=header]"></ng-content>
    </div>
    <div class="tab-content">
      <ng-content></ng-content>
    </div>
  `
})
export class TabsComponent implements AfterViewInit {
  // Single element query
  @ViewChild('tabContent', { static: true }) 
  tabContent: ElementRef;
  
  // Multiple elements query
  @ViewChildren('tabHeader') 
  tabHeaders: QueryList<ElementRef>;
  
  // Component query
  @ContentChildren(TabComponent) 
  tabs: QueryList<TabComponent>;
  
  // Static query (available in ngOnInit)
  @ViewChild('staticElement', { static: true }) 
  staticElement: ElementRef;
  
  ngAfterViewInit() {
    // Access query results here
    this.tabs.forEach((tab, index) => {
      tab.tabIndex = index;
    });
  }
}

Rule Configuration

Example Configuration

{
  "rules": {
    "no-attribute-decorator": true,
    "no-forward-ref": true,
    "no-host-metadata-property": true,
    "no-queries-metadata-property": true
  }
}

Benefits of Decorator Approach

Type Safety

  • Decorators provide better TypeScript integration
  • Compile-time checking of property names and types
  • IntelliSense support in IDEs

Readability

  • Decorators are closer to the properties they affect
  • Clearer intent and easier to understand
  • Better code organization

Maintainability

  • Easier to refactor and update
  • Less prone to typos in property names
  • Better IDE support for renaming

Performance

  • Slightly better performance in some cases
  • More efficient change detection
  • Better tree-shaking support