//This is my html file ,Here i declared "#bodyText" and #truncated to access the element in the component but showing undefined in onInit() hook, however it is working fine in ngAfterView() component.
<div class="note-card-content">
<h1 class="note-card-title">{{title}}</h1>
<div #bodyText class="note-card-body">
<p>{{body}}</p>
<div #truncator class="fade-out-truncation"></div>
</div>
</div>
<div class="x-button"></div>
// This is the component
import { Component, ElementRef, Input, OnInit, Renderer2, ViewChild } from '#angular/core';
#Component({
selector: 'app-note-card',
templateUrl: './note-card.component.html',
styleUrls: ['./note-card.component.scss']
})
export class NoteCardComponent implements OnInit {
//These two elements are showing undefined when I print on console
#ViewChild('truncator') truncator:ElementRef<HTMLElement>;
#ViewChild('bodyText') bodyText:ElementRef<HTMLElement>;
#Input() title:string;
#Input() body:string
constructor(private renderer:Renderer2) { }
ngOnInit(): void {
console.log(this.truncator)
// Work out if there is a text overflow and if so,then hide
let style = window.getComputedStyle(this.bodyText.nativeElement,null);
let viewableHeight = parseInt(style.getPropertyValue("height"),10)
if(this.bodyText.nativeElement.scrollHeight > viewableHeight){
// if there is no text overflow ,show the fade out truncator
this.renderer.setStyle(this.truncator.nativeElement,'display','block')
}else{
// else (there is a text overflow)
this.renderer.setStyle(this.truncator.nativeElement,'display','none')
}
}
}
And what's the problem? :)
This is a normal behaviour. ngOnInit(){} runs before Angular initializes the component's views and child views. You shouldn't use ngOnInit for this purpose. I suggest you to take a look at Lifecycle hooks, Angular.
Related
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.
I come from a Python background, but I started trying to learn Angular and I'm really having trouble. Working between components is confusing to me and I can't figure it out. I made a good example that I think if someone helped me with it would go along way towards understanding Angular.
I just have two components: a "header" component and an app component. In the header component, I ask for the user's name and they click a button, and then it should show "Hello {{name}}" in the next component. I cannot get it to work to say the least and it's really frustrating. The Header part seems to work okay, but it's just not communicating with the other component at all. Neither the button part or the "name" part are working so I am clearly misunderstanding something I need to do when it comes to listening from the parent component.
Here is my Header HTML:
Name: <input type="text" id="userInput" value="Joe">
<button (click)=showName()>Show More</button>
Here is my Header TS:
import { Component, OnInit, Output, EventEmitter } from '#angular/core';
#Component({
selector: 'app-header',
templateUrl: './header.component.html',
styleUrls: ['./header.component.css']
})
export class HeaderComponent implements OnInit {
bodyDiv = false;
inputName = '';
#Output() buttonClicked = new EventEmitter();
constructor() { }
ngOnInit() {
}
showName() {
console.log('showName clicked.');
this.bodyDiv = true;
this.inputName = document.getElementById('userInput').value;
console.log(this.inputName);
console.log(this.bodyDiv);
this.buttonClicked.emit(this.bodyDiv);
this.buttonClicked.emit(this.inputName);
}
}
Here is the main Component's HTML:
<app-header (buttonClicked)='showNextComponent($event)'></app-header>
<p *ngIf="![hiddenDiv]" [inputName]="name">Hello {{ name }} </p>
Here is the main component's TS:
import { Component } from '#angular/core';
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
hiddenComponent = true;
title = 'show-button';
showNextComponent() {
console.log('Button clicked.');
this.hiddenComponent = false;
console.log(this.hiddenComponent);
}
}
So who can show me what I'm doing wrong and help figure out Angular a little better? :) Thank you!
replace showName function with below code :
showName() {
console.log('showName clicked.');
this.bodyDiv = true;
this.inputName = document.getElementById('userInput').value;
console.log(this.inputName);
console.log(this.bodyDiv);
this.buttonClicked.emit(this.inputName);
}
replace below code in your main component.
name:string
showNextComponent(value:string) {
this.name = value;
}
replace below code in your html :
<app-header (buttonClicked)='showNextComponent($event)'></app-header>
<p *ngIf="name">Hello {{ name }} </p>
Please let me if you have any question and I would suggest try to use ngmodel or something else instead of directly communicating with the DOM.
Here is a slightly modified and working sample: https://stackblitz.com/edit/angular-jhhctr
The event emitter in the header component emits the name (string) which is the $event in showNextComponent($event). You have to capture this in the main component and assign it to a local variable to be able to use it in the main component's template as {{name}}
[inputName]="name" is incorrect. You can pass values like that to angular components not to actual HTML DOM elements.
There are couple of ways to communicate from one component to another in angular - Using #Input()in your child component will expects an input from parent component and #Output() from your child component will emit an event from the child component
So in your case if you want to pass a value from parent to child you need to use input property or decorator on your child property - I will provide you the code but just go through proper guidance from the link provided this will make you to create better angular applications https://angular.io/guide/component-interaction
First you need to swap your components your header component should be your parent and the child component will be your main component - if you want to work in the same way just move your codes vice versa
Header html
Name: <input type="text" id="userInput" name='userInput' [(ngModel)]='inputName' value="Joe">
<button (click)=showName()>Show More</button>
<div [hidden]='bodyDiv'>
<app-header [bindName]='inputName'></app-header>
</div>
Header Component
import { Component, OnInit, Output, EventEmitter } from '#angular/core';
#Component({
selector: 'app-header',
templateUrl: './header.component.html',
styleUrls: ['./header.component.css']
})
export class HeaderComponent implements OnInit {
bodyDiv = true;
inputName = '';
constructor() { }
ngOnInit() {
}
showName() {
bodyDiv = false;
}
}
Main Component Html
<p>Hello {{ bindName }} </p>
Main component ts
import { Component, Input } from '#angular/core';
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
#Input()
bindName: string;
}
In your header component the inputName property will be binded using two way data binding where i used [(ngModel)]='inputName' so whatever you enter in the input text it will be updated in your inputName property
Now we need to do only one thing just to show your child component with any event - so when the button is clicked the div with [hidden] property will be false and it will be displayed and as we pass the inputName to the child Component it will be updated
And finally the child component will be displayed and the input written in the text will be updated in the child component - when the child component html displays the bindName will be updated and there will be result you expected
That's all I think this should work well - Try this and let me know - Thanks Happy coding !!
Don't forget to look into the link above where you can see many types of component interactions
I have a set of components that work together. Basically I am displaying a list of labels, and a list of data for each label. Since the label pane is resizable, it becomes a sibling of the data component. I.e.- my list component looks like:
<app-list-labels></app-list-labels>
<app-list-data></app-list-data>
And list-labels and list-data look like this, respectively:
// app-list-labels:
<div class="label-header">Labels</div>
<div class="label-labels" id="labels-labels">
<!-- all of my labels looped over and displayed -->
</div>
// app-list-data:
<div class="data-header">Labels</div>
<div class="data-data" id="data-data">
<!-- all of my data rows looped over and displayed -->
</div>
Both labels-labels and data-data have overflow-y set to auto, so that they can scroll if the number of rows exceeds the container size. The number and size of rows between the two are always identical. My goal is to have both containers scroll if one container is being scrolled. So I'd need to attach a (scroll) event listener to both of those divs (#data-data and #labels-labels), and update the scroll value of the non-scrolled element. My problem is- how can I access an element from one component in a sibling component? If app-labels was embedded in app-data it would be straight forward, but being siblings, I cant see how to do it.
You could try exposing the Div's using #Output decoratos, like this:
<app-component-one (divComponent)="divOne = $event"></app-component-one>
<app-component-two (divComponent)="divTwo = $event"></app-component-two>
Sibling 1:
import { AfterViewInit, Component, ElementRef, EventEmitter, OnInit, Output, ViewChild } from '#angular/core';
#Component({
selector: 'app-component-one',
templateUrl: './component-one.component.html',
styleUrls: ['./component-one.component.css']
})
export class ComponentOneComponent implements OnInit, AfterViewInit {
#ViewChild('divComponent1') divComponent1: ElementRef;
#Output() divComponent = new EventEmitter();
constructor() {
}
ngOnInit() {
}
ngAfterViewInit(): void {
this.divComponent.emit(this.divComponent1);
}
}
Sibling 2:
import { AfterViewInit, Component, ElementRef, EventEmitter, OnInit, Output, ViewChild } from '#angular/core';
#Component({
selector: 'app-component-two',
templateUrl: './component-two.component.html',
styleUrls: ['./component-two.component.css']
})
export class ComponentTwoComponent implements OnInit, AfterViewInit {
#ViewChild('divComponent2') divComponent1: ElementRef;
#Output() divComponent = new EventEmitter();
constructor() {
}
ngOnInit() {
}
ngAfterViewInit(): void {
this.divComponent.emit(this.divComponent1);
}
}
Parent Component, the one that has the siblings in :
import { AfterViewInit, Component, ElementRef, OnInit } from '#angular/core';
#Component({
selector: 'app-component-base',
templateUrl: './component-base.component.html',
styleUrls: ['./component-base.component.css']
})
export class ComponentBaseComponent implements OnInit, AfterViewInit {
divOne: ElementRef;
divTwo: ElementRef;
constructor() {
}
ngOnInit() {
}
ngAfterViewInit(): void {
console.log('div one' , this.divOne);
console.log('div two' , this.divTwo);
}
}
I have a my-alert-component which looks like this:
import { Component, OnInit,Input } from '#angular/core';
#Component({
selector: 'app-my-alert',
template: `
<h1 (click)="alert()">{{type}}</h1>
<ng-content></ng-content>
`
})
export class MyAlertComponent implements OnInit {
#Input() type: string = "Success";
alert(){
console.log("alert");
}
constructor() { }
ngOnInit() {
}
}
And in my app component, I am doing the following:
import { Component,ComponentRef,ComponentFactory,ViewContainerRef, ComponentFactoryResolver,ChangeDetectorRef, ViewChild, TemplateRef, ViewChildren, QueryList, AfterViewInit,ElementRef, ContentChild, AfterContentInit } from '#angular/core';
import { MyAlertComponent } from './my-alert.component';
#Component({
selector: 'app-root',
template: `
<app-my-alert>
<p #insideNgContentVar>A paragraph inside ng-content</p>
</app-my-alert>
<app-my-alert type="danger"></app-my-alert>
<app-my-alert type="success"></app-my-alert>
`
})
export class AppComponent implements AfterViewInit{
#ViewChildren(MyAlertComponent) alertComponents : QueryList<AlertComponent>;
#ContentChild('insideNgContentVar') insideNgContent:ElementRef;
ngAfterContentInit(){
console.log(this.insideNgContent.nativeElement.textContent);
}
ngAfterViewInit(){
this.alertComponents.forEach((alertComponentInstance) => console.log(alertComponentInstance));
}
}
This is pretty simple, I thought.
But the error I am getting is:
EXCEPTION: Error in :0:0 caused by: Cannot read property 'nativeElement' of undefined
What am I doing wrong?
EDIT
If in my-alert-component I do:
#Component({
selector: 'app-my-alert',
template: `
<h1 (click)="alert()">{{type}}</h1>
<ng-content></ng-content>
`
})
export class MyAlertComponent implements AfterContentInit,AfterViewInit {
#Input() type: string = "Success";
#ContentChild('insideNgContent') insideNgContentRef:ElementRef;
alert(){
console.log("alert");
}
ngAfterContentInit(){
console.log(this.insideNgContentRef.nativeElement.textContent);
}
And in my app component:
template: `
<app-my-alert>
</app-my-alert>
<app-my-alert type="danger">
<p #insideNgContent>A paragraph inside ng-content</p>
</app-my-alert>
<app-my-alert type="success"></app-my-alert>
`
After these changes also, something seems to be missing.
What is that?
I think you should be using #ViewChild here, as per this answer:
https://stackoverflow.com/a/34327754/5018962
So basically, ViewChild is going to look for components in the DOM that are defined in your component's template, but ContentChild will look for components defined as ng-content of your app-root component itself, but not ng-content of some child components. But in this case no one is using your app-root component and no one is going to inject any DOM there, so that's why you get undefined
EDIT.
It would work, if you added the following line to the app-my-alert component:
#ContentChild('insideNgContentVar') insideNgContent:ElementRef;
because that means, look at what is defined in the ng-content of app-my-alert. Hope that sheds some light on this topic
i'm struggling about this problem and can't figure out.
I simply need to show a popup div situated in the page clicking from a menu entry in my navbar.component.
I added a property "show" in my popup which prints the "show" class on my div using the ngClass (with if) directive. I can get this working if the action button is inside my popup component but i cannot print the show class clicking on another component. The property in the Object get updated but the class is not printed. I'm using angular 4 with ng-bootstrap. I tried both with services and with parent/child emit event.
This is is my situation:
app.component.html
<app-nav-bar></app-nav-bar>
<app-login></app-login>
<router-outlet></router-outlet>
<app-footer></app-footer>
navbar.component.html
...
<button class="dropdown-item" (click)="showPopup()">LOGIN</button>
...
navbar.component.ts
import {Component, EventEmitter, Input, OnInit, Output} from '#angular/core';
#Component({
moduleId: module.id,
selector: 'app-nav-bar',
templateUrl: 'navbar.component.html',
styleUrls: ['./navbar.component.css'],
})
export class NavbarComponent implements OnInit {
#Output() show = new EventEmitter<boolean>();
ngOnInit() {
}
showPopup() {
this.show.emit(true);
}
}
login.component.html
<div id="wrapper-login-popup" class="fade-from-top" [(class.show)]="show">
<div id="container-login-popup">
<div class="row">
<div class="col-sm-12 text-center">
<img id="popup-bomb" src="assets/images/bomb.png" alt="bomb"/>
<img id="popup-close" class="close-icon" src="assets/images/close.png" alt="close"
(click)="closePopup()"/>
</div>
</div>
</div>
</div>
login.component.ts
import {Component, Input, OnInit} from '#angular/core';
import {AuthService} from '../services/auth.service';
import {IUser} from './user';
#Component({
selector: 'app-login',
templateUrl: 'login.component.html',
styleUrls: ['login.css']
})
export class LoginComponent implements OnInit {
private username: string;
private password: string;
#Input() show: boolean = false;
constructor(private AuthService: AuthService) {
}
ngOnInit() {
}
login() {
...
}
showPopup() {
console.log(this); //Show is false
this.show = true;
console.log(this); //Show is true but does not trigger the show class
}
closePopup() {
this.show = false;
}
}
The issue here is that your nav-bar and login components are siblings and can't directly communicate with each other. You have show as an output of navbar and as an input of login, but you haven't connected the dots.
You need to update your app.component to connect them.
export class AppComponent implements OnInit {
show = false;
onShow() { this.show = true; }
}
and in the template:
<app-nav-bar (show)="onShow()"></app-nav-bar>
<app-login [(show)]="show"></app-login>
There's a lot of two way binding going on here which works for something simple liek this, but generally it's a bad idea as it leads to unmaintainable code. You should choose one owner of the show variable and force all changes to it through him. In this case the app component is the most logical owner, so I'd change the login component to emit an event that changes the show variable in app component adn remove all 2 way bindings, but in a bigger app, you may even want a separate service that manages hiding/showing pop ups. This eliminates the need for the sending a message up and down your component tree, you can inject the service where it's needed.
As another commenter mentioned, you also should be using ngClass for class manipulation like
[ngClass]="{'show':show}"
a service based solution would look like
import {Subject} from 'rxjs/Subject';
#Injectable()
export class PopUpService {
private showPopUpSource = new Subject();
showPopUp$ = this.showPopUpSource.asObservable();
showPopUp() { this.popUpSource.next(true); }
closePopUp() { this.popUpSource.next(false); }
}
Then you provide in app module or at app component level:
providers:[PopUpService]
make sure you don't re provide this later, as you only want one copy to exist so everyone shares it.
then inject into both components, and have them call the services close or show pop up methods.
then in the login component you bind to the popUp$ observable like
constructor(private popUpSvc:PopUpService){}
show$;
ngOnInit() { this.show$ = this.popUpSvc.showPopUp$; }
showPopUp() { this.popUpSvc.showPopUp(); }
closePopUp() { this.popUpSvc.closePopUp(); }
and in the template subscribe w async pipe like
<div id="wrapper-login-popup" class="fade-from-top" [ngClass]="{'show': (show$ | async) }">
The reason for using the async pipe is garbage collection managemetn is simpler. If you don't use async, you need to garbage collect manually in ngOnDestroy by calling unsubscribe(), otherwise your subscriptions will keep stacking up. There is also a more nuanced benefit in that the async pipe triggers change detection, but this only becomes important if you start using onPush change detection for performance optimization.