Accessing child component inside modal using ViewChild in Angular - javascript

I have a PrimeNG table as a child component and it has the following code:
child-component.html
<p-table
[value]="data"
[autoLayout]="true"
[loading]="loading"
[(selection)]="selectedItems" #dt
>
child-component.ts
#Component({
selector: "manager-grid",
templateUrl: "./grid.component.html",
styleUrls: ["./grid.component.scss"],
encapsulation: ViewEncapsulation.None
})
export class GridComponent implements OnInit {
public _data: Array<any>;
public _settings: any;
public _loading: boolean;
public total = 100;
public selectedItems: any[];
public _columnFilters: any;
#ViewChild('dt') table: Table;
Now I am including this component in parent components as follows:
<manager-grid [data]="data" [settings]="tableSettings" [loading]="isLoading"></manager-grid>
The child component is added in a modal and so when I try to access the selectedItems variable, it is returning undefined. I am using following code for this :
#ViewChild(GridComponent) gridComponent: GridComponent;
const items = this.gridComponent.selectedItems;
I am using NG Bootstrap modal and I think the issue is that, when the page is initialized, the child component is not part of the DOM as it is in modal. How can I access the element inside the modal ? Any workaround ?

If you have a method for opening the modal, access selectedItems after the modal is opened. You may try the following -
openModal() {
// open modal code goes here
if (this.gridComponent) {
items = this.gridComponent.selectedItems;
}
}
If it does not work, you may try settimeout -
setTimeout(() => {
if (this.gridComponent) {
items = this.gridComponent.selectedItems;
}
},0);

Related

Angular 2+ : Get Reference of appcomponent div in another components

I have components called app.component which is the main component in the angular project.
Navigation to customer component is done by routing.
And
Folder structer
src\app
- app.component.html
- app.component.ts
and
src\app\components\customer
- customer.component.html
- customer.component.ts
In my app.component.html
<div class="top-container" #topContainerRef>
<router-outlet></router-outlet>
</div>
In my customer.component.ts
I want to get reference of the top most container div which is contained in app.components
I want to replace
document.getElementsByClassName('top-container')[0].scrollTop = some values
with something similar to
#ViewChild('topContainerRef', { read: ElementRef, static: false }) topContainerRef: ElementRef;
this.topContainerRef.nativeElement.scrollTop= "some value" //here the topContainerRef is undefined
Is there any way i can use elementRef instead of classname or Id's.
You cannot use ViewChild for the #topContainerRef to get a reference of this element, because it is not rendered by your CustomerComponent.
You either need to get the reference of this element inside the app component itself and find a way to pass it to all the other children that might need it (not recommended).
Or you can just build a service and use that to "request" the scrollTop change by whichever component has access to this element (in your case the app component).
I would do it something like this:
export class AppContainerService {
private scrollTopSource = new ReplaySubject<number>(1);
public scrollTop$ = this.scrollTopSource.asObservable();
public updateScrollTop(value: number) {
this.scrollTopSource.next(value);
}
}
Inside your CustomerComponent:
public class CustomerComponent implements OnInit {
// ...
constructor(private containerService: AppContainerService) {
}
ngOnInit() {
this.containerService.updateScrollTop(/* whatever value you need */);
}
// ...
}
And finally, the AppComponent that will react to the scrollTop changes:
export class AppComponent implements AfterViewInit {
#ViewChild('topContainerRef', { read: ElementRef, static: false }) topContainerRef: ElementRef;
private subscriptions = new Subscription();
constructor(private containerService: AppContainerService) {
}
ngAfterViewInit() {
this.subscriptions.add(this.containerService.scrollTop$.subscribe((value: number) => {
this.topContainerRef.nativeElement.scrollTop = value;
}));
}
ngOnDestroy() {
this.subscriptions.unsubscribe();
}
}
Don't forget about unsubscribing inside ngOnDestroy. This is important so that you don't have memory leaks

Programmatically adding components at dynamic positions in Angular

My starting point is some kind of HTML snippet, which is loaded at runtime from my backend, with placeholder tags:
<table>
<tbody>
<tr>
<td>
<span>Col1</span>
</td>
<td>
<span class="placeholderClass" data-placeholderdata="xy"></span>
</td>
</tr>
</tbody>
</table>
I want to display the HTML snippet but replace all <span class="placeholderClass"/> elements with my own angular component.
So far I read about the Dynamic component loader from angular, but as far as I understood this only allows my to load an dynamic component on a fixed position, and not a fixed component on a dynamic position.
Also I tried following post:
Angular 2 Dynamically insert a component into a specific DOM node without using ViewContainerRef
Section Component is my main component where I store the HTML and Value Component the component I want to replace.
Section Component:
HTML
<div [innerHTML]=sectionSnippet></div>
TS
#Component({
selector: 'section-view',
templateUrl: './section.component.html',
styleUrls: ['./section.component.sass'],
encapsulation: ViewEncapsulation.ShadowDom
})
export class SectionComponent implements OnInit {
#Input() sectionSnippetInput = '';
constructor(private sanitizer: DomSanitizer,
private resolver: ComponentFactoryResolver,
private injector: Injector,
private app: ApplicationRef) {
}
ngOnInit(): void {
this.replaceTags();
}
private replaceTags() {
let temp:HTMLDivElement = document.createElement('div');
temp.innerHTML = this.sectionSnippetInput;
let placeholders: HTMLCollectionOf<Element> = temp.getElementsByClassName('placeholderClass')
for (let i = 0; i < placeholders.length; i++) {
//dynamically create angular comp
let factory = this.resolver.resolveComponentFactory(ValueComponent);
//create instance with the value placeholder as parent
const ref = factory.create(this.injector, [], placeholders[i]);
//manually init comp
ref.instance.initElem = placeholders[i];
ref.instance.initComp();
//trigger re-rendering
ref.changeDetectorRef.detectChanges();
//attach newly created component to our angular context
this.app.attachView(ref.hostView);
}
this.sectionSnippetInput = temp.innerHTML;
}
get sectionSnippet() {
return this.sanitizer.bypassSecurityTrustHtml(this.sectionSnippetInput);
}
}
Value Component:
HTML
<span (click)="testCallback()">{{getFormatNumber()}}</span>
TS
#Component({
selector: 'value',
templateUrl: './value.component.html',
styleUrls: ['./value.component.sass']
})
export class ValueComponent implements AfterViewInit {
public param1:string|null = '';
public initElem: Element | null | undefined;
public testCallback(){
alert("Test")
}
ngAfterViewInit(): void{
this.initComp();
}
public initComp() {
// #ts-ignore
this.param1 = this.initElem.getAttribute("data-placeholderdata");
}
getFormatNumber(){
//some formatting code
return this.param1;
}
}
Which seems to work, at least for the initial rendering, but angular bindings like (click) does not work. My guess is that the newly created component is not correctly bound to the angular context, therefore it does not register the click events.
(Using angular V13)
As I am new to angular, can someone give me advice how to proceed. Is using the ComponentFactory a good idea of should I try something different?

Angular: ngOnInit hook does not work in dynamically created component

I'm having the following directive that adds dynamic component to ng-container
#Directive({
selector: '[appAddingDirective]'
})
export class AddingDirective {
constructor(protected vc: ViewContainerRef) { }
public addComponent(factory: ComponentFactory<any>, inputs: any): void {
this.vc.clear();
const ref: ComponentRef<any> = this.vc.createComponent(factory);
Object.assign(ref.instance, inputs); // can't find more elegant way to assign inputs((
ref.instance.ngOnInit(); // IMPORTANT: if I remove this call ngOnInit will not be called
}
}
The directive is used in an obvious way.
#Component({
selector: 'app-wrapper',
template: `<ng-container appAddingDirective></ng-container>`
})
export class WrapperComponent implements AfterViewInit{
#ViewChild(DynamicItemDirective)
private dynamicItem: DynamicItemDirective;
constructor() { }
ngAfterViewInit(): void {
// hope it doesn't matter how we get componentFactory
this.dynamicItem.addComponent(componentFactory, {a: '123'});
}
}
Finally in a component that is loaded dynamically I have
#Component({
selector: 'app-dynamic',
template: '<p>Dynamic load works {{ a }}</p>'
})
export class DynamicComponent implements OnInit {
#Input() a: string;
constructor() {}
ngOnInit(): void {
console.log(this.a);
debugger;
}
}
Here are my questions.
If I remove ref.instance.ngOnInit() call in AddingDirective, I do not get in ngOnInit of DynamicComponent (debugger and console.log do not fire up). Do component lifecycle hooks work in a component that is created and attached dynamically? What is the best way to make these hooks work?
I don't see rendered string Dynamic load works 123 still if I remove {{ a }} in template (template: '<p>Dynamic load works</p>'), Dynamic load works is rendered as it should. What is the reason and how can I fix that?
Is there a better way to assing inputs than doing Object.assign(ref.instance, inputs) as above?
PS. I'm using Angular 11

How to set a value of a property of a nested item in order to make it visible via *ngIf directive

I have created a component to reuse the mat-progress-spinner from angular material. I need this in order to avoid putting for every single page the same code. Here is the code that is working:
<div id="overlayProgressSpinner">
<div class="center">
<mat-progress-spinner
style="margin:0 auto;"
mode="indeterminate"
diameter="100"
*ngIf="loading">
</mat-progress-spinner>
</div>
</div>
It is simple. Only to set "loading" as true or false.
What did I do?
I put above code inside a custom component. Now it is like so:
<app-progress-spinner></app-progress-spinner>
its HTML code is the same and its TS code is as a follows:
import { Component, OnInit } from '#angular/core';
#Component({
selector: 'app-progress-spinner',
templateUrl: './progress-spinner.component.html',
styleUrls: ['./progress-spinner.component.scss']
})
export class ProgressSpinnerComponent implements OnInit {
loading = false;
constructor() { }
ngOnInit() {
}
public isLoading(value: boolean) {
this.loading = value;
}
public changeSpinnerCSSClass() {
const htmlDivElement = (window.document.getElementById('overlayProgressSpinner') as HTMLDivElement);
if (this.loading) {
htmlDivElement.className = 'overlay';
} else {
htmlDivElement.className = '';
}
}
}
when the property "loading" belongs to the current component, I can show and hide the "mat-progress-spinner" component. Otherwise, when it belongs to "app-progress-spinner" it is set but it is not being displayed. The code that I am trying to make it visible is as follows:
this.progressSpinner.isLoading(false); // it is set, but it does not work.
this.progressSpinner.changeSpinnerCSSClass(); // it works
it appears that *ngIf="loading" cannot be set by using the approach the works if the logic behind belongs to the current component.
How to achieve this?
You need to create an input in your ProgressSpinnerComponent. To do that, add the #Input() decorator before the property loading:
import { Component, OnInit, Input } from '#angular/core';
#Component({
selector: 'app-progress-spinner',
templateUrl: './progress-spinner.component.html',
styleUrls: ['./progress-spinner.component.scss']
})
export class ProgressSpinnerComponent implements OnInit {
#Input() loading = false;
So anywhere you need to use the app-progress-spinner you do:
<app-progress-spinner [loading]="loading"></app-progress-spinner>
Note: The loading variable assigned to the input loading belongs to the component that contains theapp-progress-spinner.
This happens because every component have it own scope, meaning that it have no access to external world unless you create an input or output in order to receive or send data. There's also the ngModel that can be used for bi-diretional data, but not recommend in most cases.

How to solve View Encapsulation issue in Angular8?

I have parent component and child component. Child component created as a modal component. So i have included child component selector inside parent component and i have set view encapsulation is none so that it will take parent component css and all and it's working also but parent component has #paper id applying some third party(rappidjs) libary css(for SVG diagramming). Same like child component has #dataMapper id. but here the thirdparty css is not taking because child component set to 'encapsulation: ViewEncapsulation.None'. If i will remove encapsulation: ViewEncapsulation.None it's working but modal is not working. all the modals loading instead of onclick. How to solve this issue please advice me someone.
Coding:
Parent Component TS
#Component({
selector: 'app-data-model',
templateUrl: './data-model.component.html',
styleUrls: ['./data-model.component.css']
})
export class DataModelComponent implements OnInit{
// Modal
openModal(id: string) {
this.modalApp = true;
this.modalService.open(id);
}
closeModal(id: string) {
this.modalService.close(id);
}
Parent Component HTML
<div id="toolbar">
<div class="tool-bar-section">
<button class="btn" (click)="openModal('custom-modal-1');"><i class="fa fa-file-excel-o" style="font-size: 24px"></i></button>
</div>
</div>
<div id="paper"> </div> ( this dom taking thirdparty css)
<app-modal id="custom-modal-1">
<h1 class="head-bar">Data Model - Import Excel <i class="fa fa-times-circle-o" aria-hidden="true"></i></h1>
<div class="modal-content-section">
<ul>
<li>Create New Schema</li>
<li>Import Data to existing schema</li>
</ul>
</div>
</app-modal>
<app-modal id="custom-modal-2">
<h1 class="head-bar">Data Model - Import Excel <i class="fa fa-times-circle-o" aria-hidden="true"></i></h1>
<div class="modal-content-section">
<div id="dataMapper"></div> ( this dom is not taking thirdparty css)
<p>Data Mapping</p>
</div>
</app-modal>
Child Component HTML
<div class="app-modal">
<div class="app-modal-body">
<ng-content></ng-content>
</div>
</div>
<div class="app-modal-background"></div>
Child Component Ts
#Component({
selector: 'app-modal',
templateUrl: './modal.component.html',
styleUrls: ['./modal.component.css'],
encapsulation: ViewEncapsulation.None
})
export class ModalComponent implements OnInit, OnDestroy {
#Input() id: string;
private element: any;
constructor(private modalService: ModalService, private el: ElementRef) {
this.element = el.nativeElement;
}
ngOnInit(): void {
// ensure id attribute exists
if (!this.id) {
console.error('modal must have an id');
return;
}
// move element to bottom of page (just before </body>) so it can be displayed above everything else
document.body.appendChild(this.element);
// close modal on background click
this.element.addEventListener('click', el => {
if (el.target.className === 'app-modal') {
this.close();
}
});
// add self (this modal instance) to the modal service so it's accessible from controllers
this.modalService.add(this);
}
// remove self from modal service when component is destroyed
ngOnDestroy(): void {
this.modalService.remove(this.id);
this.element.remove();
}
// open modal
open(): void {
this.element.style.display = 'block';
document.body.classList.add('app-modal-open');
}
// close modal
close(): void {
this.element.style.display = 'none';
document.body.classList.remove('app-modal-open');
}
}
Modal Service Code
import { Injectable } from '#angular/core';
#Injectable({
providedIn: 'root'
})
export class ModalService {
private modals: any[] = [];
add(modal: any) {
// add modal to array of active modals
this.modals.push(modal);
}
remove(id: string) {
// remove modal from array of active modals
this.modals = this.modals.filter(x => x.id !== id);
}
open(id: string) {
// open modal specified by id
const modal = this.modals.find(x => x.id === id);
modal.open();
}
close(id: string) {
// close modal specified by id
const modal = this.modals.find(x => x.id === id);
modal.close();
}
}
Please let me know if more details required.
Because of your modal.
The modal is created on top of your app, (out of body tag if you look for the modal wrapper by inspecting it.)
It is the Modal cause the dom structure not in sync with ng component structure
So why the modal works with ng Encapsulation? Because your modal service can handle it.
But the 3rd party CSS file is not a part of your angular compile config (i.e. only sass, scss works). So it does ignore any .css even you import it properly.
To solve it, you can apply the .css globally. Or if you are afraid of that may overwriting other global styles, you can rename the css file to '_somefile.scss' (pay attention to the dash '_', it is important.)
Then under your project global style.scss file > create a selector matches your modal wrapper > under the selector: add a line that #import somefile (don't add extension)
style.scss
.modal-wrapper {
#import somepath/somefile
}
As described by you in problem that you want to inherit the css of parent compoenent inside the children component. So, i am assuming you are having same HTML elements in parent and children components and you don't want to duplicate your css.
As we are not able to replicate your issue and you have not created any stackblitz instance. I would suggest a better alternative which will work. Please find it below:
We can specify multiple CSS URL's inside the styleUrls property of #Component decorator metadata object. So, i would suggest you to pass the parent style file Url as well.
I have created a basic stackblitz showing where i am accessing CSS of parent component:https://stackblitz.com/edit/angular-jhewzy
Parent component css file (app.component.css)
- I am specifying background color of p tag inside parent should be yellow. I want to inherit this in child component.
p {
background-color: yellow;
}
Below is child component CSS (hello.component.css)
p {
font-style: italic;
}
As you want to use parent component inside the child component just use the style URL path in styleUrls property
import { Component, Input } from '#angular/core';
#Component({
selector: 'hello',
template: `<p>I am child</p>`,
styleUrls: [`./hello.component.css`, './app.component.css'],
})
export class HelloComponent {
#Input() name: string;
}
I hope it will help you.

Categories

Resources