Angular 2 make @Input по директиве требуется

В Angular 1 мы могли бы сделать обязательным атрибут директивы. Как мы это делаем в Angular 2 с @Input? Документы не упоминают об этом.

Например.

Component({
    selector: 'my-dir',
    template: '<div></div>'
})
export class MyComponent {
  @Input() a:number; // Make this a required attribute. Throw an exception if it doesnt exist
  @Input() b:number;

  constructor(){

  }
}

Ответ 1

Проверьте в 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");
       }
    }
}

Вы также можете проверить в ngOnChanges(changes) {...}, не были ли установлены значения null. Смотрите также https://angular.io/docs/ts/latest/api/core/OnChanges-interface.html

Ответ 2

Вот мое решение с геттерами/сеттерами. ИМХО, это гораздо более элегантное решение, так как все делается в одном месте, и это решение не требует зависимости OnInit.

Решение № 1

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 }); }
}

Решение № 2:

С декораторами это можно сделать еще проще. Итак, вы определяете в своем приложении один раз декоратор, как этот:

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 });
    },
  });
}

И позже в вашем классе вам просто нужно пометить вашу собственность как требуется так:

Component({
  selector: 'my-dir',
  template: '<div></div>'
})
export class MyComponent {
  @Input() @Required a: number;
}

Пояснение:

Если атрибут a определен - установщик свойства a переопределит себя, и будет использовано значение, переданное атрибуту. В противном случае - после инициализации компонента - в первый раз, когда вы захотите использовать свойство a в своем классе или шаблоне - будет выдано сообщение об ошибке.

Примечание: геттеры/сеттеры хорошо работают в Angular-компонентах/сервисах и т.д., И их можно безопасно использовать таким образом. Но будьте осторожны при использовании этого подхода с чистыми классами вне Angular. Проблема заключается в том, как машинопись преобразует геттеры/сеттеры - они присваиваются свойству prototype класса. В этом случае мы изменяем свойство прототипа, которое будет одинаковым для всех экземпляров класса. Значит, мы можем получить что-то вроде этого:

const instance1 = new ClassStub();
instance1.property = 'some value';
const instance2 = new ClassStub();
console.log(instance2.property); // 'some value'

Ответ 3

Официальный способ 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 не является известным элементом", что не совсем ясно.

Я попробовал подход декоратора, упомянутый ihor, и столкнулся с проблемами, поскольку он применяется к классу (и, следовательно, после компиляции TS для прототипа), а не к экземпляру; это означало, что декоратор запускается только один раз для всех копий компонента, или, по крайней мере, я не смог найти способ заставить его работать для нескольких экземпляров.

Вот документы для выбора селектора. Обратите внимание, что на самом деле он позволяет очень гибко выбирать стиль в стиле CSS (приятное слово).

Я нашел эту рекомендацию в ветке запроса функции Github.

Ответ 4

Вы можете сделать это следующим образом:

constructor() {}
ngOnInit() {
  if (!this.a) throw new Error();
}

Ответ 5

Для меня я должен был сделать это следующим образом:

ngOnInit() { if(!this.hasOwnProperty('a') throw new Error("Attribute 'a' is required"); }

FYI. Если вы хотите использовать директивы @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");
    }
}

Ответ 6

Почему бы не использовать библиотеку @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)}');
  }
}