I am using Angular 10 for a side-project of mine. I have a page that, on page load, retrieves a list of entries (in this case specifically spells
) from the server and renders 1 child-component (<app-spell>
component) per entry on that page. The user can (this is optional) specify a url parameter called name
and if he does and there is an entry for which it is true spell.name === name
then I want to scroll to the component of that entry.
I am stuck trying to get the point where I have access to a list of these children. Ì was planning on using ViewChildren
to get the list of child-components. However, there don't appear to be any proper lifecycle hooks where it has accurate values. Further, even when trying to use document.querySelector (I know ahead of time that I will have a component with id "Arawns-Changing-Scales") I appear to be unable to get my required HTML Elements:
ngAfterContentInit
: You can't access ViewChildren
in general in this hook. document.querySelector
-calls return null.ngAfterViewInit
: The entries haven't been fetched at that point and neither have the child-components been initialized. Thus ViewChildren
returns an empty QueryList
. document.querySelector` returns nullngOnInit
: The child-components haven't been initialized. Thus ViewChildren
returns an empty QueryList
. document.querySelector
returns nullAnd after that I'm out of appropriate seeming lifecycle hooks. I've tried what is suggested here and put my attempt to access my ViewChildren
into a setTimeout callback, this just returned the same as not using a timeout at all. Other questions I found were using clicks or other user-triggered events to fire a scroll, which is not the case for me.
Where can I get some kind of HTMLElements/Components during the loading of the page ?
## spells.component.html
<!-- Spells -->
<div *ngFor="let spell of spells; let i = index">
<app-spell
#spellElements
[id]="spell.name.replaceAll(' ', '-').replace('\'', '')"
[index] = "i"
[cardData] = "spell"
*ngIf="hasSelectedClasses(spell)"
></app-spell>
</div>
## spells.component.ts
@Component({
selector: 'app-spells',
templateUrl: './spells.component.html',
styleUrls: ['./spells.component.scss']
})
export class SpellsComponent implements OnInit, AfterContentInit, AfterViewInit {
panelIsOpenArray: boolean[];
spells: SpellObject[];
@ViewChildren("spellElements") spellElements: QueryList<any>;
constructor(
private spellService: SpellService,
public routingService: RoutingService,
private route: ActivatedRoute,
) { }
ngOnInit(): void {
this.spellService.list().pipe(first()).subscribe(
(spells: SpellObject[]) => {
this.spells = spells;
this.panelIsOpenArray = [];
for (let spell of spells){
this.panelIsOpenArray.push(true);
};
console.log("After init");
this.scrollToSpellInUrl();
},
error => this.routingService.routeToErrorPage(error)
);
}
ngAfterViewInit(): void{
setTimeout(_=> {
console.log("After view init in timeout");
this.scrollToSpellInUrl()
});
console.log("After view init normal")
this.scrollToSpellInUrl();
}
ngAfterContentInit(): void{
console.log("After content init");
this.scrollToSpellInUrl();
}
scrollToSpellInUrl(){
console.log(this.spellElements);
console.log(document.getElementById("Arawns-Changing-Scales");
console.log(document.querySelector("#Arawns-Changing-Scales"));
return;
}
hasSelectedClasses(spell: SpellObject): boolean{
//For all intents and purposes, based on some other data and this
//data this function returns sometimes true sometimes false, more
//is not relevant for this question.
}
}
You can use the ViewChildren annotation to watch for SpellComponents. The annotation exposes a QueryList which is first populated inside the ngAfterViewInit hook. In your case, your are fetching the spells in an asynchronous way and you are not able to get them in time for the ngAfterViewInit hook. However Querylist has a changes
property which is an Observable that will emit any time a SpellComponent is added, removed or moved from the template.
@ViewChildren(SpellComponent)
public spellsComponents: QueryList<SpellComponent>;
public ngAfterViewInit() {
this.spellsComponents.changes.subscribe(queryList => {});
}
On each emit, we can search in the queryList parameter. But before Here is my SpellComponent:
export class SpellComponent implements OnInit {
@Input() name: string;
constructor(public element: ElementRef) {}
ngOnInit() {}
}
We will use the name Input to identify our spell components. I inject ElementRef as it will able us to use the element.scrollIntoView method.
this.spellsComponents.changes.subscribe(queryList => {
const target = queryList.find(cmp => cmp.name === "foo");
target.element.nativeElement.scrollIntoView();
});
Done. Here is a live demo https://stackblitz.com/edit/angular-mmjzwj-jffmyr?file=app.module.ts.