My app is passing data from a parent component to a child component view but the constructor in the child component (controller) keeps saying unknown
. It seems like the @Input
decorator is making the variable inaccessible to the constructor.
The data is going from a child component to a parent component and then to another child component. I could make a service to pass the data but I suspect I'd run into the same problem.
The data originates in this child component. The class L2Language
has three properties. The default is English
, which is emitted OnInit
. The data is also emitted when the user changes the language to Spanish
.
import { Component, OnInit, Output, EventEmitter } from '@angular/core';
export type L2Language = {
short: string;
long: string;
videos: string;
};
export class HomeToolbarComponent implements OnInit {
@Output() L2LanguageEvent = new EventEmitter<L2Language>();
ngOnInit(): void {
this.L2LanguageEvent.emit(this.language);
}
changeL2Language(lang: string) {
switch (true) {
case (lang === 'en'):
this.language.short = 'en';
this.language.long = 'English';
this.language.videos = 'English_Videos';
break;
case (lang === 'es'):
this.language.short = 'es';
this.language.long = 'Spanish';
this.language.videos = 'Spanish_Videos';
break;
default:
console.log("Error in switch-case.");
}
this.L2LanguageEvent.emit(this.language);
}
}
The data moves from the first child home-toolbar
to the parent in the parent view. The data also move from the parent to the second child videos-vocabulary
in the parent view.
<div>
<app-home-toolbar (L2LanguageEvent)="L2LanguageEventHandler($event)"></app-home-toolbar>
<app-videos-vocabulary [language]="language"></app-videos-vocabulary>
</div>
The parent controller binds the data from the parent view and then calls the Firebase database successfully, using the data from the first child.
import { Component, OnInit } from '@angular/core';
import { AngularFirestore } from '@angular/fire/compat/firestore';
import { Observable } from 'rxjs';
import { L2Language } from './home-toolbar/home-toolbar.component';
export class HomeComponent implements OnInit {
language: L2Language;
videos: Observable<any[]>;
L2LanguageEventHandler(L2LanguageEvent: L2Language) {
console.log(L2LanguageEvent.short);
this.language = L2LanguageEvent;
this.videos = this.firestore.collection('Videos').doc(L2LanguageEvent.long).collection(L2LanguageEvent.videos).valueChanges();
}
}
That works fine.
To test, I display the data in the second child view:
{{language | json}} {{language.short}}
The data displays on initialization and changes when the user changes the language.
Here's the part that isn't working. I need to call Firebase from the second child component controller, from the constructor.
import { Component, Input } from '@angular/core';
import { AngularFirestore } from '@angular/fire/compat/firestore';
import { Observable } from 'rxjs';
import { L2Language } from '../home-toolbar/home-toolbar.component';
export class VideosVocabularyComponent{
@Input() language: L2Language;
videos: Observable<any[]>;
constructor (
public firestore: AngularFirestore,
// public language: L2Language,
) {
console.log(this.language);
this.videos = firestore.collection('Videos').doc(this.language.long).collection(this.language.videos).valueChanges(); }
}
The code compiles but console.log(this.language);
logs unknown
. The Firebase call throws an error Cannot read properties of undefined (reading 'long')
.
this.videos
accesses the variable videos
from inside the constructor. Why doesn't this.language
access the variable language
from inside the constructor?
It seems like the @Input
decorator is making the variable language
inaccessible to the constructor.
The documentation Dependency injection in Angular says
To inject a dependency in a component's constructor(), supply a constructor argument with the dependency type. The following example specifies the HeroService in the HeroListComponent constructor. The type of heroService is HeroService.
constructor(heroService: HeroService)
I tried this:
constructor (
public firestore: AngularFirestore,
public language: L2Language,
) {
console.log(this.language);
this.videos = firestore.collection('Videos').doc('English').collection('English_Videos').valueChanges();
}
That threw this error:
No suitable injection token for parameter 'language' of class 'VideosVocabularyComponent'.
Consider using the @Inject decorator to specify an injection token.
CodePudding user response:
The component's inputs (@Input
) are not available at constructor
, you can move the language @Input
to one of the component's lifecycle hooks such as ngOnInit
or ngOnChanges
based on your requirements.
The ngOnChanges() method is your first opportunity to access those properties. Angular calls ngOnChanges() before ngOnInit(), but also many times after that. It only calls ngOnInit() once.
Read more here: https://angular.io/guide/lifecycle-hooks#initializing-a-component-or-directive
CodePudding user response:
You won't have your input passed in the constructor. Your input is passed after an instance is created and it can be changed at any time during the lifetime of your component so you should probably cater for that (reloading the videos for the newly selected language).
I would refactor VideosVocabularyComponent
to something like this:
import { Input } from '@angular/core';
import { of } from 'rxjs';
import { AngularFirestore } from '@angular/fire/compat/firestore';
import { L2Language } from '../home-toolbar/home-toolbar.component';
export class VideosVocabularyComponent {
private language$ = new BehaviorSubject<L2Language | null>(null);
@Input()
get language() {
return this.language$.value;
}
set language(value: L2Language) {
this.language$.next(value);
}
readonly videos = this.language$.pipe(
switchMap(l => l
? firestore.collection('Videos').doc(l.long).collection(l.videos).valueChanges()
: of([])
),
);
constructor(
public firestore: AngularFirestore,
) {
}
}