Avatar ·

Передача callback функции в дочерний компонент Angular через @Input как в AngularJS

Привет! Я перехожу с AngularJS на Angular (2+) и никак не могу понять, как правильно передать callback функцию в дочерний компонент. В AngularJS я делал так: ```javascript // Parent // Child scope.$on('save', function(event, data) { // вызываем родительскую функцию scope.onSave({$event: data}); }); ``` Теперь в Angular я пытаюсь передать функцию через @Input: ```typescript // Parent component @Component({ template: `` }) export class ParentComponent { saveHandler(data: any) { console.log('Saved:', data); } } // Child component @Component({...}) export class ChildComponent { @Input() onSave: (data: any) => void; someMethod() { this.onSave({name: 'test'}); // TypeError: this.onSave is not a function } } ``` Но получаю ошибку, что функция не определена. Я читал, что в Angular для этого лучше использовать @Output и EventEmitter, но мне нужно именно передать callback, как в AngularJS. Как правильно это сделать? Может быть, я неправильно задаю тип для @Input? Или это вообще плохая практика в Angular? Подскажите, пожалуйста, как передать функцию в дочерний компонент и вызвать её оттуда. <<>>

Проблема с передачей callback через @Input

Вы столкнулись с типичной ситуацией при миграции с AngularJS на Angular. Ваш код почти правильный, но есть несколько нюансов, которые нужно учесть. Давайте разберем их по порядку.

Почему возникает ошибка?

Ошибка this.onSave is not a function может возникать по нескольким причинам:

  • Контекст выполнения — в Angular при передаче метода класса как callback, контекст this может теряться.
  • Жизненный цикл компонента — если вы вызываете this.onSave до того, как Angular установит входное свойство (например, в конструкторе или ngOnInit до привязки).
  • Типизация — несоответствие сигнатуры функции.

Правильное решение с @Input

Вот как должен выглядеть корректный код:

// ParentComponent
@Component({
  template: `<child [onSave]="saveHandler"></child>`
})
export class ParentComponent {
  // Используем стрелочную функцию для сохранения контекста
  saveHandler = (data: any): void => {
    console.log('Saved:', data);
  }
}

// ChildComponent
@Component({...})
export class ChildComponent implements OnInit {
  @Input() onSave!: (data: any) => void;

  ngOnInit() {
    // Проверяем, что функция определена
    if (typeof this.onSave === 'function') {
      this.onSave({name: 'test'});
    }
  }
}

Ключевые моменты

  • Используйте стрелочные функции для методов, которые передаются как callback — это автоматически привязывает контекст this.
  • Добавьте проверку typeof this.onSave === 'function' перед вызовом, чтобы избежать ошибок, если свойство еще не установлено.
  • Используйте оператор definite assignment assertion (!) в типе, чтобы показать TypeScript, что свойство будет инициализировано позже.

Рекомендуемый подход: @Output и EventEmitter

Хотя технически можно передавать функции через @Input, в Angular это антипаттерн. Правильный способ — использовать @Output() и EventEmitter:

// ParentComponent
@Component({
  template: `<child (save)="saveHandler($event)"></child>`
})
export class ParentComponent {
  saveHandler(data: any) {
    console.log('Saved:', data);
  }
}

// ChildComponent
@Component({...})
export class ChildComponent {
  @Output() save = new EventEmitter<any>();

  someMethod() {
    this.save.emit({name: 'test'});
  }
}

Этот подход:

  • Соответствует архитектуре Angular и принципу однонаправленного потока данных.
  • Не требует дополнительных проверок на существование функции.
  • Лучше поддерживается инструментами разработки (например, Ivy компилятором).
  • Обеспечивает лучшую производительность за счет оптимизации Change Detection.

Когда @Input с функцией оправдан?

Иногда передача функции через @Input может быть удобна для:

  • Передачи статических функций-утилит (например, форматирования).
  • Создания кастомных декораторов или HOC-компонентов.
  • Интеграции с библиотеками, которые ожидают callback (например, для drag-and-drop).

Но в 95% случаев лучше использовать @Output.

<<>>

Альтернативный подход: использование сервиса для обмена данными

Если вам нужно передать callback в глубоко вложенный компонент (через несколько уровней), можно использовать сервис с Subject или BehaviorSubject:

// save.service.ts
@Injectable({ providedIn: 'root' })
export class SaveService {
  private saveSubject = new Subject<any>();
  save$ = this.saveSubject.asObservable();

  emitSave(data: any) {
    this.saveSubject.next(data);
  }
}

// ParentComponent
export class ParentComponent {
  constructor(private saveService: SaveService) {
    this.saveService.save$.subscribe(data => {
      console.log('Saved:', data);
    });
  }
}

// ChildComponent
export class ChildComponent {
  constructor(private saveService: SaveService) {}

  someMethod() {
    this.saveService.emitSave({name: 'test'});
  }
}

Этот подход полезен, когда:

  • Компоненты находятся в разных модулях и не связаны иерархически.
  • Нужно передать событие через 3+ уровня вложенности (избегая "prop drilling").
  • Требуется, чтобы несколько компонентов реагировали на одно событие.

Недостаток: сервис создает глобальное состояние, что может усложнить отладку. Используйте этот метод только когда @Output становится неудобным из-за глубокой вложенности.