Как реализовать межкомпонентную коммуникацию без Input/Output?

В Angular для межкомпонентной коммуникации без использования @Input() и @Output() можно применять несколько архитектурных подходов. Это особенно актуально при взаимодействии между компонентами, не связанными напрямую через иерархию DOM (например, sibling-компоненты или компоненты в разных модулях). Ключевые техники включают: общие сервисы с Subject/BehaviorSubject, сервисы-событийные шины, ViewChild, ContentChild, локальные инжекторы, глобальные хранилища и реактивные библиотеки (RxJS, Signals, NgRx и т.д.).

1. Сервис-посредник (Shared Service)

Один из самых распространённых способов — использование сервиса, который инжектируется в оба компонента и использует Subject, BehaviorSubject или ReplaySubject из RxJS.

Пример

@Injectable({ providedIn: 'root' })
export class CommunicationService {
private dataSubject = new BehaviorSubject<string>('initial');
data$ = this.dataSubject.asObservable();
setData(value: string) {
this.dataSubject.next(value);
}
}

В одном компоненте:

constructor(private comm: CommunicationService) {}
updateValue() {
this.comm.setData('New value');
}

В другом компоненте:

constructor(private comm: CommunicationService) {}
ngOnInit() {
this.comm.data$.subscribe(value => {
this.received = value;
});
}

Таким образом достигается реактивная передача данных без @Input/@Output.

2. EventBus (Custom Event System)

Если необходимо передавать события, а не только данные, можно реализовать глобальную событийную шину на основе Subject.

@Injectable({ providedIn: 'root' })
export class EventBusService {
private event$ = new Subject<{ name: string, payload?: any }>();
emit(eventName: string, payload?: any) {
this.event$.next({ name: eventName, payload });
}
on(eventName: string): Observable<any> {
return this.event$.pipe(
filter(e => e.name === eventName),
map(e => e.payload)
);
}
}

Пример использования:

// Component A
this.eventBus.emit('user-logged-in', userData);
// Component B
this.eventBus.on('user-logged-in').subscribe(user => {
this.user = user;
});

Это особенно удобно для loosely-coupled компонентов и глобальных событий.

3. ViewChild и родительский доступ к дочернему компоненту

Вместо Input/Output родитель может напрямую обращаться к дочернему компоненту.

@ViewChild(ChildComponent) childComp!: ChildComponent;
ngAfterViewInit() {
this.childComp.doSomething();
}

Такой подход не требует Output и позволяет родителю напрямую вызывать методы дочернего компонента или получать доступ к его свойствам.

4. ContentChild и доступ к проецируемому компоненту

Если используется ng-content, можно обращаться к вложенному компоненту через ContentChild.

@ContentChild(MyInnerComponent) projectedComp!: MyInnerComponent;
ngAfterContentInit() {
this.projectedComp.someMethod();
}

Это позволяет компоненту-контейнеру взаимодействовать с вложенным содержимым.

5. Service Locator через Injector

Можно использовать Injector или EnvironmentInjector, чтобы динамически получить доступ к сервисам или компонентам.

constructor(private injector: Injector) {}
ngOnInit() {
const service = this.injector.get(MyService);
service.someMethod();
}

Также можно инжектировать компонент в динамический компонент, созданный через ComponentFactoryResolver.

6. NgRx или Signal Store

В крупных приложениях удобнее использовать глобальное хранилище.

Пример (NgRx):

  • Компонент A: store.dispatch(login({ user }))

  • Компонент B: store.select(selectUser).subscribe(...)

Пример (Signal Store):

const user = signal<User | null>(null);
setUser(u: User) { user.set(u); }

Компоненты подписываются на user() и автоматически реагируют на изменения без Input/Output.

7. Dependency Injection на уровне компонента

Можно предоставить отдельный сервис на уровне компонента и инжектировать его в дочерние компоненты.

@Component({
providers: \[LocalService\]
})
export class ParentComponent {}

Теперь LocalService будет разделяться только между ParentComponent и его потомками.

8. LocalStorage / SessionStorage как буфер обмена

В некоторых случаях обмен данными может происходить через браузерное хранилище:

localStorage.setItem('theme', 'dark');
// другой компонент
const theme = localStorage.getItem('theme');

Дополнительно можно наблюдать window.addEventListener('storage', ...) для реактивных обновлений.

9. Router Navigation + NavigationExtras

При переходе между маршрутами можно передать данные через NavigationExtras:

this.router.navigate(\['/target'\], { state: { data: 'value' } });

В компоненте-получателе:

const data = this.router.getCurrentNavigation()?.extras.state?.\['data'\];

10. InjectionToken как глобальный доступ к данным

Можно создать глобальный InjectionToken:

export const APP_CONFIG = new InjectionToken<AppConfig>('app.config');
@NgModule({
providers: \[{ provide: APP_CONFIG, useValue: { title: 'Demo' } }\]
})

Затем инжектировать в компоненты без Input:

constructor(@Inject(APP_CONFIG) private config: AppConfig) {}

11. Portals (CDK)

CDK Portals позволяют отобразить один компонент внутри другого без связи по DOM-иерархии.

const portal = new ComponentPortal(MyComponent);
this.portalOutlet.attachComponentPortal(portal);

Коммуникация происходит через инжектируемые сервисы или @Input() на сам портал.

12. Signals (Angular 17+)

Signals позволяют организовать централизованное реактивное состояние.

const counter = signal(0);
const double = computed(() => counter() \* 2);

Оба компонента могут использовать один и тот же signal, независимо от их иерархии.

Эти техники позволяют гибко управлять связями между Angular-компонентами в зависимости от архитектуры, размера и специфики проекта.