I use mat-dialog to edit details of my profile page. I'm getting an ExpressionChangedAfterItHasBeenCheckedError when I click the 'Edit age' button and the dialog window pops up.
I decided to extract the styling of all edit dialogs into a single edit.component:
edit.component.html
<div class="navigation-control">
<mat-icon (click)="onCancelButtonClicked()"
class="close-button">close</mat-icon>
</div>
<div class="content-main">
<ng-content select=".content-main"></ng-content>
</div>
<div class="content-bot">
<button mat-raised-button
(click)="onCancelButtonClicked()">Cancel</button>
<button mat-raised-button
(click)="onActionButtonClicked()"
[lnDisableButton]="actionButtonDisabled">{{actionButtonValue}}</button>
</div>
edit.component.ts
#Component({ selector: 'ln-edit', ... })
export class EditComponent {
#Input() actionButtonValue: string;
#Input() actionButtonDisabled: boolean;
#Output() cancelButtonClicked = new EventEmitter<void>();
#Output() actionButtonClicked = new EventEmitter<void>();
onCancelButtonClicked() {
this.cancelButtonClicked.emit();
}
onActionButtonClicked() {
this.actionButtonClicked.emit();
}
}
To avoid the ExpressionChangedAfterItHasBeenCheckedError when trying to disable buttons and controls, I used this snippet. But that didn't solve this issue.
disable-button.directive.ts
#Directive({ selector: '[lnDisableButton]' })
export class DisableButtonDirective {
#Input('lnDisableButton') isDisabled = false;
#HostBinding('attr.disabled')
get disabled() { return this.isDisabled; }
}
The following is the contents of a mat-dialog window. This gets instantiated when I click the 'Edit age' button. When I remove the [actionButtonDisabled]="actionButtonDisabled", the error goes away, but obivously I need that line to make the functionality disable the button.
age-edit.component.html
<ln-edit [actionButtonValue]="actionButtonValue"
[actionButtonDisabled]="actionButtonDisabled"
(cancelButtonClicked)="onCancelButtonClicked()"
(actionButtonClicked)="onActionButtonClicked()">
<form [formGroup]="ageForm"
class="content-main">
<ln-datepicker formControlName="birthday"
[appearance]="'standard'"
[label]="'Birthday'"
class="form-field">
</ln-datepicker>
</form>
</ln-edit>
I handle the disabling/enabling the button in the 'ts' part of the mat-dialog popup.
age-edit.component.ts
#Component({ selector: 'ln-age-edit', ... })
export class AgeEditComponent implements OnInit, OnDestroy {
ageForm: FormGroup;
private initialFormValue: any;
actionButtonDisabled = true;
private unsubscribe = new Subject<void>();
constructor(
private editPhotoDialogRef: MatDialogRef<AgeEditComponent>,
private fb: FormBuilder,
#Inject(MAT_DIALOG_DATA) public dialogData: Date) { }
ngOnInit() {
this.initializeAgeForm();
this.loadDataToAgeForm(this.dialogData);
this.trackFormDistinct();
}
private initializeAgeForm(): void {
this.ageForm = this.fb.group({
birthday: null,
});
}
loadDataToAgeForm(birthday: Date | null): void {
if (!birthday) { return; }
this.ageForm.setValue({ birthday });
this.initialFormValue = this.ageForm.value;
}
get birthdayAC() { return this.ageForm.get('birthday') as AbstractControl; }
get actionButtonValue(): string {
return this.birthdayAC.value ? 'Update age' : 'Add age';
}
onCancelButtonClicked(): void {
this.editPhotoDialogRef.close();
}
onActionButtonClicked(): void {
this.editPhotoDialogRef.close({ ... });
}
trackFormDistinct(): void {
this.ageForm.valueChanges.pipe(
distinctUntilChanged(), // TODO: needed?
takeUntil(this.unsubscribe)
).subscribe(val => {
(this.formValueNotDistinct(this.ageForm.value, this.initialFormValue)
|| this.birthdayAC.value === null)
? this.actionButtonDisabled = true
: this.actionButtonDisabled = false;
});
}
ngOnDestroy() { ... }
}
I suspect this has something to do with content projection, but I'm not sure.
(...or perhaps with my custom 'ln-datepicker'?)
Any ideas?
Thanks.
From what I can tell, the problem resides in trackFormDistinct() method:
trackFormDistinct(): void {
this.ageForm.valueChanges.pipe(
distinctUntilChanged(), // TODO: needed?
takeUntil(this.unsubscribe)
).subscribe(val => {
(this.formValueNotDistinct(this.ageForm.value, this.initialFormValue)
|| this.birthdayAC.value === null)
? this.actionButtonDisabled = true
: this.actionButtonDisabled = false;
});
}
Looks like because of this.ageForm.valueChanges, will have different values in the 2 change detection cycles. I think this.ageForm.valueChanges emits due to <ln-datepicker>.
In a tree of form controls, if one node calls setValue, all its ancestors will have to be updated. I've written more about how Angular Forms work in this article.
I'm thinking of 2 alternatives:
skip the first emission of ageForm since it indicates the initialization of the form control tree, so this is irrelevant to the logic inside subscribe's callback.
this.ageForm.valueChanges.pipe(
skip(1),
distinctUntilChanged(), // TODO: needed?
takeUntil(this.unsubscribe)
).subscribe(/* .... */)
initialize actionButtonDisabled with false, since the error complains that it switched from true to false
actionButtonDisabled = false;
I have this block of html in my template to show or hide the div.
<div *ngIf="csvVisible">
<p>Paragraph works</p>
</div>
This is my component.
export class SettingsComponent implements OnInit {
csvVisible: boolean = false;
private dataSource: string[];
#ViewChild(MatTable, { static: true }) table: MatTable<any>;
constructor(private dialog: MatDialog, private templateParserService: TemplateParserService) { }
ngOnInit() {
this.templateParserService.subscribe({
next(result: string[]) {
if (result !== null) {
this.dataSource = result;
if (this.dataSource && this.dataSource.length) {
this.csvVisible = true;
} else {
this.csvVisible = false;
}
}
},
error(error: Error) {
console.log(error.message);
}
});
}
Eventhough the DIV is hidden at start, it doesnt automatically show / hide on the csvVisible value change. Value of csvVisible is properly set when the observer emits data. [hidden]="csvVisible" isn't working either.
Edit :
Subscriber registration on the service is done by the following code.
private subject = new Subject<string[]>();
public subscribe(observer: any): Subscription {
return this.subject.subscribe(observer);
}
Since you are using Object inside subscribe, this points to current subscribe object, Instead of using subscribe({next:()}) try using this way
component.ts
this.templateParserService.subscribe((result: string[])=>{
if (result !== null) {
this.dataSource = result;
if (this.dataSource && this.dataSource.length) {
this.csvVisible = true;
} else {
this.csvVisible = false;
}
}
},(error: Error)=>{
console.log(error.message);
});
I am trying to filter my datatable by clicking a checkbox. Checking one checkbox filters it properly, but checking multiple is the problem.
The Angular Material documentation is very vague regarding proper filtering where a lot of elements are involved. It has something to do with filterPredicate, but there is almost no (or just awfully vague) online documentation.
<mat-form-field appearance="legacy">
<mat-label>Select Province(s)</mat-label>
<mat-select placeholder='Provinces' formControlName="provinceSelector" multiple>
<mat-option *ngFor="let p of provinces" [value]='p.provinceName' (click)='addfilter()'>
{{p.provinceName}} ({{p.adsInProvince}})
</mat-option>
</mat-select>
</mat-form-field>
this.registeredUserService.GetAllAdverts().subscribe(val => {
this.dataSource = new MatTableDataSource<Card>(val);
this.dataSource.paginator = this.paginator;
const myPredicate = (myObject:IProvince,filterString:any)=>
{
let filterObj:IProvince = JSON.parse(filterString);
if(!filterObj.provinceName.includes((obj)=>obj=myObject.provinceName))
{
return false;
}
else
{return true;}
}
this.dataSource.filterPredicate=myPredicate;
myFilter:IProvince={
provinceName:[]
}
addfilter() {
this.myFilter.provinceName=this.search.value;
this.dataSource.filter = JSON.stringify(this.myFilter);
}
export interface Card {
advertDate: any;
advertDescription: any;
advertID: any;
cityName: any;
provinceName: any;
sellerID: any;
sellingPrice: any;
vehicleColor: any;
vehicleMake: any;
vehicleMileage: any;
vehicleModel: any;
vehicleYear: any;
}
export interface IProvince{
provinceName:any[];
}
it should just filter through the selected values...
it does not do it properly.
You are right with the filter predicate. You can define how to filter you dataSource. It is a function which returns true(if filter matches) or false.
const myPredicate = (myObject, filterString) => {
let filterObj: MyFilterObj = JSON.parse(filterString);
if(!filterObj.attributeA.find((obj) => obj == myObject.name) ||myObject.attributeB != filterObject.attributeB) { // <-- edit includes to find
return false;
} else {
return true;
}
}
Put the following code after generating your MatTableDataSource:
this.dataSource.filterPredicate = this.myPredicate.
Use the following method for setting your filter. I always give a type string for determining which filter I want to set.
myFilter: MyFilterObject = { // <-- define this variable in your ngOnInit method
attributeA: [], // <-- this is the place where you can put your selected options
attributeB: null
}
applyFilter(value: any, type: string) {
switch(type) {
case "attributeA":
this.myFilter.attributeA = value;
break;
case "attributeB":
this.myFilter.attributeB = value;
break;
default:
break;
}
this.dataSource.filter = JSON.stringify(this.myFilter);
}
When I click to edit I can edit but when I try to append a kind to the parent it takes the parent infoprmation and not create a new VI.
With angular dialog I made like this but with component I dont know.
This is with dialog
<div class="dropdown-menu-item" (click)="openValueItemEditDialog({valueItem: valueItem})">Edit</div>
This is the TS
openValueItemEditDialog(editOptions: EditOptions) {
this.dialog.open(ValueItemEditDialogComponent, {data: editOptions, disableClose: true});
}
And this is the TS of dialog
constructor(private store: Store<ApplicationState>, #Inject(MAT_DIALOG_DATA) public editOptions: EditOptions) {
}
ngOnInit() {
if (this.editOptions.valueItem) {
this.editedValueItem = _.cloneDeep(this.editOptions.valueItem);
} else {
this.editedValueItem = emptyValueItem();
}
export interface EditOptions {
valueItem?: ValueItem;
appendToParentId?: string;
planning?: number;
}
But without dialog I dont know how to pass the data
showChild = false
edit(editOptions: EditViOptions) {
this.showChild = !this.showChild;
if (editOptions.valueItem) {
return editOptions.valueItem;
} else if (editOptions.appendToParentId) {
return editOptions.appendToParentId;
}
this.activeSelected = this.valueItem.id;
} //this open and closes the component
}
This is the Html of Service matrix row
<app-edit-dialog-vi [showMePartially]="showChild"></app-edit-dialog-vi>
<div *ngIf="valueItem.level < 7" class="dropdown-menu-item" (click)="edit({appendToParentId: valueItem})">Append2</div>
Here is the edit-dialog-vi
<div *ngIf="showMePartially" class="container">
</div>
In the constructor I dont know what to write
This is the TS file and interface what I have created for the Edit vi dialog
constructor(private store: Store<ApplicationState>, public editOptions: ServiceMatrixRowComponent) { }
ngOnInit() {
if (this.editOptions.valueItem) {
this.editedValueItem = _.cloneDeep(this.editOptions.valueItem);
console.log(this.editedValueItem, '***');
} else {
this.editedValueItem = emptyValueItem();
}
export interface EditViOptions {
valueItem?: ValueItem;
appendToParentId?: string;
planning?: number;
}
I don't know if I understood your question well, but you can try this :
ParentHtml
<app-edit-dialog-vi [data]="dataToPass" [showMePartially]="showChild"></app-edit-dialog-vi>
<div class="dropdown-menu-item" (click)="edit(valueItem.level < 7 ? {appendToParentId: valueItem.id} : {valueItem: valueItem})">Edit2</div>
Parent Ts
Add a property dataToPass, and set it to the data you want to send to the child.
showChild = false;
dataToPass: EditViOptions = null;
edit(editOptions: EditViOptions) {
this.showChild = !this.showChild;
if (editOptions.valueItem || editOptions.appendToParentId) {
this.dataToPass = editOptions;
}
this.activeSelected = editOptions.valueItem.id;
}
Child Ts
Add an input named data to get the data sent :
#Input() showMePartially = false;
#Input() data: EditViOptions = null;
constructor(private store: Store<ApplicationState>, public editOptions: ServiceMatrixRowComponent) { }
ngOnInit() {
if (this.data.valueItem || this.data.appendToParentId) {
this.editedValueItem = _.cloneDeep(this.data.valueItem);
console.log(this.editedValueItem, '***');
} else {
this.editedValueItem = emptyValueItem();
}
export interface EditViOptions {
valueItem?: ValueItem;
appendToParentId?: string;
planning?: number;
}
To learn better i will suggest you just look at the below link. It will let you know how to pass data from Parent-> Child and Child-->Parent component in different ways.
https://angularfirebase.com/lessons/sharing-data-between-angular-components-four-methods/
Hope it gonna help you.
Thanks
How can I detect clicks outside a component in Angular?
import { Component, ElementRef, HostListener, Input } from '#angular/core';
#Component({
selector: 'selector',
template: `
<div>
{{text}}
</div>
`
})
export class AnotherComponent {
public text: String;
#HostListener('document:click', ['$event'])
clickout(event) {
if(this.eRef.nativeElement.contains(event.target)) {
this.text = "clicked inside";
} else {
this.text = "clicked outside";
}
}
constructor(private eRef: ElementRef) {
this.text = 'no clicks yet';
}
}
A working example - click here
An alternative to AMagyar's answer. This version works when you click on element that gets removed from the DOM with an ngIf.
http://plnkr.co/edit/4mrn4GjM95uvSbQtxrAS?p=preview
private wasInside = false;
#HostListener('click')
clickInside() {
this.text = "clicked inside";
this.wasInside = true;
}
#HostListener('document:click')
clickout() {
if (!this.wasInside) {
this.text = "clicked outside";
}
this.wasInside = false;
}
Binding to a document click through #Hostlistener is costly. It can and will have a visible performance impact if you overuse it (for example, when building a custom dropdown component and you have multiple instances created in a form).
I suggest adding a #Hostlistener() to the document click event only once inside your main app component. The event should push the value of the clicked target element inside a public subject stored in a global utility service.
#Component({
selector: 'app-root',
template: '<router-outlet></router-outlet>'
})
export class AppComponent {
constructor(private utilitiesService: UtilitiesService) {}
#HostListener('document:click', ['$event'])
documentClick(event: any): void {
this.utilitiesService.documentClickedTarget.next(event.target)
}
}
#Injectable({ providedIn: 'root' })
export class UtilitiesService {
documentClickedTarget: Subject<HTMLElement> = new Subject<HTMLElement>()
}
Whoever is interested for the clicked target element should subscribe to the public subject of our utilities service and unsubscribe when the component is destroyed.
export class AnotherComponent implements OnInit {
#ViewChild('somePopup', { read: ElementRef, static: false }) somePopup: ElementRef
constructor(private utilitiesService: UtilitiesService) { }
ngOnInit() {
this.utilitiesService.documentClickedTarget
.subscribe(target => this.documentClickListener(target))
}
documentClickListener(target: any): void {
if (this.somePopup.nativeElement.contains(target))
// Clicked inside
else
// Clicked outside
}
Improving J. Frankenstein's answer:
#HostListener('click')
clickInside($event) {
this.text = "clicked inside";
$event.stopPropagation();
}
#HostListener('document:click')
clickOutside() {
this.text = "clicked outside";
}
The previous answers are correct, but what if you are doing a heavy process after losing the focus from the relevant component? For that, I came with a solution with two flags where the focus out event process will only take place when losing the focus from relevant component only.
isFocusInsideComponent = false;
isComponentClicked = false;
#HostListener('click')
clickInside() {
this.isFocusInsideComponent = true;
this.isComponentClicked = true;
}
#HostListener('document:click')
clickout() {
if (!this.isFocusInsideComponent && this.isComponentClicked) {
// Do the heavy processing
this.isComponentClicked = false;
}
this.isFocusInsideComponent = false;
}
ginalx's answer should be set as the default one imo: this method allows for many optimizations.
The problem
Say that we have a list of items and on every item we want to include a menu that needs to be toggled. We include a toggle on a button that listens for a click event on itself (click)="toggle()", but we also want to toggle the menu whenever the user clicks outside of it. If the list of items grows and we attach a #HostListener('document:click') on every menu, then every menu loaded within the item will start listening for the click on the entire document, even when the menu is toggled off. Besides the obvious performance issues, this is unnecessary.
You can, for example, subscribe whenever the popup gets toggled via a click and start listening for "outside clicks" only then.
isActive: boolean = false;
// to prevent memory leaks and improve efficiency, the menu
// gets loaded only when the toggle gets clicked
private _toggleMenuSubject$: BehaviorSubject<boolean>;
private _toggleMenu$: Observable<boolean>;
private _toggleMenuSub: Subscription;
private _clickSub: Subscription = null;
constructor(
...
private _utilitiesService: UtilitiesService,
private _elementRef: ElementRef,
){
...
this._toggleMenuSubject$ = new BehaviorSubject(false);
this._toggleMenu$ = this._toggleMenuSubject$.asObservable();
}
ngOnInit() {
this._toggleMenuSub = this._toggleMenu$.pipe(
tap(isActive => {
logger.debug('Label Menu is active', isActive)
this.isActive = isActive;
// subscribe to the click event only if the menu is Active
// otherwise unsubscribe and save memory
if(isActive === true){
this._clickSub = this._utilitiesService.documentClickedTarget
.subscribe(target => this._documentClickListener(target));
}else if(isActive === false && this._clickSub !== null){
this._clickSub.unsubscribe();
}
}),
// other observable logic
...
).subscribe();
}
toggle() {
this._toggleMenuSubject$.next(!this.isActive);
}
private _documentClickListener(targetElement: HTMLElement): void {
const clickedInside = this._elementRef.nativeElement.contains(targetElement);
if (!clickedInside) {
this._toggleMenuSubject$.next(false);
}
}
ngOnDestroy(){
this._toggleMenuSub.unsubscribe();
}
And, in *.component.html:
<button (click)="toggle()">Toggle the menu</button>
Alternative to MVP, you only need to watch for Event
#HostListener('focusout', ['$event'])
protected onFocusOut(event: FocusEvent): void {
console.log(
'click away from component? :',
event.currentTarget && event.relatedTarget
);
}
Solution
Get all parents
var paths = event['path'] as Array<any>;
Checks if any parent is the component
var inComponent = false;
paths.forEach(path => {
if (path.tagName != undefined) {
var tagName = path.tagName.toString().toLowerCase();
if (tagName == 'app-component')
inComponent = true;
}
});
If you have the component as parent then click inside the component
if (inComponent) {
console.log('clicked inside');
}else{
console.log('clicked outside');
}
Complete method
#HostListener('document:click', ['$event'])
clickout(event: PointerEvent) {
var paths = event['path'] as Array<any>;
var inComponent = false;
paths.forEach(path => {
if (path.tagName != undefined) {
var tagName = path.tagName.toString().toLowerCase();
if (tagName == 'app-component')
inComponent = true;
}
});
if (inComponent) {
console.log('clicked inside');
}else{
console.log('clicked outside');
}
}
You can use the clickOutside() method from the ng-click-outside package; it offers a directive "for handling click events outside an element".
NB: This package is currently deprecated. See https://github.com/arkon/ng-sidebar/issues/229 for more info.
Another possible solution using event.stopPropagation():
define a click listener on the top most parent component which clears the click-inside variable
define a click listener on the child component which first calls the event.stopPropagation() and then sets the click-inside variable
You can call an event function like (focusout) or (blur); then you would put in your code:
<div tabindex=0 (blur)="outsideClick()">raw data </div>
outsideClick() {
alert('put your condition here');
}
nice and tidy with rxjs.
i used this for aggrid custom cell editor to detect clicks inside my custom cell editor.
private clickSubscription: Subscription | undefined;
public ngOnInit(): void {
this.clickSubscription = fromEvent(document, "click").subscribe(event => {
console.log("event: ", event.target);
if (!this.eRef.nativeElement.contains(event.target)) {
// ... click outside
} else {
// ... click inside
});
public ngOnDestroy(): void {
console.log("ON DESTROY");
this.clickSubscription?.unsubscribe();
}