使指令 @Input 需要
Make directive @Input required
在AngularJs 中,我们可以使指令属性成为必需的。我们如何使用@Input 在 Angular 中做到这一点?文档没有提到它。
例如
@Component({
selector: 'my-dir',
template: '<div></div>'
})
export class MyComponent {
@Input() a: number; // Make this a required attribute. Throw an exception if it doesn't exist.
@Input() b: number;
}
你可以这样做:
constructor() {}
ngOnInit() {
if (!this.a) throw new Error();
}
检查ngOnInit()
(执行构造函数时尚未设置输入)属性是否具有值。
Component({
selector: 'my-dir',
template: '<div></div>'
})
export class MyComponent implements OnInit, OnChanges {
@Input() a:number; // Make this a required attribute. Throw an exception if it doesnt exist
@Input() b:number;
constructor(){
}
ngOnInit() {
this.checkRequiredFields(this.a);
}
ngOnChanges(changes) {
this.checkRequiredFields(this.a);
}
checkRequiredFields(input) {
if(input === null) {
throw new Error("Attribute 'a' is required");
}
}
}
如果值未设置为 null
,您也可以签入 ngOnChanges(changes) {...}
。另见 https://angular.io/docs/ts/latest/api/core/OnChanges-interface.html
对我来说,我不得不这样做:
ngOnInit() {
if(!this.hasOwnProperty('a') throw new Error("Attribute 'a' is required");
}
仅供参考,如果你想要求@Output指令,那么试试这个:
export class MyComponent {
@Output() myEvent = new EventEmitter(); // This a required event
ngOnInit() {
if(this.myEvent.observers.length === 0) throw new Error("Event 'myEvent' is required");
}
}
官方解决方案
因为 by Ryan Miglavs – smart usage of Angular's selectors 解决了问题。
Component({
selector: 'my-dir[a]', // <-- use attribute selector along with tag to ensure both tag name and attribute are used to "select" element by Angular in DOM
});
export class MyComponent {
@Input() a: number;
}
就我个人而言,在大多数情况下我更喜欢这种解决方案,因为它不需要在编码期间进行任何额外的工作。但是,它也有一些缺点:
- 无法理解抛出的错误中缺少什么参数
- 正如它所说,错误本身令人困惑,Angular 无法识别该标签,而只是缺少一些参数
对于替代解决方案——请看下面,它们需要一些额外的编码,但没有上述缺点。
所以,这是我使用 getters/setters 的解决方案。恕我直言,这是一个非常优雅的解决方案,因为一切都在一个地方完成,而且这个解决方案不需要 OnInit
依赖。
解决方案 #2
Component({
selector: 'my-dir',
template: '<div></div>',
});
export class MyComponent {
@Input()
get a() {
throw new Error('Attribute "a" is required');
}
set a(value: number) {
Object.defineProperty(this, 'a', {
value,
writable: true,
configurable: true,
});
}
}
解决方案 #3:
使用装饰器可以更容易。所以,你在你的应用程序中定义一次这样的装饰器:
function Required(target: object, propertyKey: string) {
Object.defineProperty(target, propertyKey, {
get() {
throw new Error(`Attribute ${propertyKey} is required`);
},
set(value) {
Object.defineProperty(target, propertyKey, {
value,
writable: true,
configurable: true,
});
},
configurable: true
});
}
稍后在您的 class 中,您只需按要求标记您的 属性,如下所示:
Component({
selector: 'my-dir',
template: '<div></div>',
});
export class MyComponent {
@Input() @Required a: number;
}
解释:
如果定义了属性 a
- 属性 a
中的 setter 将覆盖自身并使用传递给属性的值。否则 - 在组件初始化之后 - 第一次你想在你的 class 或模板中使用 属性 a
- 将抛出错误。
注意: getters/setters 在 Angular 的 components/services 等中运行良好,这样使用它们是安全的。但是在 Angular 之外对纯 classes 使用这种方法时要小心。问题是 typescript 如何将 transpiles getters/setters 分配给 ES5 - 它们被分配给 prototype
属性 的 class。在这种情况下,我们会改变原型 属性,这对于 class 的所有实例都是相同的。意味着我们可以得到这样的东西:
const instance1 = new ClassStub();
instance1.property = 'some value';
const instance2 = new ClassStub();
console.log(instance2.property); // 'some value'
执行此操作的官方 Angular 方法是在组件的选择器中包含所需的属性。所以,像这样:
Component({
selector: 'my-dir[a]', // <-- Check it
template: '<div></div>'
})
export class MyComponent {
@Input() a:number; // This property is required by virtue of the selector above
@Input() b:number; // This property is still optional, but could be added to the selector to require it
constructor(){
}
ngOnInit() {
}
}
这样做的好处是,如果开发人员在模板中引用组件时不包含 属性 (a
),代码将无法编译。这意味着编译时安全而不是 运行 时安全,这很好。
不幸的是,开发人员将收到的错误消息是"my-dir
is not a known element",这不是很清楚。
我尝试了 ihor 提到的装饰器方法,但我 运行 遇到了问题,因为它适用于 Class(因此在 TS 编译到原型之后),而不是实例;这意味着对于一个组件的所有副本,装饰器只需要 运行 一次,或者至少我找不到一种方法让它适用于多个实例。
这是docs for the selector option。请注意,它实际上允许非常灵活的 CSS 风格的选择器(甜言蜜语)。
我在 Github feature request thread 上找到了这条推荐。
为什么不使用 @angular/forms
库来验证您的 @Input
?
解决方案如下:
- 快速失败(不仅仅是组件第一次访问
@input
值时)
- 允许重复使用您已经用于 Angular 表单的规则
用法:
export class MyComponent {
@Input() propOne: string;
@Input() propTwo: string;
ngOnInit() {
validateProps<MyComponent>(this, {
propOne: [Validators.required, Validators.pattern('[a-zA-Z ]*')],
propTwo: [Validators.required, Validators.minLength(5), myCustomRule()]
})
}
}
效用函数:
import { FormArray, FormBuilder, ValidatorFn, FormControl } from '@angular/forms';
export function validateProps<T>(cmp: T, ruleset: {[key in keyof T]?: ValidatorFn[]} ) {
const toGroup = {};
Object.keys(ruleset)
.forEach(key => toGroup[key] = new FormControl(cmp[key], ruleset[key]));
const formGroup = new FormBuilder().group(toGroup);
formGroup.updateValueAndValidity();
const validationResult = {};
Object.keys(formGroup.controls)
.filter(key => formGroup.controls[key].errors)
.forEach(key => validationResult[key] = formGroup.controls[key].errors);
if (Object.keys(validationResult).length) {
throw new Error(`Input validation failed:\n ${JSON.stringify(validationResult, null, 2)}`);
}
}
这是另一种基于 TypeScript 装饰器的方法,它不那么复杂,也更容易理解。它还支持组件继承。
// Map of component name -> list of required properties
let requiredInputs = new Map<string, string[]>();
/**
* Mark @Input() as required.
*
* Supports inheritance chains for components.
*
* Example:
*
* import { isRequired, checkRequired } from '../requiredInput';
*
* export class MyComp implements OnInit {
*
* // Chain id paramter we check for from the wallet
* @Input()
* @isRequired
* requiredChainId: number;
*
* ngOnInit(): void {
* checkRequired(this);
* }
* }
*
* @param target Object given by the TypeScript decorator
* @param prop Property name from the TypeScript decorator
*/
export function isRequired(target: any, prop: string) {
// Maintain a global table which components require which inputs
const className = target.constructor.name;
requiredInputs[className] = requiredInputs[className] || [];
requiredInputs[className].push(prop);
// console.log(className, prop, requiredInputs[className]);
}
/**
* Check that all required inputs are filled.
*/
export function checkRequired(component: any) {
let className = component.constructor.name;
let nextParent = Object.getPrototypeOf(component);
// Walk through the parent class chain
while(className != "Object") {
for(let prop of (requiredInputs[className] || [])) {
const val = component[prop];
if(val === null || val === undefined) {
console.error(component.constructor.name, prop, "is required, but was not provided, actual value is", val);
}
}
className = nextParent.constructor.name;
nextParent = Object.getPrototypeOf(nextParent);
// console.log("Checking", component, className);
}
}
声明必填字段的非常简单且自适应的方式
许多答案已经在展示这种官方技术。如果你想添加多个必需的文件怎么办?然后执行以下操作:
单个必填字段
@Component({
selector: 'my-component[field1]',
templateUrl: './my-component.component.html',
styleUrls: ['./my-component.component.scss']
})
多个字段,但都是必填字段
@Component({
selector: 'my-component[field1][field2][field3]',
templateUrl: './my-component.component.html',
styleUrls: ['./my-component.component.scss']
})
多个字段,但至少需要一个字段
@Component({
selector: 'my-component[field1], my-component[field2], my-component[field3]',
templateUrl: './my-component.component.html',
styleUrls: ['./my-component.component.scss']
})
下面是如何在 html
中使用
<my-component [field1]="value" [field2]="value" [field3]="value"></my-component>
我能够在第二个 Object.defineProperty
中使用 this
使 @ihor 的 Required 装饰器工作。 this
强制装饰器在每个实例上定义 属性。
export function Required(message?: string) {
return function (target: Object, propertyKey: PropertyKey) {
Object.defineProperty(target, propertyKey, {
get() {
throw new Error(message || `Attribute ${String(propertyKey)} is required`);
},
set(value) {
Object.defineProperty(this, propertyKey, {
value,
writable: true
});
}
});
};
}
在AngularJs 中,我们可以使指令属性成为必需的。我们如何使用@Input 在 Angular 中做到这一点?文档没有提到它。
例如
@Component({
selector: 'my-dir',
template: '<div></div>'
})
export class MyComponent {
@Input() a: number; // Make this a required attribute. Throw an exception if it doesn't exist.
@Input() b: number;
}
你可以这样做:
constructor() {}
ngOnInit() {
if (!this.a) throw new Error();
}
检查ngOnInit()
(执行构造函数时尚未设置输入)属性是否具有值。
Component({
selector: 'my-dir',
template: '<div></div>'
})
export class MyComponent implements OnInit, OnChanges {
@Input() a:number; // Make this a required attribute. Throw an exception if it doesnt exist
@Input() b:number;
constructor(){
}
ngOnInit() {
this.checkRequiredFields(this.a);
}
ngOnChanges(changes) {
this.checkRequiredFields(this.a);
}
checkRequiredFields(input) {
if(input === null) {
throw new Error("Attribute 'a' is required");
}
}
}
如果值未设置为 null
,您也可以签入 ngOnChanges(changes) {...}
。另见 https://angular.io/docs/ts/latest/api/core/OnChanges-interface.html
对我来说,我不得不这样做:
ngOnInit() {
if(!this.hasOwnProperty('a') throw new Error("Attribute 'a' is required");
}
仅供参考,如果你想要求@Output指令,那么试试这个:
export class MyComponent {
@Output() myEvent = new EventEmitter(); // This a required event
ngOnInit() {
if(this.myEvent.observers.length === 0) throw new Error("Event 'myEvent' is required");
}
}
官方解决方案
因为
Component({
selector: 'my-dir[a]', // <-- use attribute selector along with tag to ensure both tag name and attribute are used to "select" element by Angular in DOM
});
export class MyComponent {
@Input() a: number;
}
就我个人而言,在大多数情况下我更喜欢这种解决方案,因为它不需要在编码期间进行任何额外的工作。但是,它也有一些缺点:
- 无法理解抛出的错误中缺少什么参数
- 正如它所说,错误本身令人困惑,Angular 无法识别该标签,而只是缺少一些参数
对于替代解决方案——请看下面,它们需要一些额外的编码,但没有上述缺点。
所以,这是我使用 getters/setters 的解决方案。恕我直言,这是一个非常优雅的解决方案,因为一切都在一个地方完成,而且这个解决方案不需要 OnInit
依赖。
解决方案 #2
Component({
selector: 'my-dir',
template: '<div></div>',
});
export class MyComponent {
@Input()
get a() {
throw new Error('Attribute "a" is required');
}
set a(value: number) {
Object.defineProperty(this, 'a', {
value,
writable: true,
configurable: true,
});
}
}
解决方案 #3:
使用装饰器可以更容易。所以,你在你的应用程序中定义一次这样的装饰器:
function Required(target: object, propertyKey: string) {
Object.defineProperty(target, propertyKey, {
get() {
throw new Error(`Attribute ${propertyKey} is required`);
},
set(value) {
Object.defineProperty(target, propertyKey, {
value,
writable: true,
configurable: true,
});
},
configurable: true
});
}
稍后在您的 class 中,您只需按要求标记您的 属性,如下所示:
Component({
selector: 'my-dir',
template: '<div></div>',
});
export class MyComponent {
@Input() @Required a: number;
}
解释:
如果定义了属性 a
- 属性 a
中的 setter 将覆盖自身并使用传递给属性的值。否则 - 在组件初始化之后 - 第一次你想在你的 class 或模板中使用 属性 a
- 将抛出错误。
注意: getters/setters 在 Angular 的 components/services 等中运行良好,这样使用它们是安全的。但是在 Angular 之外对纯 classes 使用这种方法时要小心。问题是 typescript 如何将 transpiles getters/setters 分配给 ES5 - 它们被分配给 prototype
属性 的 class。在这种情况下,我们会改变原型 属性,这对于 class 的所有实例都是相同的。意味着我们可以得到这样的东西:
const instance1 = new ClassStub();
instance1.property = 'some value';
const instance2 = new ClassStub();
console.log(instance2.property); // 'some value'
执行此操作的官方 Angular 方法是在组件的选择器中包含所需的属性。所以,像这样:
Component({
selector: 'my-dir[a]', // <-- Check it
template: '<div></div>'
})
export class MyComponent {
@Input() a:number; // This property is required by virtue of the selector above
@Input() b:number; // This property is still optional, but could be added to the selector to require it
constructor(){
}
ngOnInit() {
}
}
这样做的好处是,如果开发人员在模板中引用组件时不包含 属性 (a
),代码将无法编译。这意味着编译时安全而不是 运行 时安全,这很好。
不幸的是,开发人员将收到的错误消息是"my-dir
is not a known element",这不是很清楚。
我尝试了 ihor 提到的装饰器方法,但我 运行 遇到了问题,因为它适用于 Class(因此在 TS 编译到原型之后),而不是实例;这意味着对于一个组件的所有副本,装饰器只需要 运行 一次,或者至少我找不到一种方法让它适用于多个实例。
这是docs for the selector option。请注意,它实际上允许非常灵活的 CSS 风格的选择器(甜言蜜语)。
我在 Github feature request thread 上找到了这条推荐。
为什么不使用 @angular/forms
库来验证您的 @Input
?
解决方案如下:
- 快速失败(不仅仅是组件第一次访问
@input
值时) - 允许重复使用您已经用于 Angular 表单的规则
用法:
export class MyComponent {
@Input() propOne: string;
@Input() propTwo: string;
ngOnInit() {
validateProps<MyComponent>(this, {
propOne: [Validators.required, Validators.pattern('[a-zA-Z ]*')],
propTwo: [Validators.required, Validators.minLength(5), myCustomRule()]
})
}
}
效用函数:
import { FormArray, FormBuilder, ValidatorFn, FormControl } from '@angular/forms';
export function validateProps<T>(cmp: T, ruleset: {[key in keyof T]?: ValidatorFn[]} ) {
const toGroup = {};
Object.keys(ruleset)
.forEach(key => toGroup[key] = new FormControl(cmp[key], ruleset[key]));
const formGroup = new FormBuilder().group(toGroup);
formGroup.updateValueAndValidity();
const validationResult = {};
Object.keys(formGroup.controls)
.filter(key => formGroup.controls[key].errors)
.forEach(key => validationResult[key] = formGroup.controls[key].errors);
if (Object.keys(validationResult).length) {
throw new Error(`Input validation failed:\n ${JSON.stringify(validationResult, null, 2)}`);
}
}
这是另一种基于 TypeScript 装饰器的方法,它不那么复杂,也更容易理解。它还支持组件继承。
// Map of component name -> list of required properties
let requiredInputs = new Map<string, string[]>();
/**
* Mark @Input() as required.
*
* Supports inheritance chains for components.
*
* Example:
*
* import { isRequired, checkRequired } from '../requiredInput';
*
* export class MyComp implements OnInit {
*
* // Chain id paramter we check for from the wallet
* @Input()
* @isRequired
* requiredChainId: number;
*
* ngOnInit(): void {
* checkRequired(this);
* }
* }
*
* @param target Object given by the TypeScript decorator
* @param prop Property name from the TypeScript decorator
*/
export function isRequired(target: any, prop: string) {
// Maintain a global table which components require which inputs
const className = target.constructor.name;
requiredInputs[className] = requiredInputs[className] || [];
requiredInputs[className].push(prop);
// console.log(className, prop, requiredInputs[className]);
}
/**
* Check that all required inputs are filled.
*/
export function checkRequired(component: any) {
let className = component.constructor.name;
let nextParent = Object.getPrototypeOf(component);
// Walk through the parent class chain
while(className != "Object") {
for(let prop of (requiredInputs[className] || [])) {
const val = component[prop];
if(val === null || val === undefined) {
console.error(component.constructor.name, prop, "is required, but was not provided, actual value is", val);
}
}
className = nextParent.constructor.name;
nextParent = Object.getPrototypeOf(nextParent);
// console.log("Checking", component, className);
}
}
声明必填字段的非常简单且自适应的方式
许多答案已经在展示这种官方技术。如果你想添加多个必需的文件怎么办?然后执行以下操作:
单个必填字段
@Component({
selector: 'my-component[field1]',
templateUrl: './my-component.component.html',
styleUrls: ['./my-component.component.scss']
})
多个字段,但都是必填字段
@Component({
selector: 'my-component[field1][field2][field3]',
templateUrl: './my-component.component.html',
styleUrls: ['./my-component.component.scss']
})
多个字段,但至少需要一个字段
@Component({
selector: 'my-component[field1], my-component[field2], my-component[field3]',
templateUrl: './my-component.component.html',
styleUrls: ['./my-component.component.scss']
})
下面是如何在 html
中使用<my-component [field1]="value" [field2]="value" [field3]="value"></my-component>
我能够在第二个 Object.defineProperty
中使用 this
使 @ihor 的 Required 装饰器工作。 this
强制装饰器在每个实例上定义 属性。
export function Required(message?: string) {
return function (target: Object, propertyKey: PropertyKey) {
Object.defineProperty(target, propertyKey, {
get() {
throw new Error(message || `Attribute ${String(propertyKey)} is required`);
},
set(value) {
Object.defineProperty(this, propertyKey, {
value,
writable: true
});
}
});
};
}