Angular 2 Reactive form + directive validation - forms

I'm trying to wrap my head around the following problem:
I have a 'google-place-autocomplete' directive that adds the autocomplete functionality to an input field.
Now I also wanted it to be able to force a google place selection and only be 'valid' if the user has selected a place.
E.g:
#Directive({
selector: '[googlePlace][formControlName], [googlePlace][ngModel]',
providers: [{provide: NG_VALIDATORS, useExisting: GooglePlaceDirective, multi: true}]
})
export class GooglePlaceDirective implements Validator, OnChanges {
valid = false;
#Output() googlePlaceAddressChange: any = new EventEmitter();
#Input() googlePlaceAddress: any;
#Output() ngModelChange: any = new EventEmitter();
private autocomplete: any;
constructor(private googleMapService: GoogleMapsService,
private element: ElementRef,
private zone: NgZone) {
}
ngOnInit() {
let self = this;
this.googleMapService
.load()
.subscribe(
() => {
this.autocomplete = new google.maps.places.Autocomplete(this.element.nativeElement);
this.autocomplete.addListener('place_changed', function () {
self.placeChanged(this.getPlace());
});
}
);
}
private placeChanged(place) {
this.zone.run(() => {
this.googlePlaceAddress = {
address: this.element.nativeElement.value,
formattedAddress: place.formatted_address,
latitude: place.geometry.location.lat(),
longitude: place.geometry.location.lng()
};
this.valid = true;
this.googlePlaceAddressChange.emit(this.googlePlaceAddress);
this.ngModelChange.emit(this.element.nativeElement.value);
});
}
ngOnChanges(changes): void {
let googlePlaceDefined = typeof (changes.googlePlaceAddress) !== 'undefined';
let modelDefined = typeof (changes.ngModel) !== 'undefined';
if(modelDefined && !googlePlaceDefined) {
this.valid = false;
} else if(googlePlaceDefined && !modelDefined) {
this.valid = true;
}
}
validate(control: AbstractControl) {
return this.valid === false ? {'googlePlaceAddress': true} : null;
}
}
If I use this directive in an template driven form:
...
<input name="addr" type="text" [(ngModel)]="textValue" [(googlePlaceAddress)]="googleAddress" required>
<p *ngIf="addr.errors.googlePlaceAddress">Please select a proposed address</p>
...
it works fine.
Now I need to use this in an Reactive Form using FormGroup
let groups = [
new FormControl('', [Validators.required])
];
/** HTML **/
...
<input [id]="addr"
[formControlName]="address"
class="form-control"
type="text"
googlePlace
[placeholder]="question.label"
[(googlePlaceAddress)]="googleAddress">
...
However in this case the validation from the directive is never triggered.
I suppose angular2 expects it to be given through, when using Reactive Forms:
new FormControl('', [Validators.required, ???])
I must have taken a wrong path somewhere.

For future reference:
I solved my problem creating a component out of it together with a Value accessor:
#Component({
selector: 'app-google-place',
templateUrl: './google-place.component.html',
styleUrls: ['./google-place.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => GooglePlaceComponent),
multi: true
}
]
})
export class GooglePlaceComponent implements OnInit, ControlValueAccessor {
#ViewChild('inputElement') inputElement: ElementRef;
#Input() public placeholder: string = "Address";
#Input() public textValue: string = "";
private autocomplete: any;
private _place = null;
constructor(
private googleMapService: GoogleMapsService,
private zone: NgZone
) {
}
ngOnInit() {
this.googleMapService
.load()
.subscribe(
() => {
this.autocomplete = new google.maps.places.Autocomplete(this.inputElement.nativeElement);
this.autocomplete.addListener('place_changed', () => this.placeChanged());
}
);
}
placeChanged() {
this.zone.run(() => {
let place = this.autocomplete.getPlace();
this._place = {
address: this.inputElement.nativeElement.value,
formattedAddress: place.formatted_address,
latitude: place.geometry.location.lat(),
longitude: place.geometry.location.lng()
};
this.propagateChange(this._place);
});
}
onNgModelChange($event) {
if(this._place !== null) {
if(this._place.address !== $event) {
this._place = null;
this.propagateChange(this._place);
}
}
}
onBlur() {
this.propagateTouched();
}
writeValue(obj: any): void {
if(obj !== undefined) {
this._place = obj;
}
}
propagateChange = (_: any) => {};
registerOnChange(fn) {
this.propagateChange = fn;
}
propagateTouched = () => {};
registerOnTouched(fn: any): void {
this.propagateTouched = fn;
}
}
Using this I can use it in a FormGroup with the Validators.required and it will only be valid if a user has selected a google place.
EDIT
The html:
<input type="text"
(blur)="onBlur()"
#inputElement
class="form-control"
[(ngModel)]="textValue"
(ngModelChange)="onNgModelChange($event)">
The service:
import {Injectable} from '#angular/core';
import {Subject} from 'rxjs/Subject';
import {Observable} from 'rxjs/Observable';
#Injectable()
export class GoogleMapsService {
private key = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX';
private loaded = false;
private currentRequest = null;
constructor() {
}
load() {
if (this.loaded) {
return Observable.create((observer) => {
observer.next();
observer.complete();
});
}
if (this.currentRequest === null) {
//http://reactivex.io/rxjs/manual/overview.html#multicasted-observables
const source = Observable.create((observer) => {
this.loadMaps(observer);
});
const subject = new Subject();
this.currentRequest = source.multicast(subject);
this.currentRequest.connect();
}
return this.currentRequest;
}
private loadMaps(observer: any) {
const script: any = document.createElement('script');
script.src = 'https://maps.googleapis.com/maps/api/js?key=' + this.key + '&libraries=places';
if (script.readyState) { // IE, incl. IE9
script.onreadystatechange = () => {
if (script.readyState == 'loaded' || script.readyState == 'complete') {
script.onreadystatechange = null;
this.loaded = true;
observer.next();
observer.complete();
this.currentRequest = null;
}
};
} else {
script.onload = () => { // Other browsers
this.loaded = true;
observer.next();
observer.complete();
this.currentRequest = null;
};
}
script.onerror = () => {
observer.error('Unable to load');
this.currentRequest = null;
};
document.getElementsByTagName('head')[0].appendChild(script);
}
}
The 'usage':
With template ngModel
<app-google-place ([ngModel)]="place"></app-google-place>

Related

Rest web service returns null value on mobile device but works on PC in Ionic App

i am consuming Rest JSONP Web Service in an ionic App which works fine on PC but returns null value on mobile devices
My page.ts file
import {
Component,
OnInit
} from '#angular/core';
import {
AlertController,
LoadingController
} from '#ionic/angular';
import {
ActionSheetController
} from '#ionic/angular';
import {
Router
} from '#angular/router'
import {
ProApiService
} from './../../../../services/pro-api.service';
#Component({
selector: 'app-ranked-diagnosis',
templateUrl: './ranked-diagnosis.page.html',
styleUrls: ['./ranked-diagnosis.page.scss'],
})
export class RankedDiagnosisPage implements OnInit {
tabSelect: string = 'show10';
show10Data: Array < any >= [];
showAllData: Array < any >= [];
redFlagsData: Array < any >= [];
loading: any;
constructor(
private api: ProApiService,
public alertController: AlertController,
public loadingController: LoadingController,
public actionSheetController: ActionSheetController,
private router: Router
) {}
segmentChanged(event: any) {
this.tabSelect = event.detail.value;
}
async presentActionSheet(buttons) {
const actionSheet = await this.actionSheetController.create({
header: 'Sub Diagnosis',
buttons: buttons
});
await actionSheet.present();
}
async presentAlert(msg: string, header: string) {
const alert = await this.alertController.create({
header: '',
subHeader: header,
message: msg,
buttons: ['OK']
});
await alert.present();
}
async presentLoading() {
this.loading = await this.loadingController.create({
message: 'loading...',
});
return await this.loading.present();
}
ngOnInit() {
}
ionViewWillEnter() {
if (this.api.ProApiData.diagnoses_checklist.diagnoses) {
this.showAllData =
this.api.ProApiData.diagnoses_checklist.diagnoses;
for (let i = 0; i < 10; i++) {
this.show10Data.push(this.showAllData[i]);
}
this.showAllData.forEach(item => {
if (item.red_flag == 'true') {
this.redFlagsData.push(item);
}
});
console.log(this.showAllData);
} else {
console.log('error');
this.router.navigateByUrl('isabel-pro');
}
}
why_diagnosis(url: any, weightage: any) {
this.presentLoading();
this.api.why_diagnosisApi(url).subscribe(res => {
let matched_terms = res._body.why_diagnosis.matched_terms;
console.log(matched_terms);
let alertMsg = `We matched the terms: ${matched_terms}<br><hr>Degree of match between query entered and database: ${weightage}`;
this.presentAlert(alertMsg, 'Why did this diagnosis come up ?');
this.loadingController.dismiss();
}, err => {
this.loadingController.dismiss();
console.log('error');
});
}
}
in the above code i am calling why_diagnosis function which calls the function from a service file.
My service.ts file
import {
Injectable
} from '#angular/core';
import {
HttpClient,
HttpHeaders
} from '#angular/common/http';
import {
Jsonp
} from '#angular/http';
import {
Observable
} from 'rxjs';
import {
map
} from 'rxjs/operators';
import {
ConstantsService
} from './../../../services/constants.service';
#Injectable({
providedIn: 'root'
})
export class ProApiService {
apiRoot = this.root.APIroot;
diagnosisPROData: any;
drugData: any;
ProApiData: any;
drugApiData: any;
constructor(
private jsonp: Jsonp,
private http: HttpClient,
private root: ConstantsService) {}
why_diagnosisApi(url: any): Observable < any > {
let whyUrl = `${this.apiRoot}Mob_isabelPRO.php?
why_url=${url}&callback=JSONP_CALLBACK`;
return this.jsonp.request(whyUrl, 'callback')
.pipe(
map(
res => {
let why_diagnosis = res;
return why_diagnosis;
}
)
);
}
}
above code is from my service file.
this is the value i am getting in PC
this is the return on mobile
i dont know whats wrong with it. please suggest me the solution
Thanks

Why not showing liked icon when page refresh in ionic

I am making like and dislike functionality in ionic app when user like then want to show heart icon and when user dislike then want to show empty heart icon.When like the post then icon show but when refreshing page then its show icon of empty likes.Please help me how to resolve this problem in ionic....
tab1.page.html
<div>
<img src="{{getImage.image}}" (click)="tapPhotoLike(getImage.id)" (click)="toggleIcon(getImage.id)">
</div>
<p no-margin no-padding>
<button clear ion-button icon-only class="like-btn">
<!-- <ion-icon no-padding name="like_btn.icon_name" color="{{ like_btn.color }}" class="icon-space"></ion-icon> -->
<ion-icon name="heart-empty" *ngIf="!iconToggle[getImage.id]" color="{{ like_btn.color }}"></ion-icon>
<ion-icon name="heart" *ngIf="iconToggle[getImage.id]" color="{{ like_btn.color }}"></ion-icon>
</button>
</p>
tab1.page.ts
import { Component, OnInit } from '#angular/core';
import { UserService } from '../user.service';
import { User } from '../user';
import { first } from 'rxjs/operators';
import { Storage } from '#ionic/storage';
import { ToastController } from '#ionic/angular';
import { LoadingController, NavController } from '#ionic/angular';
#Component({
selector: 'app-tab1',
templateUrl: 'tab1.page.html',
styleUrls: ['tab1.page.scss']
})
export class Tab1Page implements OnInit {
num
getImages: User[] = [];
getImages2: User[] = [];
getImages3 = [];
getcounts;
countLikes
counts
unlikes
likesID
iconToggle = [];
like_btn = {
color: 'danger',
icon_name: 'heart-empty'
};
public tap: number = 0;
public status: false;
constructor(private userService: UserService,
public toastController: ToastController,
private storage: Storage,
public navCtrl: NavController,
public loadingController: LoadingController) {
}
doRefresh(event) {
this.userPost();
setTimeout(() => {
event.target.complete();
}, 2000);
}
ngOnInit() {
this.userPost();
//this.getCountOfLikes();
}
async userPost() {
const loading = await this.loadingController.create({
message: 'Please wait...',
spinner: 'crescent',
duration: 2000
});
await loading.present();
this.userService.getUserPost().pipe(first()).subscribe(getImages => {
this.getImages3 = getImages;
loading.dismiss();
});
}
likeButton() {
const detail_id = this.userService.getCurrentIdpostId();
this.storage.get('userId').then((val) => {
if (val) {
let user_id = val
this.userService.userPostLikes(user_id, detail_id).pipe(first()).subscribe(
async data => {
this.iconToggle[this.num] = true;
if (data['status'] === 'you have already liked this post') {
const toast = await this.toastController.create({
message: 'you have already liked this post before.',
duration: 2000
});
toast.present();
}
this.userPost();
}
);
}
});
}
tapPhotoLike($detail_id, num) {
this.userService.setCurrentIdpostId($detail_id);
this.tap++;
setTimeout(() => {
if (this.tap == 1) {
this.tap = 0;
//this.unlikePost();
} if (this.tap > 1) {
this.tap = 0;
}
}, 250);
}
setIconToggles() {
let x = 0;
this.getImages3.forEach(() => {
this.iconToggle[x] = false;
x++;
});
}
toggleIcon(num) {
if (this.iconToggle[num]) {
this.iconToggle[num] = false;
this.unlikePost();
} else {
this.iconToggle[num] = true;
this.likeButton();
}
}
ionViewWillEnter() {
this.userPost();
}
unlikePost() {
let detail_id = this.userService.getCurrentIdpostId();
this.storage.get('userId').then((val) => {
if (val) {
let user_id = val;
this.userService.unLikes(user_id, detail_id).subscribe(async dislikes => {
this.unlikes = dislikes;
this.iconToggle[this.num] = false;
this.userPost();
})
}
});
}
}
How manage icon , i am inserting status on like and dislike in database..In this code how to manage status please help me.

Ionic 4 Hide Toolbar on scroll

I'm trying to implement a directive to hide the Toolbar while scrolling.
I tried using this tutorial:
https://medium.com/#gregor.srdic/ionic3-hidding-header-on-footer-on-content-scroll-15ab95b05dc5
This worked with Ionic 3, but doesn't work with Ionic 4.
On the following code I get the error:
private adjustElementOnScroll(ev) {
if (ev) {
console.log(ev);
ev.domWrite(() => {
let scrollTop: number = ev.scrollTop > 0 ? ev.scrollTop : 0;
let scrolldiff: number = scrollTop - this.lastScrollPosition;
this.lastScrollPosition = scrollTop;
let newValue = this.lastValue + scrolldiff;
newValue = Math.max(0, Math.min(newValue, this.config.maxValue));
this.renderer.setStyle(this.element.nativeElement, this.config.cssProperty, `-${newValue}px`);
this.lastValue = newValue;
});
}
}
Error:
ev.domWrite is not a function
I checked, and ev in Ionic 4 is a CustomEvent, not a ScrollEvent.
Any suggestions?
Above solutions no longer works. Ionic 4 beta APIs has changed a lot in recent months.
you have to import IonContent instead of Content.
//scroll-hide.directive.ts
import { IonContent, DomController } from '#ionic/angular';
import { Directive, ElementRef, Input, Renderer2, SimpleChanges } from '#angular/core';
import the directive where you want to use it, instead of app.module.ts
for example in relevant module,
//ex: home.module.ts
import { ScrollHideDirective } from '../../directives/scroll-hide.directive';
#NgModule({
...
declarations: [...,ScrollHideDirective],
...
})
then in the ts file,
//ex: home.page.ts
import { ScrollHideConfig } from '../../directives/scroll-hide.directive';
export class HomePage implements OnInit {
...
footerScrollConfig: ScrollHideConfig = { cssProperty: 'margin-bottom', maxValue: undefined };
headerScrollConfig: ScrollHideConfig = { cssProperty: 'margin-top', maxValue: 54 };
...
}
reversed version of directive
//ex: scroll-hide.directive.ts
import { IonContent, DomController } from '#ionic/angular';
import { Directive, ElementRef, Input, Renderer2, SimpleChanges } from '#angular/core';
#Directive({
selector: '[scrollHide]'
})
export class ScrollHideDirective {
#Input('scrollHide') config: ScrollHideConfig;
#Input('scrollContent') scrollContent: IonContent;
contentHeight: number;
scrollHeight: number;
lastScrollPosition: number;
lastValue: number = 0;
constructor(private element: ElementRef, private renderer: Renderer2, private domCtrl: DomController) {
}
ngOnChanges(changes: SimpleChanges) {
if(this.scrollContent && this.config) {
this.scrollContent.scrollEvents = true;
let scrollStartFunc = async (ev) => {
const el = await this.scrollContent.getScrollElement();
this.contentHeight = el.offsetHeight;
this.scrollHeight = el.scrollHeight;
if (this.config.maxValue === undefined) {
this.config.maxValue = this.element.nativeElement.offsetHeight;
}
this.lastScrollPosition = el.scrollTop;
};
if(this.scrollContent && this.scrollContent instanceof IonContent) {
this.scrollContent.ionScrollStart.subscribe(scrollStartFunc);
this.scrollContent.ionScroll.subscribe(async (ev) => this.adjustElementOnScroll(ev));
this.scrollContent.ionScrollEnd.subscribe(async (ev) => this.adjustElementOnScroll(ev));
} else if(this.scrollContent instanceof HTMLElement) {
(this.scrollContent as HTMLElement).addEventListener('ionScrollStart', scrollStartFunc);
(this.scrollContent as HTMLElement).addEventListener('ionScroll',async (ev) => this.adjustElementOnScroll(ev));
(this.scrollContent as HTMLElement).addEventListener('ionScrollEnd',async (ev) => this.adjustElementOnScroll(ev));
}
}
}
private adjustElementOnScroll(ev) {
if (ev) {
this.domCtrl.write(async () => {
const el = await this.scrollContent.getScrollElement();
let scrollTop: number = el.scrollTop > 0 ? el.scrollTop : 0;
let scrolldiff: number = scrollTop - this.lastScrollPosition;
this.lastScrollPosition = scrollTop;
let newValue = this.lastValue + scrolldiff;
newValue = Math.max(0, Math.min(newValue, this.config.maxValue));
this.renderer.setStyle(this.element.nativeElement, this.config.cssProperty, `-${newValue}px`);
this.lastValue = newValue;
});
}
}
}
export interface ScrollHideConfig {
cssProperty: string;
maxValue: number;
}
the html ex: page.html (No Change)
<ion-header [scrollHide]="headerScrollConfig" [scrollContent]="pageContent">
...
</ion-header>
<ion-content #pageContent fullscreen>
...
</ion-content>
<ion-footer [scrollHide]="footerScrollConfig" [scrollContent]="pageContent">
...
</ion-footer>
hope it helps.
Modified directive for Ionic 4.
import { Content, DomController } from '#ionic/angular';
import { Directive, ElementRef, Input, Renderer2, SimpleChanges } from '#angular/core';
#Directive({
selector: '[scrollHide]'
})
export class ScrollHideDirective {
#Input('scrollHide') config: ScrollHideConfig;
#Input('scrollContent') scrollContent: Content;
contentHeight: number;
scrollHeight: number;
lastScrollPosition: number;
lastValue: number = 0;
constructor(private element: ElementRef, private renderer: Renderer2, private domCtrl: DomController) {
}
ngOnChanges(changes: SimpleChanges) {
if (this.scrollContent && this.config) {
this.scrollContent.ionScrollStart.subscribe(async (ev) => {
const el = await this.scrollContent.getScrollElement();
this.contentHeight = el.offsetHeight;
this.scrollHeight = el.scrollHeight;
if (this.config.maxValue === undefined) {
this.config.maxValue = this.element.nativeElement.offsetHeight;
}
this.lastScrollPosition = el.scrollTop;
});
this.scrollContent.ionScroll.subscribe((ev) => this.adjustElementOnScroll(ev));
this.scrollContent.ionScrollEnd.subscribe((ev) => this.adjustElementOnScroll(ev));
}
}
private adjustElementOnScroll(ev) {
if (ev) {
this.domCtrl.write(async () => {
const el = await this.scrollContent.getScrollElement();
let scrollTop: number = el.scrollTop > 0 ? el.scrollTop : 0;
let scrolldiff: number = scrollTop - this.lastScrollPosition;
this.lastScrollPosition = scrollTop;
let newValue = this.lastValue + scrolldiff;
newValue = Math.max(0, Math.min(newValue, this.config.maxValue));
this.renderer.setStyle(this.element.nativeElement, this.config.cssProperty, `-${newValue}px`);
this.lastValue = newValue;
});
}
}
}
export interface ScrollHideConfig {
cssProperty: string;
maxValue: number;
}
The event may have changed in Ionic 4, but you can still import the DomController from #ionic/angular
import {..., DomController } from "#ionic/angular";
and inject it in the constructor
constructor(
// ...
private domCtrl: DomController
) { }
And then use the write() method like this:
private adjustElementOnScroll(ev) {
if (ev) {
this.domCtrl.write(() => {
// ...
});
}
}
The DomController is just a way for Ionic to create a queue for callbacks that would write or read the DOM in order to use the window.requestAnimationFrame() method behind the scenes.
For more information, please visit:
DomController source code
requestAnimationFrame MDN
The below code worked for me, with minor corrections from above code
import { Content, DomController } from '#ionic/angular';
import { Directive, ElementRef, Input, Renderer2, SimpleChanges } from '#angular/core';
#Directive({
selector: '[scrollHide]'
})
export class ScrollHideDirective {
#Input('scrollHide') config: ScrollHideConfig;
#Input('scrollContent') scrollContent: Content;
contentHeight: number;
scrollHeight: number;
lastScrollPosition: number;
lastValue: number = 0;
constructor(private element: ElementRef, private renderer: Renderer2, private domCtrl: DomController) {
}
ngOnChanges(changes: SimpleChanges) {
if(this.scrollContent && this.config) {
this.scrollContent.scrollEvents = true;
let scrollStartFunc = async (ev) => {
const el = await this.scrollContent.getScrollElement();
this.contentHeight = el.offsetHeight;
this.scrollHeight = el.scrollHeight;
if (this.config.maxValue === undefined) {
this.config.maxValue = this.element.nativeElement.offsetHeight;
}
this.lastScrollPosition = el.scrollTop;
};
if(this.scrollContent && this.scrollContent instanceof Content) {
this.scrollContent.ionScrollStart.subscribe(scrollStartFunc);
this.scrollContent.ionScroll.subscribe(async (ev) => this.adjustElementOnScroll(ev));
this.scrollContent.ionScrollEnd.subscribe(async (ev) => this.adjustElementOnScroll(ev));
} else if(this.scrollContent instanceof HTMLElement) {
(this.scrollContent as HTMLElement).addEventListener('ionScrollStart', scrollStartFunc);
(this.scrollContent as HTMLElement).addEventListener('ionScroll',async (ev) => this.adjustElementOnScroll(ev));
(this.scrollContent as HTMLElement).addEventListener('ionScrollEnd',async (ev) => this.adjustElementOnScroll(ev));
}
}
}
private adjustElementOnScroll(ev) {
if (ev) {
this.domCtrl.write(async () => {
const el = await this.scrollContent.getScrollElement();
let scrollTop: number = el.scrollTop > 0 ? el.scrollTop : 0;
let scrolldiff: number = scrollTop - this.lastScrollPosition;
this.lastScrollPosition = scrollTop;
let newValue = this.lastValue + scrolldiff;
newValue = Math.max(0, Math.min(newValue, this.config.maxValue));
this.renderer.setStyle(this.element.nativeElement, this.config.cssProperty, `-${newValue}px`);
this.lastValue = newValue;
});
}
}
}
export interface ScrollHideConfig {
cssProperty: string;
maxValue: number;
}

How to mask value in custom control

How to show transformed value in input (e.g. "1(234)567-890"), and have not transformed value ('1234567890')?
Is it possible to make separated values for maskInputEl and maskInput?
I have template:
<input #maskInputEl class="spacer" [type]="type"
[formControl]="maskInput"/>
And custom component:
export class MaskInputComponent implements ControlValueAccessor, OnInit, OnDestroy {
#ViewChild('maskInputEl') public maskInputEl: ElementRef;
#Input() public mask: any[];
public maskInput = new FormControl();
private _oldValue: string = '';
public ngOnInit(): void {
this.maskInput.valueChanges
.subscribe((value: string) => {
let valid = this.isValidValueByMask(value, this.mask);
if (valid) {
this._oldValue = value;
} else {
value = this._oldValue;
}
this._onChangeCallback(value);
this.onChange.emit(value);
this.maskInputEl.nativeElement.value = value;
},
(err) => console.warn(err)
);
}
public toggleActive(value) {
//
}
public registerOnChange(fn: any): void {
this._onChangeCallback = fn;
}
public registerOnTouched(fn: any): void {
this._onTouchedCallback = fn;
}
public _onChangeCallback: Function = (_: any) => {
//
}
public _onTouchedCallback: Function = (_: any) => {
//
}
public makeActive() {
this.maskInputEl.nativeElement.focus();
}
public writeValue(value: string): void {
this.maskInput.setValue(value);
}
public ngOnDestroy(): void {
//
}
private isValidValueByMask(value: string, mask: RegExp[]): boolean {
//
}
}
Yes, it's possible. I did something similar for my own project where I wanted to create a MoneyFieldComponent that returned a value in cents, but allowed the user to type their money value in dollars and cents.
The basic concept is that your component has to store the raw value, but, you display the formatted value in your text field. In addition, as the user interacts with your text field, you update your 'inner value' of your component with the raw value.
Note that you shouldn't use ngModel to update your text field - ngModel has some async behaviour that plays havoc in these scenarios - you can accomplish the same using raw javascript (or in my case, i used a FormControl).
Sample:
#Component({
selector: 'ec-money-field',
template: `
<md-input-container *ngIf="editMode">
<input #input mdInput class="value" type="text"
(input)="updateInnerValue(input.value)"
(blur)="formatTextValue()"
[formControl]="control" />
</md-input-container>
`,
providers: [
{provide: NG_VALUE_ACCESSOR, multi: true, useExisting: forwardRef(() => MoneyFieldComponent)},
]
})
export class MoneyFieldComponent implements OnInit, ControlValueAccessor {
private valueInCents = 0;
control = new FormControl(0);
private onChange: Function = (_: any) => {};
private onTouch: Function = (_: any) => {};
constructor() { }
#Input()
get value(): number {
return this.valueInCents;
};
// if you update the component by using the value property,
// propagate that change to the text field
set value(newValueInCents: number) {
this.valueInCents = newValueInCents;
this.control.setValue(centsToDollars(newValueInCents));
}
ngOnInit() {
}
// convert the masked value - i.e. what the user types
// into the actual numerical value that will be stored
// You'll have to provide your own conversion function
// to convert the user typing 1(855) 555 1234 to 1865551234
updateInnerValue(dollarValueString: string) {
this.valueInCents = dollarsToCents(dollarValueString);
this.onChange(this.valueInCents);
}
formatTextValue() {
this.value = this.value;
}
writeValue(newValue: number): void {
this.value = newValue;
}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouch = fn;
}
}
Note that above is a simplified version of the component. The full version can be found on Github.

Angular2 - access the AbstractControl instance in validation directive

I have to trigger validation from the inside of a validator directive.
Here is the directive I have. It works as expected. However I want it to trigger the validation process when the validator function changes. I.e. when its input variable maxDate changes.
How could I do this ?
If I could access the AbstractControl instance in the constructor I could easily do this. I can't think of a way to do it, however.
import { AbstractControl, FormGroup, ValidatorFn, Validator, NG_VALIDATORS, Validators } from '#angular/forms';
import { Directive, Input, OnChanges, SimpleChanges, EventEmitter } from '#angular/core';
function parseDate(date: string):any {
var pattern = /(\d{2})-(\d{2})-(\d{4})/;
if (date) {
var replaced = date.search(pattern) >= 0;
return replaced ? new Date(date.replace(pattern,'$3-$1-$2')) : null;
}
return date;
}
export function maxDateValidator(maxDateObj): ValidatorFn {
return (control:AbstractControl): {[key: string]: any} => {
const val = control.value;
let date = parseDate(val);
let maxDate = parseDate(maxDateObj.max);
if (date && maxDate && date > maxDate) {
return {
maxDateExceeded: true
};
}
return null;
};
}
...
#Directive({
selector: '[maxDate]',
providers: [{provide: NG_VALIDATORS, useExisting: maxDateDirective, multi: true}]
})
export class maxDateDirective implements Validator, OnChanges {
#Input() maxDate: string;
private valFn = Validators.nullValidator;
constructor() { }
ngOnChanges(changes: SimpleChanges): void {
const change = changes['maxDate'];
if (change) {
const val: string = change.currentValue;
this.valFn = maxDateValidator(val);
}
else {
this.valFn = Validators.nullValidator;
}
//This is where I want to trigger the validation again.
}
validate(control): {[key: string]: any} {
return this.valFn(control);
}
}
Usage:
<input type="text" [(ngModel)]="deathDateVal">
<input class="form-control"
type="text"
tabindex="1"
[maxDate]="deathDateVal"
name="will_date"
[textMask]="{pipe: datePipe, mask: dateMask, keepCharPositions: true}"
ngModel
#willDate="ngModel">
Here is what I've just come up with:
#Directive({
selector: '[maxDate]',
providers: [{provide: NG_VALIDATORS, useExisting: maxDateDirective, multi: true}]
})
export class maxDateDirective implements Validator, OnChanges {
#Input() maxDate: string;
private valFn = Validators.nullValidator;
private control:AbstractControl;
constructor() { }
ngOnChanges(changes: SimpleChanges): void {
const change = changes['maxDate'];
if (change) {
const val: string = change.currentValue;
this.valFn = maxDateValidator(val);
}
else {
this.valFn = Validators.nullValidator;
}
if (this.control) {
this.control.updateValueAndValidity(this.control);
}
}
validate(_control:AbstractControl): {[key: string]: any} {
this.control = _control;
return this.valFn(_control);
}
}
It works. Validate is called on initialization so I just store its parameter.
It is fuckin' ugly but it works.
To get your hands on the abstractControl of the input you can do something like this:
#Directive({
// tslint:disable-next-line:directive-selector
selector: 'input[type=date][maxDate]'
})
export class InputFullWithDirective implements Validator, OnChanges {
constructor(#Self() private control: NgControl) {}
/** the rest is mostly unchanged from the question */
}