Skip to content

Commit b228f07

Browse files
devversionmmalerba
authored andcommitted
refactor: annotate remaining base classes with @directive for… (#17279)
* build: add lint rule to ensure classes with angular features are decorated Read more about this here: https://hackmd.io/vuQfavzfRG6KUCtU7oK_EA. * refactor: annotate base classes with @directive for ivy. These base classes define class members with Angular features. Therefore they need to be decorated with `@Directive()` so that the classes can be extended in Ivy. This is an official migration that will be performed for CLI applications in version 9. angular/angular#32590.
1 parent 230fd2f commit b228f07

File tree

8 files changed

+114
-7
lines changed

8 files changed

+114
-7
lines changed

src/cdk-experimental/popover-edit/popover-edit.spec.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {LEFT_ARROW, UP_ARROW, RIGHT_ARROW, DOWN_ARROW, TAB} from '@angular/cdk/k
33
import {CdkTableModule} from '@angular/cdk/table';
44
import {dispatchKeyboardEvent} from '@angular/cdk/testing';
55
import {CommonModule} from '@angular/common';
6-
import {Component, ElementRef, Type, ViewChild} from '@angular/core';
6+
import {Component, Directive, ElementRef, Type, ViewChild} from '@angular/core';
77
import {ComponentFixture, fakeAsync, flush, TestBed, tick, inject} from '@angular/core/testing';
88
import {FormsModule, NgForm} from '@angular/forms';
99
import {BidiModule, Direction} from '@angular/cdk/bidi';
@@ -61,6 +61,10 @@ interface PeriodicElement {
6161
weight: number;
6262
}
6363

64+
@Directive({
65+
// TODO(devversion): this selector can be removed when we update to Angular 9.0.
66+
selector: 'do-not-use-abstract-cdk-popover-edit-base-test-component'
67+
})
6468
abstract class BaseTestComponent {
6569
@ViewChild('table', {static: false}) table: ElementRef;
6670

src/material-experimental/mdc-button/button-base.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import {Platform} from '@angular/cdk/platform';
10-
import {ElementRef, NgZone, ViewChild} from '@angular/core';
10+
import {Directive, ElementRef, Inject, NgZone, Optional, ViewChild} from '@angular/core';
1111
import {
1212
CanColor,
1313
CanColorCtor,
@@ -21,6 +21,7 @@ import {
2121
mixinDisableRipple,
2222
RippleAnimationConfig
2323
} from '@angular/material/core';
24+
import {ANIMATION_MODULE_TYPE} from '@angular/platform-browser/animations';
2425
import {numbers} from '@material/ripple';
2526

2627
/** Inputs common to all buttons. */
@@ -78,6 +79,10 @@ export const _MatButtonBaseMixin: CanDisableRippleCtor&CanDisableCtor&CanColorCt
7879
typeof MatButtonMixinCore = mixinColor(mixinDisabled(mixinDisableRipple(MatButtonMixinCore)));
7980

8081
/** Base class for all buttons. */
82+
@Directive({
83+
// TODO(devversion): this selector can be removed when we update to Angular 9.0.
84+
selector: 'do-not-use-abstract-mat-button-base'
85+
})
8186
export class MatButtonBase extends _MatButtonBaseMixin implements CanDisable, CanColor,
8287
CanDisableRipple {
8388
/** The ripple animation configuration to use for the buttons. */
@@ -94,7 +99,8 @@ export class MatButtonBase extends _MatButtonBaseMixin implements CanDisable, Ca
9499

95100
constructor(
96101
elementRef: ElementRef, public _platform: Platform, public _ngZone: NgZone,
97-
public _animationMode?: string) {
102+
// TODO(devversion): Injection can be removed if angular/angular#32981 is fixed.
103+
@Optional() @Inject(ANIMATION_MODULE_TYPE) public _animationMode?: string) {
98104
super(elementRef);
99105

100106
// For each of the variant selectors that is present in the button's host
@@ -144,10 +150,16 @@ export const MAT_ANCHOR_HOST = {
144150
/**
145151
* Anchor button base.
146152
*/
153+
@Directive({
154+
// TODO(devversion): this selector can be removed when we update to Angular 9.0.
155+
selector: 'do-not-use-abstract-mat-anchor-base'
156+
})
147157
export class MatAnchorBase extends MatButtonBase {
148158
tabIndex: number;
149159

150-
constructor(elementRef: ElementRef, platform: Platform, ngZone: NgZone, animationMode?: string) {
160+
constructor(elementRef: ElementRef, platform: Platform, ngZone: NgZone,
161+
// TODO(devversion): Injection can be removed if angular/angular#32981 is fixed.
162+
@Optional() @Inject(ANIMATION_MODULE_TYPE) animationMode?: string) {
151163
super(elementRef, platform, ngZone, animationMode);
152164
}
153165

src/material-experimental/mdc-button/module.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {CommonModule} from '@angular/common';
1010
import {NgModule} from '@angular/core';
1111
import {MatCommonModule, MatRippleModule} from '@angular/material/core';
1212
import {MatAnchor, MatButton} from './button';
13+
import {MatAnchorBase, MatButtonBase} from './button-base';
1314
import {MatFabAnchor, MatFabButton} from './fab';
1415
import {MatIconAnchor, MatIconButton} from './icon-button';
1516

@@ -31,6 +32,10 @@ import {MatIconAnchor, MatIconButton} from './icon-button';
3132
MatIconButton,
3233
MatFabAnchor,
3334
MatFabButton,
35+
// TODO(devversion): remove when `MatButtonBase` becomes a selectorless Directive.
36+
MatButtonBase,
37+
// TODO(devversion): remove when `MatAnchorBase` becomes a selectorless Directive.
38+
MatAnchorBase,
3439
],
3540
})
3641
export class MatButtonModule {

src/material-experimental/popover-edit/popover-edit.spec.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {LEFT_ARROW, UP_ARROW, RIGHT_ARROW, DOWN_ARROW, TAB} from '@angular/cdk/k
33
import {MatTableModule} from '@angular/material/table';
44
import {dispatchKeyboardEvent} from '@angular/cdk/testing';
55
import {CommonModule} from '@angular/common';
6-
import {Component, ElementRef, Type, ViewChild} from '@angular/core';
6+
import {Component, Directive, ElementRef, Type, ViewChild} from '@angular/core';
77
import {ComponentFixture, fakeAsync, flush, TestBed, tick, inject} from '@angular/core/testing';
88
import {FormsModule, NgForm} from '@angular/forms';
99
import {OverlayContainer} from '@angular/cdk/overlay';
@@ -59,6 +59,10 @@ interface PeriodicElement {
5959
weight: number;
6060
}
6161

62+
@Directive({
63+
// TODO(devversion): this selector can be removed when we update to Angular 9.0.
64+
selector: 'do-not-use-abstract-mat-popover-edit-base-test-component'
65+
})
6266
abstract class BaseTestComponent {
6367
@ViewChild('table', {static: false}) table: ElementRef;
6468

src/material/menu/menu-module.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {CommonModule} from '@angular/common';
1111
import {NgModule} from '@angular/core';
1212
import {MatCommonModule, MatRippleModule} from '@angular/material/core';
1313
import {MatMenuContent} from './menu-content';
14-
import {_MatMenu} from './menu';
14+
import {_MatMenu, _MatMenuBase, MatMenu} from './menu';
1515
import {MatMenuItem} from './menu-item';
1616
import {
1717
MatMenuTrigger,
@@ -24,7 +24,14 @@ import {
2424
*/
2525
@NgModule({
2626
exports: [MatMenuTrigger, MatMenuContent, MatCommonModule],
27-
declarations: [MatMenuTrigger, MatMenuContent],
27+
declarations: [
28+
MatMenuTrigger,
29+
MatMenuContent,
30+
// TODO(devversion): remove when `MatMenu` becomes a selectorless Directive.
31+
MatMenu,
32+
// TODO(devversion): remove when `_MatMenuBase` becomes a selectorless Directive.
33+
_MatMenuBase
34+
],
2835
providers: [MAT_MENU_SCROLL_STRATEGY_FACTORY_PROVIDER]
2936
})
3037
// tslint:disable-next-line:class-name

src/material/menu/menu.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
Component,
2626
ContentChild,
2727
ContentChildren,
28+
Directive,
2829
ElementRef,
2930
EventEmitter,
3031
Inject,
@@ -90,6 +91,10 @@ export function MAT_MENU_DEFAULT_OPTIONS_FACTORY(): MatMenuDefaultOptions {
9091
const MAT_MENU_BASE_ELEVATION = 4;
9192

9293
/** Base class with all of the `MatMenu` functionality. */
94+
@Directive({
95+
// TODO(devversion): this selector can be removed when we update to Angular 9.0.
96+
selector: 'do-not-use-abstract-mat-menu-base'
97+
})
9398
// tslint:disable-next-line:class-name
9499
export class _MatMenuBase implements AfterContentInit, MatMenuPanel<MatMenuItem>, OnInit,
95100
OnDestroy {
@@ -445,6 +450,10 @@ export class _MatMenuBase implements AfterContentInit, MatMenuPanel<MatMenuItem>
445450
}
446451

447452
/** @docs-private We show the "_MatMenu" class as "MatMenu" in the docs. */
453+
@Directive({
454+
// TODO(devversion): this selector can be removed when we update to Angular 9.0.
455+
selector: 'do-not-use-abstract-mat-menu'
456+
})
448457
export class MatMenu extends _MatMenuBase {}
449458

450459
// Note on the weird inheritance setup: we need three classes, because the MDC-based menu has to
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import * as Lint from 'tslint';
2+
import * as ts from 'typescript';
3+
4+
const RULE_FAILURE = `Undecorated class defines fields with Angular decorators. Undecorated ` +
5+
`classes with Angular fields cannot be extended in Ivy since no definition is generated. ` +
6+
`Add a "@Directive" decorator to fix this.`;
7+
8+
/**
9+
* Rule that doesn't allow undecorated class declarations with fields using Angular
10+
* decorators.
11+
*/
12+
export class Rule extends Lint.Rules.TypedRule {
13+
applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): Lint.RuleFailure[] {
14+
return this.applyWithWalker(
15+
new Walker(sourceFile, this.getOptions(), program.getTypeChecker()));
16+
}
17+
}
18+
19+
class Walker extends Lint.RuleWalker {
20+
constructor(
21+
sourceFile: ts.SourceFile, options: Lint.IOptions, private _typeChecker: ts.TypeChecker) {
22+
super(sourceFile, options);
23+
}
24+
25+
visitClassDeclaration(node: ts.ClassDeclaration) {
26+
if (this._hasAngularDecorator(node)) {
27+
return;
28+
}
29+
30+
for (let member of node.members) {
31+
if (member.decorators && this._hasAngularDecorator(member)) {
32+
this.addFailureAtNode(node, RULE_FAILURE);
33+
return;
34+
}
35+
}
36+
}
37+
38+
/** Checks if the specified node has an Angular decorator. */
39+
private _hasAngularDecorator(node: ts.Node): boolean {
40+
return !!node.decorators && node.decorators.some(d => {
41+
if (!ts.isCallExpression(d.expression) ||
42+
!ts.isIdentifier(d.expression.expression)) {
43+
return false;
44+
}
45+
46+
const moduleImport = this._getModuleImportOfIdentifier(d.expression.expression);
47+
return moduleImport ? moduleImport.startsWith('@angular/core') : false;
48+
});
49+
}
50+
51+
/** Gets the module import of the given identifier if imported. */
52+
private _getModuleImportOfIdentifier(node: ts.Identifier): string|null {
53+
const symbol = this._typeChecker.getSymbolAtLocation(node);
54+
if (!symbol || !symbol.declarations.length) {
55+
return null;
56+
}
57+
const decl = symbol.declarations[0];
58+
if (!ts.isImportSpecifier(decl)) {
59+
return null;
60+
}
61+
const importDecl = decl.parent.parent.parent;
62+
const moduleSpecifier = importDecl.moduleSpecifier;
63+
return ts.isStringLiteral(moduleSpecifier) ? moduleSpecifier.text : null;
64+
}
65+
}

tslint.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@
101101
"no-import-spacing": true,
102102
"no-private-getters": true,
103103
"no-undecorated-base-class-di": true,
104+
"no-undecorated-class-with-ng-fields": true,
104105
"setters-after-getters": true,
105106
"ng-on-changes-property-access": true,
106107
"rxjs-imports": true,

0 commit comments

Comments
 (0)