On an Angular project, I want to restrict the viewport of an IntersectionObserver to a specific part of a DOM.
I define the element to use as root with an id:
<div id="container">
<div class="page-list">
<infinite-scroll-page (scrolled)="onInfiniteScroll()">
...
</infinite-scroll-page>
</div>
</div>
In the corresponding component I define the root using getElementById:
export class InfiniteScrollPageComponent implements OnDestroy {
#Input() options: {root, threshold: number} = {root: document.getElementById('container'), threshold: 1};
#ViewChild(AnchorDirectivePage, {read: ElementRef, static: false}) anchor: ElementRef<HTMLElement>;
private observer: IntersectionObserver;
constructor(private host: ElementRef) { }
get element() {
return this.host.nativeElement;
}
ngAfterViewInit() {
const options = {
root: document.getElementById('container'),
...this.options
};
console.log("options: ", JSON.stringify(options));
//...
But the root loged is always null.
What do I do wrong ?
Firstly, your spread operator is the wrong way around, so unfortunately you're overwriting your root assignment immediately after it is set with the default value in the #Input() (which as far as I can see is not actually used as an input?).
Reversing this might be all you need to fix the issue:
const options = {
root: document.getElementById('container'),
...this.options
};
should be
const options = {
...this.options,
root: document.getElementById('container')
};
Secondly, I wonder whether it would make more sense to use #ViewChild and pass a reference to the container element into the InfiniteScrollPageComponent from the parent.
parent.component.html
<div
#Container
id="container">
<div class="page-list">
<infinite-scroll-page
*ngIf="containerRef"
[containerRef]="containerRef"
(scrolled)="onInfiniteScroll()">
...
</infinite-scroll-page>
</div>
</div>
parent.component.ts
export class ParentComponent {
#ViewChild('Container') containerRef: ElementRef<HTMLDivElement>;
}
infinite-page-component.component.ts
export class InfiniteScrollPageComponent {
#Input() containerRef: ElementRef<HTMLDivElement>;
ngAfterViewInit() {
const options = {
...this.options
root: containerRef.nativeElement,
};
}
Related
In my code, I have a component that holds all the data retrieved from firebase. Including a variable called "currentTurnName" which gets passed from the parent to the child component. That variable is used to render a twitch channel inside the child component but doesn't update when the data changes.. I followed a few stack overflow guide that talk about using the *ngIf directive like <childComponent *ngIf"currentTurnName"> to stop the child component from loading until the data is retrieved- it works on page load, except it doesn't work when that variable is passed in new data asynchronously.
Child Component
export class TwitchVideoComponent implements OnInit {
constructor() {
}
player: any;
#Input() currentTurnName: any;
ngOnInit(): void {
var options = {
width: 1080,
height: 720,
channel: this.currentTurnName,
};
this.player = new Twitch.Player("<player div ID>", options)
this.player.setVolume(0.5);
console.log(this.currentTurnName);
}
// ngOnChanges(changes: SimpleChanges) {
// if (this.currentTurnName) {
// this.player.setChannel(this.currentTurnName);
// }
// }
}
Child Template
<div id="<player div ID>" class="twitch"></div>
Parent component
export class GameRoomComponent implements OnInit {
public users$: Observable<User[]>
currentUser: Observable<CurrentUser[]>;
constructor(private usersService: UsersService, private afs: AngularFirestore) {
this.currentTurn = afs.collection<CurrentUser>('currentPlayerDB').valueChanges().pipe(
tap(res => {
this.currentTurnName = res[0].user;
console.log(this.currentTurnName);
})
).subscribe();
}
currentTurn: any;
items: any;
currentTurnName : any;
Parent Template
<app-twitch-video *ngIf="currentTurnName"
[currentTurnName]="currentTurnName" >
This somewhat talks about my issue, but mine is more complex considering the data is changed
Angular 2 child component loads before data passed with #input()
You could use Angular's property setters. The documentation can be found here in this section (Intercept input property changes with a setter): https://angular.io/guide/component-interaction
Here is a snippet using your code:
export class TwitchVideoComponent implements OnInit {
constructor() {}
player: any;
#Input() set currentTurnName(name) {
this._currentTurnName = name;
console.log('This should fire every time this Input updates', this._currentTurnName);
if (this._currentTurnName) {
this.player.setChannel(this._currentTurnName);
}
}
public _currentTurnName: any;
ngOnInit(): void {
var options = {
width: 1080,
height: 720,
channel: this._currentTurnName,
};
this.player = new Twitch.Player("<player div ID>", options)
this.player.setVolume(0.5);
console.log(this._currentTurnName);
}
}
I'm trying to build a list of cards which may contain different components; So for example I have the following array of objects:
{
title: 'Title',
descrption: 'Description',
template: 'table',
},
{
title: 'Title',
descrption: 'Description',
template: 'chart',
}
I get this array as a response from a service, then I need to match each of thos objects to a component based on the template property, so for example, the first item should match to the TableComponent and the second one to the ChartComponent;
I'm trying to follow the Angular Docs regarding Dynamic Component Loading, but I'm not sure how tell the method how to match each object in the array to a specific component.
In my parent component I have made an anchor point where the components should load with a directive:
<ng-template appCheckpointHost></ng-template>
And I'm trying to use the ComponentFactoryResolver as it shows in the example.
loadComponent() {
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(ChartCheckpointComponent);
const viewContainerRef = this.checkHost.viewContainerRef;
}
The example shows a scenario in which the "service" runs every three seconds, gets a random item, and shows it; but what I'm trying to do instead is to fetch all the items when the parent component loads, and render each item with its respective component.
Any ideas to get this to work?
You can create a dictionary like:
const nameToComponentMap = {
table: TableComponent,
chart: ChartComponent
};
And then just use this dictionary to determine which component should be rendered depending on the template property of particular item in your items array:
const componentTypeToRender = nameToComponentMap[item.template];
this.componentFactoryResolver.resolveComponentFactory(componentTypeToRender);
You can view my blog here
First I will need to create a directive to reference to our template instance in view
import { Directive, ViewContainerRef } from "#angular/core";
#Directive({
selector: "[dynamic-ref]"
})
export class DynamicDirective {
constructor(public viewContainerRef: ViewContainerRef) {}
}
Then we simply put the directive inside the view like this
<ng-template dynamic-ref></ng-template>
We put the directive dynamic-ref to ng-content so that we can let Angular know where the component will be render
Next I will create a service to generate the component and destroy it
import {
ComponentFactoryResolver,
Injectable,
ComponentRef
} from "#angular/core";
#Injectable()
export class ComponentFactoryService {
private componentRef: ComponentRef<any>;
constructor(private componentFactoryResolver: ComponentFactoryResolver) {}
createComponent(
componentInstance: any,
viewContainer: any
): ComponentRef<any> {
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(
componentInstance
);
const viewContainerRef = viewContainer.viewContainerRef;
viewContainerRef.clear();
this.componentRef = viewContainerRef.createComponent(componentFactory);
return this.componentRef;
}
destroyComponent() {
if (this.componentRef) {
this.componentRef.destroy();
}
}
}
Finally in our component we can call the service like this
#ViewChild(DynamicDirective) dynamic: DynamicDirective;
constructor(
private componentFactoryService: ComponentFactoryService
) {
}
ngOnInit(){
const dynamiCreateComponent = this.componentFactoryService.createComponent(TestComponent, this.dynamic);
(<TestComponent>dynamiCreateComponent.instance).data = 1;
(<TestComponent>dynamiCreateComponent.instance).eventOutput.subscribe(x => console.log(x));
}
ngOnDestroy(){
this.componentFactoryService.destroyComponent();
}
/////////////////////////////////
export class TestComponent {
#Input() data;
#Output() eventOutput: EventEmitter<any> = new EventEmitter<any>();
onBtnClick() {
this.eventOutput.emit("Button is click");
}
}
I Want to know how to create nested dynamic components and maintains its parent child relationship.
For example, I have data like this,
- A
--A.1
--A.2
-B
--B.1
-C
I wanted to create the component like this,
<A>
<A1></A1>
<A2></A2>
</A>
<B>
<B1></B1>
</B>
<C></C>
But with my code I could only create parent component or child component. But not both.
Below is my code,
setRootViewContainerRef(view: ViewContainerRef): void {
this.rootViewContainer = view;
}
createComponent(content: any, type: any) {
console.log(content);
if (content.child && content.child.length > 0) {
content.child.forEach(type => {
const typeP = this.contentMappings[type.type];
this.createComponent(type, typeP);
});
} else {
this.renderComp(content,type)
}
}
renderComp(content,type) {
if (!type) {
return
}
this.componentFactory = this.componentFactoryResolver.resolveComponentFactory(type);
this.componentReference = this.rootViewContainer.createComponent(this.componentFactory);
if (this.componentReference.instance.contentOnCreate) {
this.componentReference.instance.contentOnCreate(content);
}
}
With this code, I get this output.
Link to working example, StackBlitz
Please help me to resolve this issue.
Updated.
Even after adding the viewChild, It still throws the viewchild not defined.
Refer this image, In the component.instance I'm not seeing the view child element.
Updated stackblitz link https://stackblitz.com/edit/angular-dynamic-new-mepwch?file=src/app/content/a/a.component.ts
You should create ViewContainer on each level that is going to render child components:
a.component.html
<p>
a works!
</p>
<ng-container #container></ng-container>
a.component.ts
export class AComponent implements OnInit {
#ViewChild('container', { read: ViewContainerRef, static: true }) embeddedContainer: ViewContainerRef;
And then render component to dedicated container:
create-dynamic-component.service.ts
#Injectable()
export class CreateDynamicComponentService {
constructor(
private componentFactoryResolver: ComponentFactoryResolver,
#Inject(CONTENT_MAPPINGS) private contentMappings: any,
private inlineService: InlineService
) { }
createComponent(content: any, type: any, vcRef) {
const componentRef = this.renderComp(content, type, vcRef)
if (content.child && content.child.length) {
if (!componentRef.instance.embeddedContainer) {
const cmpName = componentRef.instance.constructor.name;
throw new TypeError(`Trying to render embedded content. ${cmpName} must have #ViewChild() embeddedContainer defined`);
}
content.child.forEach(type => {
const typeP = this.contentMappings[type.type];
this.createComponent(type, typeP, componentRef.instance.embeddedContainer);
});
}
}
renderComp(content,type, vcRef: ViewContainerRef) {
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(type);
const componentRef = vcRef.createComponent<any>(componentFactory);
if (componentRef.instance.contentOnCreate) {
componentRef.instance.contentOnCreate(content);
}
return componentRef;
}
}
Note how renderComp method takes ViewContainerRef from the component with children:
this.createComponent(type, typeP, componentRef.instance.embeddedContainer);
Forked Stackblitz
import { Component, Prop } from '#stencil/core';
#Component({
tag: 'my-component',
styleUrl: 'my-component.css',
shadow: true
})
export class MyComponent {
#Prop() first: string;
#Prop() last: string;
getElementHere() {
// how can I get the div here?
}
render() {
return (
<div>
Hello, World! I'm {this.first} {this.last}
</div>
);
}
}
I want to get the DOM element just like in native JS. How do you do this in Stencil? getElementById does not work.
To expand on Fernando's answer, the #Element decorator binds the component's root element to this property. It's important to note a few properties of this approach:
The #Element bound property is only available after the component has been loaded (componentDidLoad).
Because the element is a standard HTMLElement, you can access elements within your current component using the standard .querySelector(...) or .querySelectorAll(...) methods to retrieve and manipulate them.
Here is an example showing when the element is accessible, and how to manipulate nodes within this element (correct as of stencil 0.7.24):
import { Component, Element } from '#stencil/core';
#Component({
tag: 'my-component'
})
export class MyComponent {
#Element() private element: HTMLElement;
private data: string[];
constructor() {
this.data = ['one', 'two', 'three', 'four'];
console.log(this.element); // outputs undefined
}
// child elements will only exist once the component has finished loading
componentDidLoad() {
console.log(this.element); // outputs HTMLElement <my-component ...
// loop over NodeList as per https://css-tricks.com/snippets/javascript/loop-queryselectorall-matches/
const list = this.element.querySelectorAll('li.my-list');
[].forEach.call(list, li => li.style.color = 'red');
}
render() {
return (
<div class="my-component">
<ul class="my-list">
{ this.data.map(count => <li>{count}</li>)}
</ul>
</div>
);
}
}
From the official docs
In cases where you need to get a direct reference to an element, like you would normally do with document.querySelector, you might want to use a ref in JSX.
So in your case:
import { Component, Prop } from '#stencil/core';
#Component({
tag: 'my-component',
styleUrl: 'my-component.css',
shadow: true
})
export class MyComponent {
#Prop() first: string;
#Prop() last: string;
divElement!: HTMLElement; // define a variable for html element
getElementHere() {
this.divElement // this will refer to your <div> element
}
render() {
return (
<div ref={(el) => this.divElement= el as HTMLElement}> // add a ref here
Hello, World! I'm {this.first} {this.last}
</div>
);
}
}
you can get the current HTML element adding this into your component as property:
#Element() myElement: HTMLElement;
You can read more about this here
Hope this helps you :)
I am testing a simple recursive Treeview component in angular4 using the below code. But whenever I try to expand the children view on toggle().
I am getting an exception error:
ERROR TypeError: _v.context.$implicit.toggle is not a function
Thanks
tree-view.component.ts
export class TreeViewComponent implements OnInit {
constructor() { }
ngOnInit() {}
#Input() directories: Directory[];
}
tree-view.component.html
<ul>
<li *ngFor="let dir of directories">
<label (click)="dir.toggle()"> {{ dir.title }}</label>
<div *ngIf="dir.expanded">
<tree-view [locations]="dir.children"></tree-view>
</div>
</li>
</ul>
Directory.ts
export class Directory{
title: string;
children: Directory[]
expanded = true;
checked = false;
constructor() {
}
toggle() {
this.expanded = !this.expanded;
}
getIcon() {
if (this.expanded) {
return '-';
}
return '+';
}
}
Like yurzui suggested, in case you are just typing your data, you don't have class instances, so the method toggle isn't available. If you truly want to have your array with instances of your Directory class, add the properties to constructor and when you get the data from api, create instances of your objects, so shortened version:
export class Directory {
title: string;
expanded: boolean;
constructor(title: string, expanded: boolean) {
this.title = title;
this.expanded = expanded
}
}
and in your api call:
return this.httpClient.get<Directory[]>('url')
.map(res => res.map(x => new Directory(x.title, x.expanded)))
Now you have access to the toggle method.
StackBlitz