How to handle input events on custom input component in Angular? - html

I'm trying to create a custom Input component using Reative Form in Angular 7 but i'm having trouble passing events to the input field.
My component has a label and an input field and a specific layout. My component html looks like this:
<div id="customInputGroup" [formGroup]="parentFormGroup" [ngClass]="{'input-value': hasValue()}">
<label [for]="inputId">{{label}}</label>
<input [id]="inputId" [name]="inputName" class="form-control" [placeholder]="placeholder" [formControlName]="inputName" [mask]="mask">
</div>
And in my Component:
#Component({
selector: 'app-input-field',
templateUrl: './input-field.component.html',
styleUrls: ['./input-field.component.css']
})
export class InputFieldComponent implements OnInit {
#Input('placeholder') placeholder = '' ;
#Input('label') label = '';
#Input('inputId') inputId = '';
#Input('inputName') inputName = '';
#Input('parentFormGroup') parentFormGroup:FormGroup;
#Input('mask') mask;
constructor() { }
ngOnInit() {
}
hasValue(){
return (this.parentFormGroup.get(this.inputName).value != undefined
&& this.parentFormGroup.get(this.inputName).value != null
&& this.parentFormGroup.get(this.inputName).value != '')
}
Everything was working fine till i had to handle an input onBlur event(or it could be any other input event).
The result i want is to call my component passing any event like this:
<app-input-field (blur)="functionName()"></app-input-field>
or
<app-input-field (keyup)="functionName()"></app-input-field>
And my component will be able to pass these events to my input field 'dynamically'. Is it possible to do it?

Events like blur works on input field , not selectors like <app-input-field>
You can emit event for all events like blur, keyup, mouseover etc..
InputFieldComponent:
HTML:
<input (blur)="functionName('blur')" (keyup)="functionName('keyUp')" [id]="inputId" [name]="inputName" class="form-control" [placeholder]="placeholder" [formControlName]="inputName" [mask]="mask">
TS:
#Output() OnInputEvent= new EventEmitter();
functionName(eventName) {
this.OnInputEvent.emit(eventName);
}
In your component:
<app-input-field (OnInputEvent)="functionName($event)"></app-input-field>
TS:
functionName(event) {
switch (event) {
case "blur":
...
break;
case "keyUp":
...
break;
}
}
Working Demo

for blur event you need to create a eventemitter with the same name and emit this event on input elemnt blur emit
export class InputFieldComponent implements OnInit {
#Output() blur:EventEmitter<any> = new EventEmitter(); // 👈
constructor() { }
ngOnInit() {
}
}
template
<input type="text" (blur)="blur.emit($event)" >
app.template
<app-input-field (blur)="onBlur($event)" (keyup)="onKeyup($event)"></app-input-field>
we did this for the blur event because the event is not bubble where the keyup will work without create any custom event because it bubble.
demo 🔥🔥
you can implement two way data binding like this
#Input() value:EventEmitter<any> = new EventEmitter();
#Output() valueChange:EventEmitter<any> = new EventEmitter();
template
<app-input-field [(value)]="name" ></app-input-field>
demo 🌟🌟
any bubble event like input,keypress,keyup,keydown you can capture on the element itself or you can work around it like what we did on blur event.

Related

ngx-intl-tel-input can't able to give custom tabindex

I have used ngx-intl-tel-input as a separate component and tried to
give custom tabindex but it's not taking to country box field and
input box field actually how can i make it possible?
<ngx-intl-tel-input *ngIf="!tooltip && type == 'phone' && !showPasswordIcon" [inputId]="fcn ? fcn : 'mobileNumber'" cssClass="form-control input-md" class="phone-box" [enablePlaceholder]="enablePlaceholder" [selectedCountryISO]="CountryISO.Australia" [maxLength]="15" [separateDialCode]="true" [formControl]="form.controls[fcn]" [phoneValidation]="true" container="body" (paste)="onlyNumbers($event)" (drop)="onlyNumbers($event)" triggers="manual" #inputContainer customPlaceholder="Mobile number"> </ngx-intl-tel-input>
You may consider creating a directive that can help you to add tabIndex on desired input element.
#Directive({
selector: '[addTabIndex]',
})
export class AddTabIndexDirective {
#Input() addTabIndex;
#Input() targetElement = 'input';
constructor(private el: ElementRef, private render: Renderer2) {}
ngAfterViewInit() {
const input = this.el.nativeElement.querySelector(this.targetElement);
if (input && this.addTabIndex) {
this.render.setAttribute(input, 'tabindex', this.addTabIndex);
}
}
}
Usage
<ngx-intl-tel-input tabIndex="2"
Stackblitz

Angular material Modal dialog not able to pass event to parent?

I have a component which has one child component. Child component has a button which will open a material dialog Box.
In dialog we have form, username and passwrod and submit button. When I submit i am calling backend REST api.
this is getting called in child component:
dialogRef.afterClosed().subscribe(result => {
console.log("result", result);
this.onModalClosePassData.emit(result);//emit to parent
});
which is sending event to parent. updateComment() is getting called and I can see the data in console.
But when I fill the form and click on submit. It calls submitForm method which is asynchronus call and I am closing dialog after successful login.But then event is not emmiting. updateComment() is not getting called.
See the full code:
parent component.html
<ng-template #showTextOnly>
<child-component [branch]="releaseBranch" [date]="dateString"
(onModalClosePassData)="updateComment($event)">
</child-component>
</ng-template>
parent component.ts
//This method is getting called when i click on backDrop,
but If i logged in successfully this is not getting called
updateComment(event:any){
consile.log(event);
}
child-component.html
<button class="btn btn-default" (click)="openDialog()">Login</button>
child-component.ts
export class ChildComponent implements OnInit {
#Output() onModalClosePassData = new EventEmitter();
constructor(public dialog: MatDialog) { }
openDialog(): void {
const dialogConfig = new MatDialogConfig();
dialogConfig.disableClose = false;
dialogConfig.autoFocus = false;
dialogConfig.hasBackdrop= true;
dialogConfig.width = '300px';
dialogConfig.autoFocus=true;
dialogConfig.data = {branch: this.branch, date: this.date};
const dialogRef = this.dialog.open(LoginDialog, dialogConfig);
dialogRef.afterClosed().subscribe(result => {
console.log("result", result); //result is getting called in both cases
this.onModalClosePassData.emit(result);
});
}
}
LoginDialog.component.ts
import {MatDialogRef, MAT_DIALOG_DATA} from '#angular/material/dialog';
export class LoginDialog implements OnInit{
constructor(private loginService: LoginService, public dialogRef: MatDialogRef<LoginDialog>,
#Inject(MAT_DIALOG_DATA) public data: any) {}
public submitForm = (formValue: any) => {
if (this.noteForm.valid) {
let encryptData = btoa(`${formValue.username}:${formValue.password}`);
this.loginService.login(encryptData)
.subscribe((response:any)=>{
if(response.STATUS === "FAILED"){
} else {
this.dialogRef.close(this.noteDetail);
}
})
}
}
}
LoginDialog.component.html
<form [formGroup]="noteForm" autocomplete="off" novalidate (ngSubmit)="submitForm(noteForm.value)">
<mat-dialog-content class="mat-typography">
<mat-form-field>
<mat-label>User Name</mat-label>
<input matInput type="text" formControlName="username" id="username">
</mat-form-field>
<mat-form-field>
<mat-label>Password</mat-label>
<input matInput type="password" formControlName="password">
</mat-form-field>
</mat-dialog-content>
<mat-dialog-actions align="center">
<button mat-raised-button color="primary" [disabled]="!noteForm.valid">Submit</button>
</mat-dialog-actions>
</form>
I have faced same issue and figured it out, may be this is usefull for others.
We will get this issue when we used custom component on modal. For example, we have formComponent on pop up modal and on submit we need to close the modal and emit the form value, this should work but we can face the issue when our formComponent is destroyed before emitting the value.
This is because we opened our formComponent on Modal later on form submit we closed the modal which contains formComponent and opened success modal then trying to emit the value.
Solution is: Don't close modal which contains formComponent before emmiting the value or else use a service to trigger.

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

focus for ng-select not working Angular 5

i'm trying to use focus for ng-select in angular 5 but its not working
i'm working with ng2-select
How to use focus for ng-select in Angular 5 ?
<label class="input">
<input (blur)="goTo()" type="text">
</label>
<label class="select" >
<div *ngIf="items.length > 0" >
<ng-select #select [items]="items">
</ng-select>
</div>
</label>
#ViewChild('select') myElement: ElementRef;
goTo(){
this.myElement.element.nativeElement.focus();
}
this works fine for me,
#ViewChild('ngSelect') ngSelect: NgSelectComponent;
ngAfterViewInit() {
if (this.autofocus) {
setTimeout(() => {
this.ngSelect.focus();
});
}
}
refer https://github.com/ng-select/ng-select/issues/762
Change this
goTo(){
this.myElement.element.nativeElement.focus();
}
to this,
import { ChangeDetectorRef } from '#angular/core';
constructor (private cRef: ChangeDetectorRef) {}
// import 'SelectComponent' from your library for 'ng2-select'
goTo(){
let el = null;
if (this.items.length > 0) {
this.cRef.detectChanges();
el = (this.myElement.nativeElement as SelectComponent).element.nativeElement.querySelector('div.ui-select-container > input');
if (el) { el.focus(); }
}
}
You may have to check if the element is defined or not (or if you need an extra 'nativeElement' in there, but I'm basically reusing the logic in here.
Just a cautionary note, this may not be a stable fix though if the library updates to rename these classes or modify the template.
Hope it helps.
A quick reply, in this solution,each select elements are recorded for future usage (and in this case , blur). This needs being adapted to your situation.
import { Component, OnInit } from '#angular/core';
import { ViewChildren, QueryList } from '#angular/core';
#Component({
selector: 'app-editor',
templateUrl: './editor.component.html',
styleUrls: ['./editor.component.css']
})
export class EditorComponent implements AfterViewInit {
// Viewchildren
// https://netbasal.com/understanding-viewchildren-contentchildren-and-querylist-in-angular-896b0c689f6e
myindex:number;
#ViewChildren("select") inputs: QueryList<any>
constructor() {
this.myindex=0
}
// You will have to setup some input parameters in this function in order to switch to the right "select", (you have control)
private goTo()
{
// focus next input field
if (this.myindex +1 < this.inputs.toArray().length)
{
this.inputs.toArray()[this.myindex +1].nativeElement.focus();
this.myindex = this.myindex + 1;
}
else {
this.inputs.toArray()[0].nativeElement.focus();
this.myindex = 0;
}
}
private processChildren(): void {
console.log('Processing children. Their count:', this.inputs.toArray().length)
}
ngAfterViewInit() {
console.log("AfterViewInit");
console.log(this.inputs);
this.inputs.forEach(input => console.log(input));
// susbcribe to inputs changes, each time an element is added (very optional feature ...)
this.inputs.changes.subscribe(_ =>
this.processChildren() // subsequent calls to processChildren
);
}
}
A Simple way, you can do this also.
<label class="input">
<input (blur)="goTo(select)" type="text">
</label>
<label class="select">
<div *ngIf="items.length > 0">
<ng-select #select [items]="items">
</ng-select>
</div>
</label>
And in Typescript File.
goTo(select){
select.focus();
}

Binding ngModel to complex data

For some time I have been researching if, and how to bind complex model to ngModel. There are articles showing how it can be done for simple data (e.g. string) such as this. But what I want to do is more complex. Let's say that I have a class:
export class MyCoordinates {
longitude: number;
latitude: number;
}
Now I am going to use it in multiple places around the application, so I want to encapsulate it into a component:
<coordinates-form></coordinates-form>
I would also like to pass this model to the component using ngModel to take advantage of things like angular forms but was unsuccessful thus far. Here is an example:
<form #myForm="ngForm" (ngSubmit)="onSubmit()">
<div class="form-group">
<label for="name">Name</label>
<input type="text" [(ngModel)]="model.name" name="name">
</div>
<div class="form-group">
<label for="coordinates">Coordinates</label>
<coordinates-form [(ngModel)]="model.coordinates" name="coordinates"></coordinates-form>
</div>
<button type="submit" class="btn btn-success">Submit</button>
</form>
Is actually possible to do it this way or is my approach simply wrong? For now I have settled on using component with normal input and emitting event on change but I feel like it will get messy pretty fast.
import {
Component,
Optional,
Inject,
Input,
ViewChild,
} from '#angular/core';
import {
NgModel,
NG_VALUE_ACCESSOR,
} from '#angular/forms';
import { ValueAccessorBase } from '../Base/value-accessor';
import { MyCoordinates } from "app/Models/Coordinates";
#Component({
selector: 'coordinates-form',
template: `
<div>
<label>longitude</label>
<input
type="number"
[(ngModel)]="value.longitude"
/>
<label>latitude</label>
<input
type="number"
[(ngModel)]="value.latitude"
/>
</div>
`,
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: CoordinatesFormComponent,
multi: true,
}],
})
export class CoordinatesFormComponent extends ValueAccessorBase<MyCoordinates> {
#ViewChild(NgModel) model: NgModel;
constructor() {
super();
}
}
ValueAccessorBase:
import {ControlValueAccessor} from '#angular/forms';
export abstract class ValueAccessorBase<T> implements ControlValueAccessor {
private innerValue: T;
private changed = new Array<(value: T) => void>();
private touched = new Array<() => void>();
get value(): T {
return this.innerValue;
}
set value(value: T) {
if (this.innerValue !== value) {
this.innerValue = value;
this.changed.forEach(f => f(value));
}
}
writeValue(value: T) {
this.innerValue = value;
}
registerOnChange(fn: (value: T) => void) {
this.changed.push(fn);
}
registerOnTouched(fn: () => void) {
this.touched.push(fn);
}
touch() {
this.touched.forEach(f => f());
}
}
Usage:
<form #form="ngForm" (ngSubmit)="onSubmit(form.value)">
<coordinates-form
required
hexadecimal
name="coordinatesModel"
[(ngModel)]="coordinatesModel">
</coordinates-form>
<button type="Submit">Submit</button>
</form>
The error I am getting Cannot read property 'longitude' of undefined. For simple model, like string or number it works without a problem.
The value property is undefined at first.
To solve this issue you need to change your binding like:
[ngModel]="value?.longitude" (ngModelChange)="value.longitude = $event"
and change it for latitude as well
[ngModel]="value?.latitude" (ngModelChange)="value.latitude = $event"
Update
Just noticed you're running onChange event within settor so you need to change reference:
[ngModel]="value?.longitude" (ngModelChange)="handleInput('longitude', $event)"
[ngModel]="value?.latitude" (ngModelChange)="handleInput('latitude', $event)"
handleInput(prop, value) {
this.value[prop] = value;
this.value = { ...this.value };
}
Updated Plunker
Plunker Example with google map
Update 2
When you deal with custom form control you need to implement this interface:
export interface ControlValueAccessor {
/**
* Write a new value to the element.
*/
writeValue(obj: any): void;
/**
* Set the function to be called when the control receives a change event.
*/
registerOnChange(fn: any): void;
/**
* Set the function to be called when the control receives a touch event.
*/
registerOnTouched(fn: any): void;
/**
* This function is called when the control status changes to or from "DISABLED".
* Depending on the value, it will enable or disable the appropriate DOM element.
*
* #param isDisabled
*/
setDisabledState?(isDisabled: boolean): void;
}
Here is a minimal implementation:
export abstract class ValueAccessorBase<T> implements ControlValueAccessor {
// view => control
onChange = (value: T) => {};
onTouched = () => {};
writeValue(value: T) {
// control -> view
}
registerOnChange(fn: (_: any) => void): void {
this.onChange = fn;
}
registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
}
It will work for any type of value. Just implement it for your case.
You do not need here array like (see update Plunker)
private changed = new Array<(value: T) => void>();
When component gets new value it will run writeValue where you need to update some value that will be used in your custom template. In your example you are updating value property which is used together with ngModel in template.
In my example i am drawing new marker.
DefaultValueAccessor just updates value property https://github.com/angular/angular/blob/4.2.0-rc.0/packages/forms/src/directives/default_value_accessor.ts#L76
Datepicker in angular2 material is setting inner value as you do https://github.com/angular/material2/blob/123d7eced4b4f808fc03c945504d68280752d533/src/lib/datepicker/datepicker-input.ts#L202
When you need to propagate changes to AbstractControl you have to call onChange method which you registered in registerOnChange.
I wrote this.value = { ...this.value }; because it is just
this.value = Object.assign({}, this.value)
it will call setter where you call onChange method
Another way is calling onChange directly that is usually used
this.onChange(this.value);
Your example https://plnkr.co/edit/Q11HXhWKrndrA8Tjr6KH?p=preview
DefaultValueAccessor https://github.com/angular/angular/blob/4.2.0-rc.0/packages/forms/src/directives/default_value_accessor.ts#L88
Material2 https://github.com/angular/material2/blob/123d7eced4b4f808fc03c945504d68280752d533/src/lib/datepicker/datepicker-input.ts#L173
You can do anything you like inside custom component. It can have any template and any nested components. But you have to implement logic for ControlValueAccessor to do it working with angular form.
If you open some library such angular2 material or primeng you can find a lot of example how to implement such controls