Angular decorator and metadata validation rules that prevent problematic decorator usage and enforce proper metadata configuration patterns.
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
}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;
}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;
}
}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
}@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;
}
}@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) {}
}@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;
});
}
}{
"rules": {
"no-attribute-decorator": true,
"no-forward-ref": true,
"no-host-metadata-property": true,
"no-queries-metadata-property": true
}
}