使指令 @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)}`);
      }
    }

Stackblitz

这是另一种基于 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
                });
            }
        });
    };
}