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.
Related
It works but i anot getting the results it should sort. I am getting the same results regardless what i type in the searchbar
I want it to sort like autocomplete. to show results of what i type in the search bar
search.ts
#Component({ selector: "page-search", templateUrl: "search.html" })
export class SearchPage {
filter: string = '';
public userDetails: any;
public resposeData: any;
public dataSet: any;
public userSet: any;
public mediaSet: any;
public noRecords: boolean;
userPostData = {
uid: "",
token: "",
username: "",
bio: ""
};
constructor(
public common: Common,
public navCtrl: NavController,
public app: App,
public menu: MenuController,
public authService: AuthService,
public http: Http,
platform: Platform,
statusBar: StatusBar,
splashScreen: SplashScreen
) {
this.initializeItems();
this.mostmediaList();
}
initializeItems() {
return this.userPostData;
}
getItems(ev: any) {
this.initializeItems();
let val = ev.target.value;
if (val && val.trim() != '') {
this.authService.postData(this.userPostData, "userGroupSearch").then(
result => {
this.resposeData = result;
if (this.resposeData.allArtistsData) {
this.userSet = this.resposeData.allArtistsData;
console.log(this.userSet);
} else {
console.log("No access");
}
},
);
}
}
Since your code is wrapped into
if (this.resposeData.items) {
//some code
}
we know for sure that this.resposeData is not an array, since it has an items member (otherwise your code inside the if would not be executed and hence you would not get an error as in the case we have).
Since you call the parameter items at
this.userSet = this.resposeData.filter((items) => {
//some code
};
it is safe to assume that you wanted to filter this.resposeData.items instead of this.resposeData. So, you will need to make sure it is an array at the if
if (this.resposeData.items && Array.isArray(this.resposeData.items)) {
//some code
}
and filter this.resposeData.items instead of this.resposeData:
this.userSet = this.resposeData.items.filter((items) => {
//some code
};
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 */
}
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>
I have a component with an input-element, i use defaultValue to set the initial value.
I want to focus that element and select the whole value initially, but it seems that the defaultvalue is not set when componentDidMount is called.
Do you have any tips?
i use window.setTimeout but i want to avoid that in my react-components:
public componentDidMount(): void {
if (this.props.focus) {
let tInput: HTMLInputElement = ReactDOM.findDOMNode(this).getElementsByTagName("input").item(0);
if (tInput) {
tInput.focus();
// FixMe: defaultValue is set too late by react so i cant set selection instantly
if (this.props.defaultValue) {
window.setTimeout(
() => {tInput.setSelectionRange(0, this.props.defaultValue.length); },
100
);
}
}
}
}
Works fine for me:
class MyComponent extends React.Component {
componentDidMount () {
const { myInput } = this.refs
myInput.focus()
myInput.select()
}
render () {
return (
<input type='text' defaultValue='Foobar' ref='myInput' />
)
}
}
I wouldn't use any reactDOM methods other than render/renderToString. These apis are "escape hatches" and usage is not recommended.
I want to modify the class so that it does not use the ApplicationRef. In other words how to get hold of main app not using app ref.
#Injectable()
export class ToastsManager {
container: ComponentRef<any>;
private options = {
autoDismiss: true,
toastLife: 1000
};
private index = 0;
container: ComponentRef<any>;
private options = {
autoDismiss: true,
toastLife: 1000
};
private index = 0;
constructor(private resolver: ComponentResolver,
private appRef: ApplicationRef,
#Optional() #Inject(ToastOptions) options) {
if (options) {
Object.assign(this.options, options);
}
}
show(toast: Toast) {
if (!this.container) {
// a hack to get app element in shadow dom
let appElement: ViewContainerRef = new ViewContainerRef_(this.appRef['_rootComponents'][0]._hostElement);
this.resolver.resolveComponent(ToastContainer)
.then((factory: ComponentFactory<any>) => {
this.container = appElement.createComponent(factory);
this.setupToast(toast);
});
} else {
this.setupToast(toast);
}
}
I try with the #ViewChild but it does not work.
You could do with ApplicationRef what Brandon Roberts demonstrates in https://github.com/angular/angular/issues/4112#issuecomment-139381970 to get a reference to the Router in CanActivate().
Probably better would be a shared service
#Injectable()
export class Shared {
appRef = new BehaviorSubject();
setAppRef(appRef:ApplicationRef) {
this.appRef.emit(appRef);
}
}
export class ToastsManager {
constructor(private resolver: ComponentResolver,
private appRef: ApplicationRef,
shared:Shared,
#Optional() #Inject(ToastOptions) options) {
shared.setAppRef(appRef);
}
}
export class OtherClassThatNeedsAppRef {
constructor(shared:Shared) {
shared.appRef.subscribe(appRef => this.appRef = appRef);
}
}