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
Related
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.
i am trying to understand the callback ngOnChanges() so i created the below posted example. but at the compile time despite the interface Post
has values for its attributes title and content respectively, however, i do not receive any logs from ngOnChanges
please let me know how to use correctly
app.component.ts:
import { Component, OnInit, OnChanges, SimpleChanges,Output, EventEmitter } from '#angular/core';
export interface Post {
title:string;
content:string;
}
#Component({
selector: 'app-post-create',
templateUrl: './post-create.component.html',
styleUrls: ['./post-create.component.css']
})
export class PostCreateComponent implements OnInit {
#Output() post : Post;
#Output() onPostSubmittedEvtEmitter: EventEmitter<Post> = new EventEmitter<Post>();
constructor() {
this.post = {} as Post;
}
ngOnInit(): void {
}
ngOnChanges(changes: SimpleChanges) {
for (let changedProperty in changes) {
console.log("ngOnChanges->: changes[changedProperty].previousValue: " + changes[changedProperty].previousValue);
console.log("ngOnChanges->: changes[changedProperty].currentValue):" + changes[changedProperty].currentValue);
}
}
onSubmitPost(post: Post) {
this.post = {
title: this.post.title,
content: this.post.content
};
this.onPostSubmittedEvtEmitter.emit(this.post);
console.log("onSubmitPost->: post.title: " + post.title);
console.log("onSubmitPost->: post.content:" + post.content);
}
}
update 05.04.2021
as recommended i have added the ngOnChanges to observe changes in a prpoperty annotated with Input decorator as follows:
#Input() postsToAddToList: Post[] = [];
now, when I compile the code i add some values, i receive the following logs from ngOnChanges :
ngOnChanges->: changes[changedProperty].previousValue: undefined
post-list.component.ts:20 ngOnChanges->: changes[changedProperty].currentValue):
but the problem is when i keep adding more values, i do not receive any logs from the ngOnChanges
please let me know why despite i keep adding more values that result in changing the contents of the object that is decorated with #Input??!
post-list.component.ts:
import { Component, Input,OnInit, OnChanges, SimpleChanges,Output, EventEmitter } from '#angular/core';
import { Post } from '../post-create/post-create.component';
#Component({
selector: 'app-post-list',
templateUrl: './post-list.component.html',
styleUrls: ['./post-list.component.css']
})
export class PostListComponent implements OnInit {
constructor() {}
#Input() postsToAddToList: Post[] = [];
ngOnInit(): void {}
ngOnChanges(changes: SimpleChanges) {
for (let changedProperty in changes) {
console.log("ngOnChanges->: changes[changedProperty].previousValue: " + changes[changedProperty].previousValue);
console.log("ngOnChanges->: changes[changedProperty].currentValue):" + changes[changedProperty].currentValue);
}
}
}
ngOnChanges() only gets called when component's inputs changed from the parent component(fields that marked with #Input decorator). But you have #Output fields. The idea of ngOnChanges() is to react to changes that were done by the parent.
Following your business logic, you can handle whatever you want straight in onSubmitPost.
Answer for the update 05.04.2021
You add values to the array itself. Since the link to the array hasn't changed, ngOnChanges() does not catch these changes. But if you put new link to the component and do the following in the parent:
component:
this.yourArrInTheParent = [...this.yourArrInTheParent];
template:
<app-post-lis [postsToAddToList]="yourArrInTheParent"></app-post-lis>
Now value that you passed to the input changed and you will see the changes in the ngOnChanges(). The same goes for objects if you change object's property, angular won't see it as a change in ngOnChanges() since it only detects changes in #Input() values.
In order to catch those changes, you can use ngDoCheck hook. But it is power consuming, bear in mind not to perform heavy calculations there.
I think you are doing in correct way. Its just you missing to implement onChanges class. In latest Angular versions it straight throws error but in older version it does not.
Try this.
export class PostListComponent implements OnInit, OnChanges{
constructor() {}
#Input() postsToAddToList: Post[] = [];
ngOnInit(): void {}
ngOnChanges(changes: SimpleChanges) {
for (let changedProperty in changes) {
console.log("ngOnChanges->: changes[changedProperty].previousValue: " +
changes[changedProperty].previousValue);
console.log("ngOnChanges->: changes[changedProperty].currentValue):" +
changes[changedProperty].currentValue);
}
}
}
As already pointed out by #Vadzim Lisakovich
ngOnChanges() only gets called when component's inputs changed from
the parent component
Now, the thing is that the input is compared using === operator i.e. shallow comparison. If you add something to the post array, the reference to the array stays the same thus no event is triggered.
To fix that you can implement ngDoCheck() or replace the reference.
Here is a very similar question to yours:
Angular2 change detection: ngOnChanges not firing for nested object
And of cause the documentation:
https://angular.io/guide/lifecycle-hooks#docheck
I have an Angular 10 application, I am trying to remove the navside component from the login component, so I created a service on the nave side component contains this code :
visible: boolean;
constructor() {
this.visible = true;
}
show() {
this.visible = true;
}
hide() {
this.visible = false;
}
toggle() {
this.visible = !this.visible;
}
doSomethingElseUseful() { }
and inside the naveside component i put :
export class NavsideComponent implements OnInit {
constructor(public sr: ServService ) { }
ngOnInit(): void {
}
and the Html component :
<div *ngIf="sr.visible">
<mat-sidenav-container class="example-container">
<mat-sidenav #sidenav mode="push" class="app-sidenav" opened>
<mat-toolbar class="co">
<span class="toolbar-filler"></span>
.
.
.
.
.
. </div>
but this error was displayed :
ERROR Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'ngIf: true'. Current value: 'ngIf: false'.
EDIT 1
Login.component.ts
import { Component, OnInit } from '#angular/core';
import { ServService } from '../../navside/serv.service';
#Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.css']
})
export class LoginComponent implements OnInit {
constructor( private sr :ServService) { }
ngOnInit(): void {
this.sr.hide();
this.sr.doSomethingElseUseful();
}
}
EDIT 2
Stackblitz putting together the snippets above, where the warning can be seen:
https://stackblitz.com/edit/angular-ivy-vbkytu?file=src/app/login/login.component.ts
I have edited your post to include a Stackblitz with a minimal reproducible example of the reported error. I suggest that in your next posts you include a minimal reproducible example, hence it´s more likelly you will have an answer.
About ExpressionChangedAfterItHasBeenCheckedError warning
This post has usefull informations about this warning: Expression ___ has changed after it was checked
In your specific case, the warning ExpressionChangedAfterItHasBeenCheckedError is shown because the value of sr.visible is changed twice during the initialization process:
once during the service SerService creation and
again in loginComponent.ngOnInit() when you call sr.hide();
In the same round of change detection a value that is binded in the view (sr.visible is binded in <div *ngIf="sr.visible">) is not supposed to change values more than once, that's the reason of the warning.
Solving your problem calling cdr.changeDetection() in the right component
You can solve the problem calling cdr.detectChanges() in order to fire a second round of change detections. But for that to work, you need to call it in the component that has the affected binding.
Calling cdr.detectChanges() on LoginComponent has no effect, since the binding of <div *ngIf="sr.visible"> is not in that component. You should call cdr.detectChanges() in the parent component where the binding is.
That said, the following use of cdr.detectChanges() in LoginComponent will NOT solve the problem:
export class LoginComponent {
constructor(private sr: ServService, private cdr: ChangeDetectorRef) {}
ngOnInit() {
this.sr.hide();
this.sr.doSomethingElseUseful();
//code bellow will NOT SOLVE the problem:
this.cdr.detectChanges();
}
}
What will solve the problem is invoking cdr.detectChanges() on the component that has the binding <div *ngIf="sr.visible">.
In my working stackblitz (at the end of this answer), that component is AppComponent:
app.component.html:
<div *ngIf="sr.visible">
Toolbar here
<!-- your mat-side-nav and mat-toolbar here -->
</div>
<app-login></app-login>
So, the cdr.detectChanges() is called in the ngOnInit() of app.component.ts:
export class AppComponent {
constructor(public sr: ServService,
private cdr: ChangeDetectorRef) {}
ngOnInit() {
this.cdr.detectChanges();
}
}
Working version - stackblitz
The working version calling detectChanges in the right component is available in the following Stackblitz:
https://stackblitz.com/edit/angular-ivy-nbb7bz?file=src/app/app.component.ts
This shall solve it:
import { ChangeDetectorRef } from '#angular/core';
constructor(private changeDetector: ChangeDetectorRef) {}
ngAfterViewChecked() {
this.changeDetector.detectChanges();
}
This question related to Syntactically anonymous/Arrow Function/add-hoc/factory DP functions:
I have a component which is embedded in the Html.
The component has a click event which is binded to a function. This function content depend on another component which has a reference to this component.
This is the component with the click event:
HTML:
<div (click)="doSomething()">Content.....</div> \\ Should it be with a brackets ?
In the component I just want to define the function signature:
#Component({
selector: 'app-embedded'
})
export class className
{
constructor() { }
ngOnInit() { }
doSomething:(booleanparams: boolean) => any; //The function get a boolean parameter as input and return void or any
}
Now this is where the component is embedded:
<div >
<app-embedded #emb></app-embedded>
</div>
This is the component of the container of the embedded component, which has a reference to the embedded component:
#Component({
selector: 'app-container',
})
export class container
{
#ViewChild('emb') private emb: ElementRef;
booleanParam : booelan;
constructor()
{
emb.doSomething = containerFunction(true);
}
containerFunction(booleanParam : boolean)
{
// do something in this context
}
}
The idea is that this embedded component is embedded in many other containers and whenever the click event triggered a function that was set in the doSomething function variable should be executed.
What changes in the code I need to do in order to accomplish this ?
The best way i see of doing this would be to simply use an event emitter and capture the event on the other side? so embedded would have this:
#Component({
selector: 'app-embedded'
})
export class className
{
#Output()
public something: EventEmitter<boolean> = new EventEmitter<boolean>();
constructor() { }
ngOnInit() { }
doSomething:(booleanparams: boolean) {
this.something.emit(booleanparams);
}; //The function get a boolean parameter as input and return void or any
}
Then where it is called:
<div >
<app-embedded #emb (something)="doSomething($event)"></app-embedded>
</div>
Other solution that would allow a return
#Component({
selector: 'app-embedded'
})
export class className
{
#Input()
public somethingFunc: (boolean)=>any;
constructor() { }
ngOnInit() { }
doSomething:(booleanparams: boolean) {
let w_potato = this.somethingFunc(booleanparams);
//Do whatever you want with w_potato
}; //The function get a boolean parameter as input and return void or any
}
in this case the view would be
<div >
<app-embedded #emb [somethingFunc]="doSomething"></app-embedded>
</div>
I hope this helps! Passing the function or emitting an event will be much more angular than trying to modify an instance of a component. On top of that, a constructor is only called once when Angular starts up so #emb at that time will not be defined to be anything. If you wanted to do it that way you would have to bind yourself in something ngAfterViewInit.
But again, I think that passing it through attributes will be much more angular looking.
Good Luck let me know if this doesn't suit your answer.
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.