Reputation: 10930
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.
Upvotes: 0
Views: 2007
Reputation: 10930
The answer is, build a service. What I learned is, data can pass between components passively or actively. Passive means, for example, that you want elements of the view to hide or show as data is passed to a component. Active means that you want to call a method when data is passed to a component.
In this case, I want to call the database when the user clicks between English and Spanish. The view presents Spanish videos when the user wants to learn Spanish, or English videos when the user wants to learn English.
Passing data between parent and child components with an event emitter is passive. I don't know of a way to make a method fire when new data arrives.
Passing data around with a service is active. You can set a method to fire when new data arrives.
I built a LanguageL2Service
, put it in the constructor
, and it works:
import { Component, OnInit, Input, OnChanges, Inject, } from '@angular/core';
// Firebase
import { AngularFirestore } from '@angular/fire/compat/firestore';
import { Observable } from 'rxjs';
// types
import { L2Language } from '../home-toolbar/home-toolbar.component';
// service
import { LanguageL2Service } from '../../services//languageL2/language-l2.service';
export declare type LearningModes = 'videos' | 'vocabulary' | 'wordSearch';
@Component({
selector: 'app-videos-vocabulary',
templateUrl: './videos-vocabulary.component.html',
styleUrls: ['./videos-vocabulary.component.css']
})
export class VideosVocabularyComponent implements OnInit, OnChanges{
@Input() language: L2Language;
videos: Observable<any[]>;
constructor (
public firestore: AngularFirestore,
@Inject(LanguageL2Service) private languageL2Service: LanguageL2Service
) {
console.log(this.language); // undefined
this.languageL2Service.getLanguage().subscribe((lang: L2Language) => {
this.language = lang;
console.log(this.language); // data object is here
this.videos = this.firestore.collection('Videos').doc(this.language.long).collection(this.language.videos).valueChanges();
});
}
}
The first console.log(this.language);
logs unknown
, the second (in the service) logs the data object.
The service also works in ngOnChanges
and in ngOnInit
:
export class VideosVocabularyComponent implements OnInit, OnChanges{
@Input() language: L2Language;
videos: Observable<any[]>;
constructor (
public firestore: AngularFirestore,
@Inject(LanguageL2Service) private languageL2Service: LanguageL2Service
) { }
ngOnChanges() { // or ngOnInit
console.log(this.language);
this.languageL2Service.getLanguage().subscribe((lang: L2Language) => {
this.language = lang;
console.log(this.language);
this.videos = this.firestore.collection('Videos').doc(this.language.long).collection(this.language.videos).valueChanges();
});
}
}
What doesn't work is referencing the variable language
with its @Input
decorator from the constructor
. This doesn't even compile:
export class VideosVocabularyComponent implements OnInit, OnChanges{
@Input() language: L2Language;
videos: Observable<any[]>;
constructor (
public firestore: AngularFirestore,
@Inject(LanguageL2Service) private languageL2Service: LanguageL2Service
) {
this.videos = this.firestore.collection('Videos').doc(this.language.long).collection(this.language.videos).valueChanges();
}
}
Calling the variable language
from ngOnInit
and ngOnChanges
compiles and gets the initial data, that the default language is English. But changing the language to Spanish doesn't get new data.
What gets new data is to click a button in the view that runs a handler function.
export class VideosVocabularyComponent implements OnInit, OnChanges{
@Input() language: L2Language;
videos: Observable<any[]>;
constructor (
public firestore: AngularFirestore,
@Inject(LanguageL2Service) private languageL2Service: LanguageL2Service
) {
console.log(this.language);
}
ngOnChanges() {
console.log(this.language);
this.videos = this.firestore.collection('Videos').doc(this.language.long).collection(this.language.videos).valueChanges(); // doesn't get the data
}
ngOnInit() {
console.log(this.language);
this.videos = this.firestore.collection('Videos').doc(this.language.long).collection(this.language.videos).valueChanges();
}
clickMe() {
console.log(this.language);
this.videos = this.firestore.collection('Videos').doc(this.language.long).collection(this.language.videos).valueChanges();
}
}
The console logs fire in the order above: constructor
, ngOnChanges
, ngOnInit
, clickMe
.
Upvotes: 0
Reputation: 8295
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,
) {
}
}
Upvotes: 1
Reputation: 6706
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
Upvotes: 2