How do I access the DOM element of another component in Angular? - javascript

My app is structured like this:
<nav></nav>
<router-outlet></router-outlet>
<footer></footer>
In the router-outlet I have different components based on the route. In a few of those pages I need to access the DOM elements of either the nav or the footer for things like pageYOffset, height and etc. for sticky positioning. I know of things like ViewChild, Renderer2 and ElementRef but not really sure how it would work. Do I create a service? What might that look like?

Assuming that the child components are of type NavComponent and FooterComponent, you can retrieve the corresponding DOM elements with #ViewChild(..., { read: ElementRef }) in ngAfterViewInit:
#ViewChild(NavComponent, { read: ElementRef }) private navElementRef: ElementRef;
#ViewChild(FooterComponent, { read: ElementRef }) private footerElementRef: ElementRef;
ngAfterViewInit() {
const navElement = this.navElementRef.nativeElement;
const footerElement = this.footerElementRef.nativeElement;
...
}
See this stackblitz for a demo.
These elements can be made accessible to other parts of the application with a simple service:
export class AppElementsService {
public navElement: HTMLElement;
public footerElement: HTMLElement;
}
and set in the application component:
#ViewChild(NavComponent, { read: ElementRef }) private navElementRef: ElementRef;
#ViewChild(FooterComponent, { read: ElementRef }) private footerElementRef: ElementRef;
constructor(private appElementsService: AppElementService) { }
ngAfterViewInit() {
this.appElementsService.navElement = this.navElementRef.nativeElement;
this.appElementsService.footerElement = this.footerElementRef.nativeElement;
}
Note: you may need to set the display style attribute of the nav and footer components to block or inline-block in order to get the position and size of the corresponding elements.

Related

Angular - Using custom directive to load several components is returning undefined

I am developing an app and for now, I have a dynamic grid generator which divides the space in the screen to fit several components dynamically. So, the component encharged of this must render the components after angular has rendered the page. In order to achieve that I've followed the angular dynamic component loader guide (https://angular.io/guide/dynamic-component-loader).
So I am in a point where I do have the component where the other components must be rendered, I have my custom directive to render the components.
The directive
#Directive({
selector: '[componentLoader]'
})
export class ComponentLoaderDirective {
constructor (
public ViewContainerRef: ViewContainerRef
) {}
}
Now the component ( grid component )
grid.component.ts
// ... Stuff above
export class GridComponent implements OnInit {
#Input() public items: gridItem[] = [];
#ViewChild(ComponentLoaderDirective) componentLoader: ComponentLoaderDirective | undefined;
constructor(
private sanitizer: DomSanitizer,
private componentFactoryResolver: ComponentFactoryResolver
) {}
ngOnInit(): void { this.processRow(this.items) }
processRow( row: gridItem[] ) {
// Some grid related stuff ...
for ( let item of row ) {
// Stuff performed over every item in each grid row
this.renderComponentIfNeeded(item)
}
}
renderComponentIfNeeded( item: gridItem ):void {
if ( item.components ) {
let componentFactory = this.componentFactoryResolver.resolveComponentFactory(component);
let viewContainerRef = this.componentLoader.ViewContainerRef;
viewContainerRef.clear();
let componentRef = viewContainerRef.createComponent<any>(componentFactory);
componentRef.instance.data = item;
console.log('Directive ', this.componentLoader, 'ComponentRef: ', componentRef);
}
}
And the HTML of the component:
<!-- Dynamic grid generation using ng-template and ng-content. This is generated several times using the *ngFor, for every item in the items array we will have a componentLoader -->
<ng-template componentLoader>
</ng-template>
There is a lot more content in these files but for simplicity I will only post this, If you need more code just tell me.
Okay, so my problem is that when I access to this.contentLoader the returned value is just undefined, so this.componentLoader.viewContainerRef causes an error because componentLoader is undefined.
I've tried adding the exportAs property to the directive's decorator and it is giving exacly the same error.
I've also tried to add the directive in the module declarations without success, and changed the <ng-template componentLoader> to <ng-template #loader=componentLoader> which causes a different error ( No directive has 'componentLoader' exportAs or something like this )
PS: In the ´´´this.componentFacotryResolver.resolveComponentFactory(component)``` I successfully have each component that has been given to the grid.
I prefer you not to solve my issue but to point me in the right direction and help me see what am I doing wrong in order to improve myself.
Any help will be much appreciated :)
I've managed to solve this issue in a very simple way.
I was trying to do too many things inside the grid component so I removed to code related to the component loader and moved it into a single component, called ComponentLoaderComponent.
Inside the component I've setted up all the logic in the same way than I did in the grid component. So now I have a new ts file like this:
import { Component, ComponentFactoryResolver, Input, OnInit, ViewChild } from '#angular/core';
import { ComponentLoaderDirective } from 'src/app/shared/directives/componentLoader.directive';
#Component({
selector: 'component-loader',
templateUrl: './component-loader.component.html',
styleUrls: ['./component-loader.component.css']
})
export class ComponentLoaderComponent implements OnInit {
#Input() public component: any;
#ViewChild(ComponentLoaderDirective, { static: true }) componentLoader!: ComponentLoaderDirective;
constructor(
private componentFactoryResolver: ComponentFactoryResolver
) { }
ngOnInit(): void {
this.loadComponent();
}
loadComponent():void {
if (this.component) {
let componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.component);
let viewContainerRef = this.componentLoader.viewContainerRef;
viewContainerRef.clear();
let componentRef = viewContainerRef.createComponent<any>(componentFactory);
}
}
And an HTML like this:
<ng-template componentLoader>
</ng-template>
Now from the grid component I only have to call the ComponentLoader for every component I want to add to the grid, so the grid html will look like this:
<div
*ngIf=" gridItem.components && gridItem.components.length > 0"
class="component-container"
>
<component-loader
*ngFor="let component of gridItem.components"
[component]="component">
</component-loader>
</div >
Now the components are getting loaded correclty, anyways I still don't know what I was missing in before.

Angular renders component first then removes another in ng-If

I've two component displayed one at a time using a ngif directive.
<app-root>
<first-Comp *ngIf="showFirst"></first-Comp>
<second-Comp *ngIf="!showFirst"></second-Comp>
</app-root>
The Points are
The showFirst variable is initialized using true.
first-comp contains a element having height 100px;
second-comp have dynamic element
Inside the second component, i'm calculating the height using document.body.scrollHeight inside ngOnInit
The problem is when the showFrist becomes false angular first renders the second-comp then removes the first-comp. As result I am getting the height 100+ instead of 0. But I need the height of the body with only second-comp on component render.
Another important things I've missed to mention as I've thought that might not hamper. That is the first and second both components are detached from angular automatic change detection for performance. I've a base component like this
export class BaseComponent {
private subscriptions: Subscription[] = [];
constructor(private childViewRef: ChangeDetectorRef) {
this.childViewRef.detach();
}
public updateUI(): void {
try {
this.childViewRef.reattach();
this.childViewRef.detectChanges();
this.childViewRef.detach();
} catch (ex) {
// ignored
}
}
protected addSubscriptions(subs: Subscription) {
this.subscriptions.push(subs);
}
protected unSubscribeSubscriptions() {
this.subscriptions.forEach(item => item.unsubscribe());
this.subscriptions = [];
}
}
All the components inherit this BaseComponent except AppComponent
So the code of SecondComp looks something like this.
#Component({
selector: 'second-comp',
templateUrl: './SecondComponent.component.html',
styleUrls: ['./SecondComponent.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class SecondComponent extends BaseComponent implements OnInit, AfterViewInit{
constructor(private ref:ChangeDetectorRef){
super(ref);
}
ngAfterViewInit(): void {
this.updateUi();
this.publishHeight()
}
ngOnInit() {
this.updateUi();
this.publishHeight()
}
}
Is there anything wrong for which I'm getting this unexpected behavior.
It feels like you are doing it the wrong way. You can inject #Self in second-comp constructor, it will give you the ElementRef of itself(second-comp).
constructor( #Self() private element: ElementRef ) {}
It might not work but it wont be affected by first-comp
ngOnInit() {
this.element.nativeElement.offsetHeight //the height for whatever you need it for
}
Calculate the height in setTimeout() in second-comp inside ngOnInit
setTimeout(() => {
//calculateHeight()
}, 200);
You should calculate the height when the component's view is fully rendered. That means calculating the height inside ngAfterViewInit() hook. See https://angular.io/api/core/AfterViewInit

Wrapping an Angular 2+ directive with another directive

I'm trying to reduce clutter in my (Angular 7) app by creating a directive which takes a simplified set of parameters (such as a user ID) and displays a ng-bootstrap popover.
I'd like the directive to work as similarly as possible to a normal ng-bootstrap popover, except that it's created using the custom directive instead. I know I could do something similar with a component, but I'm planning on using this directive on enough different elements that it wouldn't be feasible.
Is it possible to wrap directives like this in Angular 2+, and if so what would the best approach be to making this happen?
I've created a StackBlitz with what I've created so far here:
import { Directive, ElementRef, OnInit, Input } from '#angular/core';
#Directive({
selector: '[app-custom-directive]'
})
export class CustomDirective implements OnInit {
private element: HTMLInputElement;
#Input() parameter: string = 'Parameter';
constructor(private elRef: ElementRef) {
this.element = elRef.nativeElement;
}
ngOnInit() {
this.element.onclick = () => {
alert('This should open a popover containing the directive parameter "' + this.parameter + '". But how?')
};
}
}
1) First of all, you should never do the
this.element.onclick = () => {
Use #HostListener instead. It's angular-way to listen for events in angular on the Directives.
2) You really need a component here which will have a directive and the input you need.
3) I don't know if it will work but you can at least try to extend a NgbPopover directive:
export class CustomDirective extends NgbPopover {
// private element: HTMLInputElement;
constructor(
private _elementRef: ElementRef<HTMLElement>,
private _renderer: Renderer2,
injector: Injector,
componentFactoryResolver: ComponentFactoryResolver,
viewContainerRef: ViewContainerRef,
config: NgbPopoverConfig,
private _ngZone: NgZone,
#Inject(DOCUMENT) private _document: any
) {
super(_elementRef, _renderer, injector, componentFactoryResolver, viewContainerRef, config, _ngZone, _document);
}

Checking an event only in one page of the app in angular2

i'm developing my first app in angular 4.0.
My problem is pretty simple but i would like to know if there is a best practice to solve my issue.
I have an header with position: 'fixed' and when the user scrolls the page this element changes some of its properties (height, background-size..) by adding a 'small' class dynamically.
This is my component
import {Component, OnInit, HostListener} from '#angular/core';
#Component({
selector: 'my-header',
templateUrl: './my-header.component.html',
styleUrls: ['./my-header.component.css']
})
export class HeaderComponent implements OnInit {
scrollState: boolean;
constructor() { }
ngOnInit() {
this.scrollState = false;
}
#HostListener('window:scroll', [])
toggleScrollState() {
if(window.pageYOffset == 0){
this.scrollState = false;
}
else{
this.scrollState = true;
}
}
}
and this the html
<header class="bk-blue clearfix" [ngClass]="{small: scrollState}">
<a class="sx" href="#">Login</a>
<a class="dx arrow-down"></a>
<a class="dx" href="#">It</a>
</header>
Everything works fine but this should happen only in the home page. In the other page the header element should already be in the 'small' state without any DOM manipulations based on the scroll event.
I was thinking of checking the current route to set an additional variable (false if the current route matches the home page path, true otherwise) and put that in OR with scrollState. Something like this:
<header class="bk-blue clearfix" [ngClass]="{small: notHome || scrollState}">
By doing so, however, i can't avoid calling the listener with its implications in term of reduced performance.
What is for you the best approach to avoid calling the listener even in internal pages where it is not necessary?
well, I would do this using ActivatedRouteSnapshot
#Component({...})
class Header implements OnInit {
readonly snapshot: ActivatedRouteSnapshot = null;
constructor(private route: ActivatedRoute) {
this.snapshot = route.snapshot;
}
ngOnInit() {
// if this.snapshot is HOMEPAGE
// then
// subscribe to the scroll and switch class when you need.
}
}
You could also set a property on your route.data which tells you to animate or not the header.
const routes: Routes = [
{
path: '',
component: HomeRouteComponent,
data: { ANIMATE_HEADER: true }
}
];
// header.ts
ngOnInit() {
if(this.snapshot.data.ANIMATE_HEADER) {
// do stuff here
}
}

Create a "clone" of an Angular2 component with ng-content

I'm creating a 3d "card flip" using angular 2. A parent 'card-flip' component contains a nested 'card-flip-front' and 'card-flip-back' component.
<card-flip card-flip-id="demo-1" class="grid_col-6">
<card-flip-front class="card">
<div class="card__inner">
Card Front
</div>
</card-flip-front>
<card-flip-back class="card">
<div class="card__inner">
Card Back
</div>
</card-flip-back>
</card-flip>
I would like to create a "clone" of the card-flip-front component with content projection and data-binding in tact. The "clone" would be used for animating and the "original" would remain in it's original position hidden. That way I have a reference of where the "clone" should animate to when it returns to the original position (even if the user scrolls or resizes the window).
The main challenge I'm facing is that I need the content within the ng-content tag to also be projected in the "clone". The problem being that the first ng-content tag will be used by Angular for content projection and additional, unlabeled ng-content tags will be empty (which I know is the expected behavior).
One might ask, "why not just create a dumb, static copy of the element in the DOM?". I would like to avoid this so that nested components and data bindings that inject data (thereby modifying the dimensions of the element) will continue to work.
Here's my work so far which creates an instance of the CardFlipFront component via ComponentFactory to serve as the "clone" and simply inserts the innerHTML of the "original" CardFlipFront.
import {
Component,
ComponentFactory,
ComponentFactoryResolver,
ComponentRef,
ContentChild,
Inject,
Input,
OnInit,
ViewChild,
ViewContainerRef
} from '#angular/core';
import { CardFlipFrontComponent } from './card-flip-front.component';
import { CardFlipBackComponent } from './card-flip-back.component';
import { CardFlipService } from './card-flip.service';
#Component({
selector: 'card-flip',
templateUrl: './card-flip.component.html',
styleUrls: ['./card-flip.component.css'],
entryComponents: [
CardFlipFrontComponent
]
})
export class CardFlipComponent implements OnInit {
#Input('card-flip-id') public id: string;
#ContentChild(CardFlipFrontComponent) private front: CardFlipFrontComponent;
#ContentChild(CardFlipBackComponent) private back: CardFlipBackComponent;
#ViewChild('frontCloneContainer', { read: ViewContainerRef }) private frontCloneContainer: ViewContainerRef;
private frontComponentRef: ComponentFactory<CardFlipFrontComponent>;
private frontClone: ComponentRef<CardFlipFrontComponent>;
constructor(
#Inject(CardFlipService) private _cardFlipService: CardFlipService,
private _componentFactoryResolver: ComponentFactoryResolver
) {
this.frontComponentRef = this._componentFactoryResolver.resolveComponentFactory(CardFlipFrontComponent);
}
ngOnInit() {
this._cardFlipService.register(this.id);
}
ngAfterViewInit() {
// Create a card-flip-front component instance to serve as a "clone"
this.frontClone = this.frontCloneContainer.createComponent(this.frontComponentRef);
// Copy the innerHTML of the "original" into the "clone"
this.frontClone.instance.el.nativeElement.innerHTML = this.front.el.nativeElement.innerHTML;
}
ngOnDestroy() {
this.frontClone.destroy();
}
}
<ng-content select="card-flip-front"></ng-content>
<ng-container #frontCloneContainer></ng-container>
<ng-content select="card-flip-back"></ng-content>
import {
Component,
ElementRef,
HostBinding,
Input,
OnInit,
Renderer
} from '#angular/core';
#Component({
selector: 'card-flip-front',
templateUrl: './card-flip-front.component.html',
styleUrls: ['./card-flip-front.component.css']
})
export class CardFlipFrontComponent implements OnInit {
constructor(private _el: ElementRef, private _renderer: Renderer) { }
public get el(): ElementRef {
return this._el;
}
public get renderer(): Renderer {
return this._renderer;
}
ngOnInit() { }
}
<ng-content></ng-content>
UPDATE:
Ok, so after reading about some similar challenges and the github issue here, I tried the following.
<ng-template #frontTemplate>
<ng-content select="card-flip-front"></ng-content>
</ng-template>
<ng-container *ngIf="isOpen == true" #front1>
<ng-container *ngTemplateOutlet="frontTemplate"></ng-container>
</ng-container>
<ng-container *ngIf="isOpen == false" #front2>
<ng-container *ngTemplateOutlet="frontTemplate"></ng-container>
</ng-container>
<ng-content select="card-flip-back"></ng-content>
Basically, we can get around the single projection issue with ng-content by placing it within a template and using two ng-container tags with an *ngIf statement that will only show one instance of the template based on a class property isOpen.
This doesn't solve the entire issue though because only one container will be rendered at any given time. So, I can't get the current position of "original" to figure out where to animate the "clone" during the return animation described above.
I think you can have an intermediary component <card-flip-content> inside <card-flip> template which is duplicated and which receives the <ng-content> of the <card-flip>.
Something like:
#Component(
selector = 'card-flip',
template = `
<card-flip-content #theOne>
<ng-content />
</card-flip-content>
<card-flip-content #theClone>
<ng-content />
</card-flip-content>
`)
Then bind data as needed to #theOne and #theClone and animate only #theClone.
This way can have #Input and #Output thus leaving the actions of one component to be interpreted by the parent in order to act on the other component.
Would that work?

Categories