Angular 2 contenteditable div, move caret to end position - html

I have following Angular 2 code:
write.component.ts:
import { CommentHeaderComponent } from './comment.header.component';
import { StyleService } from './../../services/services/style.service';
import { Parameters } from './../../services/services/parameters';
import { Component, Input, ViewChild,ElementRef,HostBinding} from "#angular/core";
import { ContenteditableModel } from './../directives/contenteditable.directive';
#Component({
selector:'write',
templateUrl:'app/templates/write.component.html',
styleUrls:['app/templates/css/write.component.css']
})
export class WriteComponent
{
#HostBinding('class.hbox')
parameters:Parameters;
private writeText:string="";
private rows:number=1;
private maxRows:number=4;
private comment:boolean=false;
private lineHeight:string="1.7em";
#ViewChild('writeBox') writeBox:ElementRef;
constructor(private stService:StyleService){}
ngOnInit()
{
this.writeBox.nativeElement.innerText="";
this.stService.setProperty(this.writeBox,[{rows:this.rows}]);
this.stService.setStyle(this.writeBox,[{lineHeight:this.lineHeight}]);
if (this.parameters.Iskey("comment"))
{
this.comment=true;
}
}
write(data:any)
{
this.parameters.cfunc({event:"comment",message : this.writeText});
this.writeText="";
}
getWriteText():String
{
return this.writeText;
}
}
write.component.html:
<div contenteditable="true" #writeBox [ceModel]="writeText" (ceChange)="writeText=$event" [innerHTML]="writeText">
</div>
<ng-template [ngIf]="this.comment">
<button type="button" (click)="files($event)" class="btn fa fa-paperclip"> </button>
</ng-template>
<button type="button" (click)="write($event)" class="btn fa fa-chevron-right"> </button>
contenteditable.directive.ts:
import { ValidatorService } from './../../services/service/validator.service';
import { Directive, HostListener, Input, ElementRef, OnInit, SimpleChanges, EventEmitter, Output } from '#angular/core';
#Directive({selector: '[ceModel]'})
export class ContenteditableModel
{
#Input('ceModel') ceModel: any="";
#Output('ceChange') ceChange = new EventEmitter();
constructor(private elRef: ElementRef,private v:ValidatorService) {}
#HostListener('keyup', ['$event'])
onChange($event: any)
{
if ($event.keyCode==32)
{
console.log(this.ceModel);
this.ceModel="<a>"+this.ceModel+"<\a>";
this.ceChange.emit(this.ceModel);
return;
}
this.ceModel=this.elRef.nativeElement.innerText;
this.ceChange.emit(this.ceModel);
}
#HostListener('paste', ['$event'])
onPaste($event: any)
{
console.log($event);
}
}
I want to update dynamically a contenteditable div, see write.component.html, that is linked to a model that is managed by a directive,contenteditable.directive.ts. The class variable writeText is sent to the directive in order to check whether a user has written a url, if this is the case, the url content should be transformed to: url. This is rendered in the div as html:
The problem is, that whenever I type some text, the caret/cursor jumps always
to the start position:
Is it possible to manually move the cursor to end of the div content?
The content may be text and html.
I have tried the solutions proposed here with no luck:
Set the caret position always to end in contenteditable div
Thank you for your help.

in my problem, I trigger focus when enter (it auto go to new line) and refocus (to the last character of last todo) if i delete
....
// somewhere you trigger that focus event:
(div:HTMLElement) => {
// div.focus();
if (div.lastChild) this.setSelectionRange(div)
else div.focus();
this.changeDetector.detectChanges();
}
....
setSelectionRange(el:HTMLElement) {
let range = document.createRange();
let pos = el.lastChild.textContent.length;
let sel = window.getSelection();
console.log('el last child',el.lastChild.textContent.length,typeof(el.lastChild.textContent.length));
range.setStart(el.lastChild, pos);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
}
notice at "el.lastChild", your contenteditable using (textContent or innerHTML?)

I hope you're still interested in getting an answer to this. The easiest approach I can imagine is to save a reference to the <a> node and use it to place the caret.
Inside ContenteditableModel you simply can create the following:
private moveCaret(): void {
let range = document.createRange(),
pos = this.elRef.lastChild.innerText.length - 1,
sel = window.getSelection();
range.setStart(this.elRef, pos);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
}
This allows you to use your elRef to place the caret at the last char of its last child, the <a> element.

Related

How to replace innerHtml by matetrial icon

I want to change the texts by icons, what do I have to do to change the texts by icons ?
I have to find the result like this :
file.ts:
ngAfterViewInit() {
this.current.paginator = this.paginator;
const lastBtn = this.el.nativeElement.querySelector(
'.mat-paginator-navigation-last'
);
if (lastBtn) {
lastBtn.innerHTML = 'Last';
}
const firstBtn = this.el.nativeElement.querySelector(
'.mat-paginator-navigation-first'
);
if (firstBtn) {
firstBtn.innerHTML = 'First';
}
}
If you want to change the button icons, it is possible to do so.\
In order to do so, we need to alter the html that is being generated by the mat-paginator.
The following directive does it:
#Directive({
selector: '[customPaginatorIcons]'
})
export class CustomPaginatorIcons implements AfterViewInit {
constructor(
private el: ElementRef,
private renderer2: Renderer2
) {}
ngAfterViewInit(): void {
this.setFirstIcon();
this.setPreviousIcon();
this.setNextIcon();
this.setLastIcon();
}
private replaceSvgWithIcon(
btn: HTMLButtonElement,
iconName: string
): HTMLSpanElement {
this.renderer2.removeChild(btn, btn.querySelector('svg'));
const icon: HTMLSpanElement = this.renderer2.createElement('span');
this.renderer2.addClass(icon, 'material-icons');
icon.innerHTML = iconName;
this.renderer2.appendChild(btn, icon);
return icon;
}
private setFirstIcon(): void {
const btn = this.el.nativeElement.querySelector(
'.mat-mdc-paginator-navigation-first'
);
if (btn) {
this.replaceSvgWithIcon(btn, 'skip_previous');
}
}
private setPreviousIcon(): void {
const btn = this.el.nativeElement.querySelector(
'.mat-mdc-paginator-navigation-previous'
);
if (btn) {
const icon = this.replaceSvgWithIcon(btn, 'play_arrow');
this.renderer2.setStyle(icon, 'transform', 'rotate(180deg)');
}
}
private setNextIcon(): void {
const btn = this.el.nativeElement.querySelector(
'.mat-mdc-paginator-navigation-next'
);
if (btn) {
this.replaceSvgWithIcon(btn, 'play_arrow');
}
}
private setLastIcon(): void {
const btn = this.el.nativeElement.querySelector(
'.mat-mdc-paginator-navigation-last'
);
if (btn) {
this.replaceSvgWithIcon(btn, 'skip_next');
}
}
}
Now onto the why.
Directive: we create our attribute directive that will adjust the icons of the MatPaginator. Attribute Directives are recommended when we only want to edit the html of something.
AfterViewInit: we can only edit the contents of the MatPaginator html after it has been fully initialise. The AfterViewInit lifecycle hook is the best hook for the task.
ElementRef: this provides access to the HTML code that our directive is placed on.
Renderer2: the recommended utility to modify HTML elements safely. It is the basis that directives like ngStyle and ngClass use being the scenes. We can achieve the same goal by directly editing the DOM elements, however, this may raise errors if we edit it incorrectly.
setFirstIcon, setPreviousIcon, setNextIcon, setLasttIcon: these are very similar methods, they search for the button that needs to be updated and if it exists, they call the replaceSvgWithIcon method to perform the actual changes. Only exception to this is the setPreviousIcon method since there is no icon that matches what you want. To achieve the look you want, I rotate the next icon.
replaceSvgWithIcon: starts by removing the <svg>...</svg> tag from the button. This is the tag that contains the actual image for the icon, the remaining HTML in the button element is for other things like the ripple. Once the element has been removed, we create a new HTMLSpanElement. It is on this element that we will set the material-icons class (so that it uses the Material Icons), and the value of the icon. After this is done, we append it to the provided button and return it (we return the element in case we want to modify something else that is not generic).
To use this directive, we simply call on the html selector of the paginator:
<mat-paginator
...
customPaginatorIcons>
</mat-paginator>
The above case is meant for Angular 15.
For previous versions, simple remove the '-mdc' from the selectors, like so:
'.mat-mdc-paginator-navigation-first' to `'.mat-paginator-navigation-first';
'.mat-mdc-paginator-navigation-previous' to '.mat-paginator-navigation-previous';
'.mat-mdc-paginator-navigation-next' to '.mat-paginator-navigation-next';
'.mat-mdc-paginator-navigation-last' to '.mat-paginator-navigation-last';
You can also get it only with .css
In styles.css
.mat-mdc-paginator-navigation-first svg path{
d:path("M6.5 18q-.425 0-.713-.288Q5.5 17.425 5.5 17V7q0-.425.287-.713Q6.075 6 6.5 6t.713.287Q7.5 6.575 7.5 7v10q0 .425-.287.712Q6.925 18 6.5 18Zm10.45-1.025l-6.2-4.15q-.45-.3-.45-.825q0-.525.45-.825l6.2-4.15q.5-.325 1.025-.038q.525.288.525.888v8.25q0 .6-.525.9q-.525.3-1.025-.05Z")
}
.mat-mdc-paginator-navigation-previous svg path{
d:path("M7.05 16.975q-.5.35-1.025.05q-.525-.3-.525-.9v-8.25q0-.6.525-.888q.525-.287 1.025.038l6.2 4.15q.45.3.45.825q0 .525-.45.825Z")
}
.mat-mdc-paginator-navigation-next svg path{
d:path("M7.05 16.975q-.5.35-1.025.05q-.525-.3-.525-.9v-8.25q0-.6.525-.888q.525-.287 1.025.038l6.2 4.15q.45.3.45.825q0 .525-.45.825Z")
}
.mat-mdc-paginator-navigation-last svg path{
d:path("M17.5 18q-.425 0-.712-.288q-.288-.287-.288-.712V7q0-.425.288-.713Q17.075 6 17.5 6t.712.287q.288.288.288.713v10q0 .425-.288.712q-.287.288-.712.288ZM7.05 16.975q-.5.35-1.025.05q-.525-.3-.525-.9v-8.25q0-.6.525-.888q.525-.287 1.025.038l6.2 4.15q.45.3.45.825q0 .525-.45.825Z")
}
.mat-mdc-paginator-navigation-previous svg
{
transform:rotate(180deg) translateX(3px)
}
.mat-mdc-paginator-navigation-next svg
{
transform:translateX(3px)
}

Angular: ExpressionChangedAfterItHasBeenCheckedError when trying to disable button

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;

How do I check if the user has scrolled down (or crossed ) to a particular element (based on id) in Angular7?

How do I check if the user has scrolled down (or crossed ) to a particular element (based on id) in the browser so that I can check the condition and assign class name dynamically in angular 7?
Basically, you can listen to window scrolling event with Angular using HostListener with window:scroll event like this:
#HostListener('window:scroll', ['$event'])
onWindowScroll() {
// handle scrolling event here
}
Available StackBlitz Example for the explanation below
ScrolledTo directive
What I would do for maximum flexibility in this case is to create a directive to apply on any HTML element that would expose two states:
reached: true when scrolling position has reached the top of the element on which the directive is applied
passed: true when scrolling position has passed the element height on which the directive is applied
import { Directive, ElementRef, HostListener } from '#angular/core';
#Directive({
selector: '[scrolledTo]',
exportAs: 'scrolledTo', // allows directive to be targeted by a template reference variable
})
export class ScrolledToDirective {
reached = false;
passed = false;
constructor(public el: ElementRef) { }
#HostListener('window:scroll', ['$event'])
onWindowScroll() {
const elementPosition = this.el.nativeElement.offsetTop;
const elementHeight = this.el.nativeElement.clientHeight;
const scrollPosition = window.pageYOffset;
// set `true` when scrolling has reached current element
this.reached = scrollPosition >= elementPosition;
// set `true` when scrolling has passed current element height
this.passed = scrollPosition >= (elementPosition + elementHeight);
}
}
Assign CSS classes
Using a Template Reference Variable you would then be able to retrieve those states specifying the directive export #myTemplateRef="scrolledTo" in your HTML code and apply CSS classes as you wish according to the returned values.
<div scrolledTo #scrolledToElement="scrolledTo">
<!-- whatever HTML content -->
</div>
<div
[class.reached]="scrolledToElement.reached"
[class.passed]="scrolledToElement.passed">
<!-- whatever HTML content -->
</div>
That way you can assign classes on other HTML elements or on the spied element itself ... pretty much as you want, depending on your needs!
Hope it helps!
Use "IntersectionObserver" - https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API
Create a directive as given below in the example and apply to the element you want to track. When the element is visible the intersectionobserver will be triggered!
Below is an angular based example to load contents of div (an image) only when the div boundaries are visible.
https://stackblitz.com/edit/angular-intersection-observor
<span class="card" *ngFor="let card of data" (deferLoad)="card.visible = true">
<img src={{card.url}} *ngIf="card.visible"/>
</span>
import { Directive, Output, EventEmitter, ElementRef, AfterViewInit } from '#angular/core';
#Directive({
selector:"[deferLoad]"
})
export class DeferLoadDirective implements AfterViewInit {
private _intersectionObserver: IntersectionObserver;
#Output() deferLoad: EventEmitter<any> = new EventEmitter();
constructor(
private _element: ElementRef
) {};
ngAfterViewInit() {
this._intersectionObserver = new IntersectionObserver(entries => {
this.checkIntersection(entries);
});
this._intersectionObserver.observe(this._element.nativeElement as Element);
}
checkIntersection(entries: IntersectionObserverEntry[]) {
entries.forEach((entry) => {
if ((<any>entry).isIntersecting && entry.target === this._element.nativeElement) {
this.deferLoad.emit();
this._intersectionObserver.unobserve(this._element.nativeElement as Element);
this._intersectionObserver.disconnect();
}
});
}
}

Pass string to directive from html

I'm trying to add a "clipboard" directive using this example.
However I want to compute the string to copy to the clipboard.
I want to pass the result of a function which returns a string, to my directive.
Currently, it passes "getCopyDetails(supplier)" string, to the directive.
As I am trying to pass a computed string to the button (which contains the directive) in html:
<button [clipboard]="foo, getCopyDetails(supplier)" (clipboardSuccess)="onSuccess()">Copy</button>
Which causes the error.
code:
clipboard.directive.ts:
import {Directive,ElementRef,Input,Output,EventEmitter, ViewChild, AfterViewInit} from "#angular/core";
import Clipboard from "clipboard";
import { SupplierSearchComponent } from "../components/suppliersearch.component"
declare var jQuery:any;
#Directive({
selector: "[clipboard]"
})
export class ClipboardDirective implements AfterViewInit {
clipboard: Clipboard;
#Input("clipboard")
elt:ElementRef;
text: string;
#Output()
clipboardSuccess:EventEmitter<any> = new EventEmitter();
#Output()
clipboardError:EventEmitter<any> = new EventEmitter();
constructor(private eltRef:ElementRef, text) {
this.text = text
}
ngAfterViewInit() {
this.clipboard = new Clipboard(this.eltRef.nativeElement, {
target: () => {
return this.elt;
}
} as any);
this.clipboard.on("success", (e) => {
this.clipboardSuccess.emit();
});
this.clipboard.on("error", (e) => {
this.clipboardError.emit();
});
}
ngOnDestroy() {
if (this.clipboard) {
this.clipboard.destroy();
}
}
}
search.component.html:
<div class="website" *ngIf="xxx.website !== undefined"><a #foo href="{{formatUrl(xxx.website)}}" target="_blank" (click)="someclickmethod()">{{xxx.website}}</a></div>
<button [clipboard]="foo, getCopyDetails(supplier)" (clipboardSuccess)="onSuccess()">Copy</button>
snippet from search.component.ts (same component as above):
getCopyDetails(supplier: Supplier): string {
var details: string = "";
details += xxx.yyy + "\n\n";
details += xxx.yyy + "\n";
details += xxx.yyy + "\n";
details += xxx.yyy + "\n";
details += xxx.yyy + "\n";
return details;
}
Another programmer's old html code (he used "ngx-clipboard": "^5.0.8" plugin which is a wrapper around clipboard.js which I am using for the clipboard) has been commented out and I have basically tried to apply it to my directive in the html above:
<!--<button class="btn anchor" (click)="supplierCopyDetailsClick()" ngxClipboard [cbContent]="getCopyDetails(supplier)">Copy details</button>-->
How do I pass the string obtained from getCopyDetails() to my directive?
Judging from this SO answer I can do my-object='{"Code":"test"}' within the HTML and then use that in my directive to copy it. I am a little unsure what code I place in my directive to hold that object though.
You can inject using Angular DI ONLY: #Injectable() services, Parents Components or InjectionToken.
So text is incorrect.
constructor(private eltRef:ElementRef, text) {
this.text = text
}

Detect click outside Angular component

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();
}