I have an input drop-down component which is used in multiple places in the same app but in different tabs.
The issue I am facing is when I select a value from the drop-down in Tab 1 and an API call is done with the value, the same component in Tab 2 also does that with the selected value in Tab1.
How do I fix this as I am subscribing to the same service in the different tabs?
<ng-select
[items]="Groups"
[virtualScroll]="true"
bindLabel="bg_desc"
bindValue="bg_desc"
placeholder="Groups"
[(ngModel)]="selectedGroup"
[clearable]="false"
(change)="selectGroups()">
<ng-template
ng-notfound-tmp
let-searchTerm="searchTerm">
<div class="ng-option disabled">
No data found for "{{searchTerm}}"
</div>
</ng-template>
<ng-template
ng-option-tmp
let-item="item"
let-search="searchTerm">
<div
[ngOptionHighlight]="search"
class="text-uppercase">
{{item.bg_desc}}
</div>
</ng-template>
</ng-select>
This is in my component:
selectGroups() {
this._data.changeGroup(this.selectedGroup);
}
This is my service:
changeGroup(bg: string) {
this.changeGroupData.next(bg);
}
private changeGroupData = new BehaviorSubject<string>('');
currentChangeGroupData = this.changeGroupData.asObservable();
This is my stackbliz example: https://stackblitz.com/edit/angular-1oucud
I want individual calls on these tabs. Should I create three instances of same component with different names to achieve this?
Think about the architecture of your program? Should DropDownComponent really be updating a service after a model change or is more like a more specific input control and any binding or application logic should occur outside of it?
It seems to me that the second case is more appropriate, especially if you feel the need to reuse it. You can easily modify the DropDownComponent to have an Input and Output and have the outer component bind to it. Or you can go the extra mile and have your component extend NgModelAccessor, so you can use it properly in forms.
I'll give an example of the simpler approach below.
DropDownComponent is to changed to be completely standalone. It has an input and output that other components will bind to.
AppComponent has a model and the properties of the model are bound to instances of dropdown in the view. For no particular reason I also bound to the change event just to show you what happens. It really isn't necessary as doing the banana-in-a-box syntax will cause the Output to be bound to by convention - the Output having the same name as the input with Change appended to the end.
DropDownComponent.ts
export class DropdownComponent {
colors = colors;
#Input() selectedColor;
#Output() selectedColorChange = new EventEmitter<string>();
changeColor(e) {
this.selectedColorChange.emit(this.selectedColor);
}
}
AppComponent.ts
declare type ColorType = { color: string, value: string };
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent {
colors: { first?: ColorType, second?: ColorType, third?: ColorType } = {};
doSomething(colorKey: string) {
console.log(`The color changed was ${colorKey} with a value of ${this.colors[colorKey].value}.`)
}
}
AppComponent.html
<mat-tab-group>
<mat-tab label="First">
<dropdown [(selectedColor)]="colors.first" (selectedColorChange)="doSomething('first')"></dropdown>
<p>Selected Color is {{colors.first?.color}}</p>
</mat-tab>
<mat-tab label="Second">
<dropdown [(selectedColor)]="colors.second" (selectedColorChange)="doSomething('second')"></dropdown>
<p>Selected Color is {{colors.second?.color}}</p>
</mat-tab>
<mat-tab label="Third">
<dropdown [(selectedColor)]="colors.third" (selectedColorChange)="doSomething('third')"></dropdown>
<p>Selected Color is {{colors.third?.color}}</p>
</mat-tab>
</mat-tab-group>
You just need to use output to communicate to outer component. Thats it
https://stackblitz.com/edit/angular-1oucud
Related
I got two components that i want to connect. In component "x" i got some text in the template file and in component "y" i got a button that i want to click to replace/change the text in component "x".
this is "x" text i want to change:
<p> Text i want to replace </p>
this is "y" component.ts text i want to replace with:
changeTheText: string = "Text i want to change to";
showMyContainer2: boolean = false;
clearMe(){
this.showMyContainer2 = true;
this.UIS.clearStorage();
}
this is "y" component.template:
<button id="clearBtn" (click)="clearMe()">Change the text button</button>
<div *ngIf="showMyContainer2">
{{changeTheText}}
</div>
You can do this by using EventEmitters
https://angular.io/api/core/EventEmitter
Is x a direct child of y? Meaning is the HTML like this?
<y>
<x></x>
</y>
If so, you can use #Input() properties
In x component.ts, do this:
// import Output and EventEmitter from '#angular/core`;
#Input text: string;
And I assume the HTML is:
<p>{{ text }}</p>
Then in y.component.ts, do:
clearMe(){
this.showMyContainer2 = true;
this.UIS.clearStorage();
this.changeTheText = 'New Text you want'
}
And in y.html, do:
<div class="y">
<x [text]="changeTheText"></x>
</div>
You can possibly use EventEmitters like Mamosek mentioned but it depends on the heirarchy of x and y. Are they parent => child or child => parent.
If they don't have parent child relationship, you have to create a middle man Service that has a BehaviorSubject and both x and y have to inject this Service and communicate through that BehaviorSubject by doing .next to push a new value and .subscribe to listen to values.
====================== Edit ================================
Does it make sense for the text to live in component Z?
Component Z.ts
textToChange = 'Change this text';
changeTheText() {
this.textToChange = 'new text';
}
Component Z.html
<div class="z">
// feed the text down to the x component
<x [text]="textToChange"></x>
// listen to the textChanged event from component Y and every time it happens call the function changeTheText
<y (textChanged)="changeTheText()"></y>
</div>
Component X.ts
// take input of text from parent's location
#Input() text: string;
Component X.html
<div>{{ text }}</div>
Component Y.ts
#Output textChanged: EventEmitter<void> = new EventEmitter();
changeTheText() {
// emit the event so the text can change in component Z
this.textChanged.emit();
}
Component Y.html
<button (click)="changeTheText()">Change the text</button>
I freestyled all of that so there might be some errors but you get the idea.
If the text cannot live in component Z, you will have to have a centralized approach like I mentioned before this edit.
import { Injectable } from '#angular/core';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
#Injectable({
providedIn: 'root',
})
export class TextChangeService {
text = new BehaviorSubject('initial value of text');
constructor() { }
}
component X.ts (The HTML's for both components remain the same)
text: string;
constructor(textService: TextService){ }
ngOnInit() {
this.text.subscribe(text => this.text = text);
}
component Y.ts
constructor(textService: TextService){ }
changeTheText() {
this.textService.next('New text');
}
This last approach I showed you, I don't recommend because it turns into convoluted code the more you build on it. I suggest you learn the Redux pattern and use NGRX.
This question already has answers here:
Detect click outside Angular component
(12 answers)
Closed 7 months ago.
I know there are countless of questions around this, and I tried every solution but nothing suited my needs. Tried directives, tried it straight in my component but it didn't work a single time.
So my situation is like this; I have a tableview and at the end of every tableview you can open a small dropdown by clicking on an icon. The dropdown that pops up is a small component. So in my parent component it looks like this;
<tbody>
<tr *ngFor="let rows of subscriptionTable.data; let i = index;">
<td *ngFor="let cell of subscriptionTable.data[i].data">
<!-- Table actions -->
<div *ngIf="cell.actions && cell.actions.length > 0">
<app-dropdown
[actions] = cell.actions
(onActionClicked) = "handleSubscriptionAction($event, i)"
>
</app-dropdown>
</div>
</td>
</tr>
</tbody>
So I'm rendering multiple child app-dropdown components that look like this;
<div class="fs-action">
<div class="fs-action__dots" (click)="openActionSheet($event)">
<span class="fs-action__dot"></span>
<span class="fs-action__dot"></span>
<span class="fs-action__dot"></span>
</div>
<ul class="fs-action__list">
<li
class="fs-action__item"
*ngFor="let action of actions; let i = index;"
(click)="actionClicked( $event, i )"
[ngClass]="{'fs-action__item--danger': action.type === 'destructive' }"
>
{{ action.label }}
</li>
</ul>
</div>
What I want to do now is, that as soon as I click outside the fs-action element, the dropdown dismisses. I would like to do that inside the app-dropdown component, then everything related to this component is in the same place.
But as soon as I attach a directive or anything else, the hostlistener get's added for every row. Every outside click is then being triggered multiple times. Checking if I was clicking outside the action element then becomes a burden, because it then renders false for all the other hostlisteners and true for the one that's active.
The issue is illustrated inside this stackblitz; https://stackblitz.com/edit/angular-xjdmg4
I commented the section which causes issues inside dropdown.component.ts;
public onClickOutside( event ) {
// It executes 3 times, for each of the items that is added
// Even when clicking inside the dropdown, it calls this function
console.log("click")
}
Just use the (click) event binding, pass a function from the component’s instance and you are done.
Solution:
Declare Directive ClickOutside like below:
#Directive({
selector: '[clickOutside]',
})
export class ClickOutsideDirective {
#Output('onClickOutside') onClickOutside = new EventEmitter<MouseEvent>();
constructor(private _eref: ElementRef) {}
#HostListener('document:click', ['$event', '$event.target'])
onDocumentClicked(event: MouseEvent, targetElement: HTMLElement) {
if (targetElement && document.body.contains(targetElement) && !this._eref.nativeElement.contains(targetElement)) {
this.onClickOutside.emit(event);
}
}
}
}
<div clickOutside (onClickOutside)="onClickOutside()">
...
</div>
And there is directive in an npm module called ng-click-outside.
Installation: npm install --save ng-click-outside
Read full documentation in link below:
ng-click-outside directive
I used a directive to a similar situation.
To install:
npm install --save ng-click-outside
Then import in your module
import { ClickOutsideModule } from 'ng-click-outside';
#NgModule({
declarations: [AppComponent],
imports: [BrowserModule, ClickOutsideModule],
bootstrap: [AppComponent]
})
class AppModule {}
In your component
#Component({
selector: 'app',
template: `
<div (clickOutside)="onClickedOutside($event)">Click outside this</div>
`
})
export class AppComponent {
onClickedOutside(e: Event) {
console.log('Clicked outside:', e);
}
}
It has some options. You can see it in https://www.npmjs.com/package/ng-click-outside
Regards
The ngAfterViewInit lifecycle hook is not being called for a Component that is transcluded into another component using <ng-content> like this:
<app-container [showContent]="showContentContainer">
<app-input></app-input>
</app-container>
However, it works fine without <ng-content>:
<app-input *ngIf="showContent"></app-input>
The container component is defined as:
#Component({
selector: 'app-container',
template: `
<ng-container *ngIf="showContent">
<ng-content></ng-content>
</ng-container>
`
})
export class AppContainerComponent {
#Input()
showContentContainer = false;
#Input()
showContent = false;
}
The input component is defined as:
#Component({
selector: 'app-input',
template: `<input type=text #inputElem />`
})
export class AppInputComponent implements AfterViewInit {
#ViewChild("inputElem")
inputElem: ElementRef<HTMLInputElement>;
ngAfterViewInit() {
console.info("ngAfterViewInit fired!");
this.inputElem.nativeElement.focus();
}
}
See a live example here: https://stackblitz.com/edit/angular-playground-vqhjuh
There are two issues at hand here:
Child components are instantiated along with the parent component, not when <ng-content> is instantiated to include them. (see https://github.com/angular/angular/issues/13921)
ngAfterViewInit does not indicate that the component has been attached to the DOM, just that the view has been instantiated. (see https://github.com/angular/angular/issues/13925)
In this case, the problem can be solved be addressing either one of them:
The container directive can be re-written as a structural directive that instantiates the content only when appropriate. See an example here: https://stackblitz.com/edit/angular-playground-mrcokp
The input directive can be re-written to react to actually being attached to the DOM. One way to do this is by writing a directive to handle this. See an example here: https://stackblitz.com/edit/angular-playground-sthnbr
In many cases, it's probably appropriate to do both.
However, option #2 is quite easy to handle with a custom directive, which I will include here for completeness:
#Directive({
selector: "[attachedToDom],[detachedFromDom]"
})
export class AppDomAttachedDirective implements AfterViewChecked, OnDestroy {
#Output()
attachedToDom = new EventEmitter();
#Output()
detachedFromDom = new EventEmitter();
constructor(
private elemRef: ElementRef<HTMLElement>
) { }
private wasAttached = false;
private update() {
const isAttached = document.contains(this.elemRef.nativeElement);
if (this.wasAttached !== isAttached) {
this.wasAttached = isAttached;
if (isAttached) {
this.attachedToDom.emit();
} else {
this.detachedFromDom.emit();
}
}
}
ngAfterViewChecked() { this.update(); }
ngOnDestroy() { this.update(); }
}
It can be used like this:
<input type=text
(attachedToDom)="inputElem.focus()"
#inputElem />
If you check the console of your stackblitz, you see that the event is fired before pressing any button.
I can only think of that everything projected as will be initialized/constructed where you declare it.
So in your example right between these lines
<app-container [showContent]="showContentContainer">
{{test()}}
<app-input></app-input>
</app-container>
If you add a test function inside the app-container, it will get called immediatly. So <app-input> will also be constructed immediatly. Since ngAfterVieWInit will only get called once (https://angular.io/guide/lifecycle-hooks), this is where it will be called already.
adding the following inside AppInputComponent is a bit weird however
ngOnDestroy() {
console.log('destroy')
}
the component will actually be destroyed right away and never initialized again (add constructor or onInit log to check).
I have a scenario where I am generating dynamic elements with the data from backend some what like
<tr *ngFor="let data of datas" (click)="display(data.id)">
<div id="'full'+data.id" [style.display]=" active? 'block': 'none'">
</div>
</tr>
My Ts File
export class Component{
active=false;
display(id)
{
document.getElementById(`full${id}`).display="block";
}
}
What I want to do is something like above. I tried something like below but that doesn't work it throws error
Property 'display' does not exist on type 'HTMLInputElement'
import { DOCUMENT } from '#angular/common';
import { Inject } from '#angular/core';
export class Component{
active=false;
constructor(#Inject(DOCUMENT) document) {
}
display(id)
{
document.getElementById(`full${id}`).display="block";
}
}
any suggestions ,how to do this .Thanks
Property 'display' does not exist on type 'HTMLInputElement'
That's because HTML elements do not have a property display. What you're looking for is:
document.getElementById(`full${id}`).style.display='block';
Rather than directly manipulating the DOM, the more Angular way of doing this would be to track the visibility state of each row and drive visibility through NgIf
NgIf: A structural directive that conditionally includes a template based on the value of an expression coerced to Boolean. When the expression evaluates to true, Angular renders the template provided in a then clause, and when false or null, Angular renders the template provided in an optional else clause. The default template for the else clause is blank.
Here is an example with a single boolean driving the toggle of a single div, but you could do something similar with a map on your data.
#Component({
selector: 'ng-if-simple',
template: `
<button (click)="show = !show">{{show ? 'hide' : 'show'}}</button>
show = {{show}}
<br>
<div *ngIf="show">Text to show</div>
`
})
export class NgIfSimple {
show: boolean = true;
}
I've solve problem like this:
let fullId = document.getElementById(`full${id}`) as HTMLElement;
fullId.style.display='block';
I have two components : TileComponent.ts and FullScreenComponent.ts.
On clicking the TileComponent, the FullScreenComponent opens up. In the TileComponent,I have the following code. ngOnInit() method gets triggered whenever the TileComponent loads.
TileComponent.ts:
ngOnInit() {
console.log("TileCompnent :ngOnInit");
this.crossDomainService.globalSelectors.subscribe(selectors => {
globalCountries = selectors.jurisdiction || [];
this.getArticles(globalCountries);
});
// Multiple Language
this.crossDomainService.globalLanguage.subscribe(() => {
console.log("TileCompnent :ngOnInit : crossDomainService");
this.getArticles(globalCountries || countries);
});
}
Now on closing the FullScreenComponent leads to the loading of the TileComponent but this time I see that ngOnInit() method is not getting triggered.
Can anyone help me to know any reason this is not working?
tile.component.html:
<div class="carousel-inner">
<a
(click)="openFullScreen(article)"
*ngFor="let article of articles"
[ngClass]="getItemClassNames(article)"
class="item"
>
</div>
tile.component.ts
ngOnInit() {
console.log("TileCompnent :ngOnInit");
const countries =
this.crossDomainService.initialGlobalSelectors &&
this.crossDomainService.initialGlobalSelectors.jurisdiction.length
? this.crossDomainService.initialGlobalSelectors.jurisdiction
: [];
this.getArticles(countries);
let globalCountries;
this.crossDomainService.globalSelectors.subscribe(selectors => {
globalCountries = selectors.jurisdiction || [];
this.getArticles(globalCountries);
});
// Multiple Language
this.crossDomainService.globalLanguage.subscribe(() => {
console.log("TileCompnent :ngOnInit : crossDomainService");
this.getArticles(globalCountries || countries);
});
}
openFullScreen(article: ArticlePreview) {
this.crossDomainService.openFullScreen(article);
}
full-screen.component.html:
<div class="layout-center-wrapper" [hidden]="isPolicyShown">
<app-header></app-header>
<div class="row wrapper">
<app-sidebar></app-sidebar>
<router-outlet></router-outlet>
</div>
</div>
<app-policy [hidden]="!isPolicyShown"></app-policy>
header.component.html:
<header class="row header">
<p class="header__title">
Application Name
<a (click)="closeFullScreen()" class="header__close">
<span class="icon icon_close"></span>
</a>
</p>
</header>
header.component.ts:
import { Component } from '#angular/core';
import { CrossDomainService } from '../../core';
#Component({
selector: 'app-header',
templateUrl: './header.component.html',
styleUrls: ['./header.component.less']
})
export class HeaderComponent {
constructor(private crossDomainService: CrossDomainService, private analyticsService: AnalyticsService) {}
closeFullScreen() {
this.crossDomainService.closeFullScreen();
}
}
ngOnInit lifecycle is only run when the view of the component is first rendered.
Since the old Tile Component is not destroyed and is always in the background even when the FullScreenComponent is displayed, the lifecycle hook never gets triggered even when you close the component.
( I am assuming you are not using the router to navigate, but use it as a popup since there is a close button as shown in the question )
Cannot help you isolate the issue or help you with suggestions unless you share some code. But the reason for ngOnInit not firing as per the question is because the component is not re-created.
Update :
I still can't realise why you need to trigger the ngOnInit ? If you just want to execute the code inside, make it a separate function say initSomething then call it inside ngOnInit to execute it the first time. Now if you just invoke this function on crossDomainService.closeFullScreen you get the desired effect.
To trigger the function whenever the closeFullScreen is called, you can create a Subject in the crossDomainService Service, and subscribe this subject it inside the ngOnInit(), and run the initSomething function mentioned above everytime it emits a value. Inside the closeFullScreen function, all you have to now do is do a Subject.next()
Pardon the brevity since I am away from my desk and typing on mobile, though the explanation should be enough to develop the code on your own.
One of the simple workaround would be to use changedetectorRef to hook up the initial state of component.
`import { Component, OnInit, ChangeDetectorRef } from '#angular/core';`
and insert it in constructor and you can keep OnInit function blank
constructor(){
this.changeDetectorRef.detectChanges();}