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);
}
}
Related
The code starts with an initial value in product variable, which is setted into sessionStorage. When i trigger the side-panel (child component), this receive the product.name from params in url, then this component searchs in sessionStorage and updates the product.amount value (and set it to sessionStorage).
The parent component function that i'm trying to invoke from the child component is getProductStatus(); When i update the product.amount value in the side-panel i need to update also the product object in parent component at the same time. This is what i've been trying, Thanks in advance.
Code:
https://stackblitz.com/edit/angular-ivy-npo4z7?file=src%2Fapp%2Fapp.component.html
export class AppComponent {
product: any;
productReturned: any;
constructor() {
this.product = {
name: 'foo',
amount: 1
};
}
ngOnInit() {
this.getProductStatus();
}
getProductStatus(): void {
this.productReturned = this.getStorage();
if (this.productReturned) {
this.product = JSON.parse(this.productReturned);
} else {
this.setStorage();
}
}
setStorage(): void {
sessionStorage.setItem(this.product.name, JSON.stringify(this.product));
}
getStorage() {
return sessionStorage.getItem(this.product.name);
}
reset() {
sessionStorage.clear();
window.location.reload();
}
}
You have two options for data sharing in this case. If you only need the data in your parent component:
In child.component.ts:
#Output() someEvent = new EventEmitter
someFunction(): void {
this.someEvent.emit('Some data...')
}
In parent template:
<app-child (someEvent)="handleSomeEvent($event)"></app-child>
In parent.component.ts:
handleSomeEvent(event: any): void {
// Do something (with the event data) or call any functions in the component
}
If you might need the data in another component aswell, you could make a service bound to the root of the application with a Subject to subscibe to in any unrelated component wherever in your application.
Service:
#Injectable()
export class DataService {
private _data = new BehaviorSubject<SnapshotSelection>(new Data());
private dataStore: { data: any }
get data() {
return this.dataStore.asObservable();
}
updatedDataSelection(data: Data){
this.dataStore.data.push(data);
}
}
Just pass the service in both constructors of receiving and outgoing component.
In ngOnInit() on receiving side:
subscription!: Subscription
...
dataService.data.subscribe(data => {
// Do something when data changes
})
...
ngOnDestroy() {
this.subscription.unsubscribe()
}
Then just use updatedDataSelection() where the changes originate.
I documented on all types of data sharing between components here:
https://github.com/H3AR7B3A7/EarlyAngularProjects/tree/master/dataSharing
For an example on the data service:
https://github.com/H3AR7B3A7/EarlyAngularProjects/tree/master/dataService
I have a component which needs to show the data in the grid on the component/page Load and when a button is clicked from parent component it needs refresh the grid with new data. My component is like below
export class TjlShipdateFilterComponent implements DoCheck {
tljShipDate: ShipDateFilterModel[];
constructor(private psService: ProjectShipmentService) {
}
ngDoCheck() {
// this data is from the service, trying to get it on Page load
}
#Input() filter: ShipDateFilterModel[];
//Load or refresh the data from parent when the button clicked from parent component
ngOnChanges(changes: SimpleChanges) {
}
The ngOnChanges works fine, it gets the data from the parent component and displays when the button is clicked from the parent component. But on load of the page/component the grid it doesn't show anything and says this.psService.tDate; is undefined.
Below is the service where I get the tDate
export class ProjectShipmentService {
......
constructor(service: DataService, private activatedRoute: ActivatedRoute) {
service.get<ShipDateFilterModel[]>(this.entityUrl).subscribe(x => this.tDate = x);
}
I am unsure what am I missing here. How can I achieve this scenario
It happened because when the component is loaded, the request in your service may not completed and the data may not return yet, that why tDate is undefined, try subscribe to it inside your component, also use ngOnInit() instead of ngDoCheck().
In your service:
tDate: Observable<ShipDateFilterModel[]>
constructor(service: DataService, private activatedRoute: ActivatedRoute) {
...
this.tDate = service.get<ShipDateFilterModel[]>(this.entityUrl)
}
In your component:
export class TjlShipdateFilterComponent implements OnInit, OnChanges {
tljShipDate: ShipDateFilterModel[];
constructor(private psService: ProjectShipmentService) {
}
ngOnInit() {
// this data is from the service, trying to get it on Page load
this.psService.tDate.subsribe(x => this.tljShipDate = x);
}
#Input() filter: ShipDateFilterModel[];
//Load or refresh the data from parent when the button clicked from parent component
ngOnChanges(changes: SimpleChanges) {
if (changes.filter && changes.filter.currentValue)
{
this.tljShipDate = this.filter;
}
}
}
You have a couple options here.
NgOnInit will run when the component is created, before it is rendered. This is the most common way to load data on component initialization.
If you need the data even before the component is initialized, then you may need to utilize a Resolver.
Here's an example:
import { Injectable } from '#angular/core'
import { HttpService } from 'services/http.service'
import { Resolve } from '#angular/router'
import { ActivatedRouteSnapshot } from '#angular/router'
#Injectable()
export class DataResolver implements Resolve<any> {
constructor(private http: HttpService) { }
resolve(route: ActivatedRouteSnapshot) {
return this.http.getData(route.params.id);
}
}
Then, in your route config:
{
path: 'data/:id',
component: DataComponent,
resolve: { data: DataResolver }
}
The inclusion of the ActivatedRouteSnapshot is optional, you only need it if you're using route data, like params.
Edit:
Looking at your example closer, is it possible that the ngDoCheck is firing before the psService subscription does?
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
I have a component with 2 direct children, both of which use the event variable that is held within the parent. However, upon changing one of the components which uses a dropdown list from using a <select> to a custom-animated dropdown... I can no longer see the event within this child, despite using the near-identical code.
Parent.ts
event: IEvent;
constructor(private eventService: EventService) {
}
ngOnInit() {
this.subToEventService();
}
subToEventService() {
this.eventService.eventSubject
.subscribe(res => {
this.event = res;
}
}
Child 1 (can see event)
export class ChildOne extends ParentComponent implements OnInit {
constructor(eventService: EventService) {
super(eventService);
}
ngOnInit() {
console.log(this.event);
}
}
Child 2 (cannot see event)
export class ChildTwo extends ParentComponent implements OnInit {
#ViewChild('dropdown') dropdown: ElementRef;
expanded = false;
constructor(eventService: EventService) {
super(eventService);
}
ngOnInit() {
console.log(this.event);
}
toggleDropdown() {
const dropdown = this.dropdown.nativeElement;
if (this.expanded) {
TweenMax.to(dropdown, 0.5, {...});
} else {
TweenMax.to(dropdown, 0.5, {...});
}
this.expanded = !this.expanded;
}
determineStyle() {
const style = this.dropdown.nativeElement.style;
style.height = this.expanded ? 376 : 34;
}
}
In both your child components, you are overriding the ngOnInit method of the ParentComponent class.
You need to call super.ngOnInit(); on both child ngOnInit to subscribe to your subject inside those components.