I am working on an Angular project and I have an object of type Project that contains the following values:
cost: 56896 and
costHR: 27829
I want to modify the object using a form and bind the fields with ngModel to the attributes.
But the problem I am facing is that in the field, I want to display the number in a currency format (e.g. 56,896€) which is not compatible with the variable type which is float.
Can someone help me with a solution to this problem? I have tried using built-in Angular pipes and custom formatter and parser functions, but none of them seem to work as expected.
Any help would be greatly appreciated.
import { Component, OnInit } from '#angular/core';
import { CurrencyPipe } from '#angular/common';
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
providers: [CurrencyPipe]
})
export class AppComponent implements OnInit {
project = {
cost: 56896,
costRH: 27829
}
constructor(private currencyPipe: CurrencyPipe) { }
ngOnInit() {
this.project.cost = this.currencyPipe.transform(this.project.cost, 'EUR', 'symbol', '1.0-0');
this.project.costRH = this.currencyPipe.transform(this.project.costRH, 'EUR', 'symbol', '1.0-0');
}
onBlur(event, projectProperty) {
this.project[projectProperty] = this.currencyPipe.parse(event.target.value);
}
}
<form>
<label for="cost">Cost</label>
<input [(ngModel)]="project.cost" (blur)="onBlur($event, 'cost')" [ngModelOptions]="{updateOn: 'blur'}" [value]="project.cost | currency:'EUR':'symbol':'1.0-0'">
<label for="costRH">Cost RH</label>
<input [(ngModel)]="project.costRH" (blur)="onBlur($event, 'costRH')" [ngModelOptions]="{updateOn: 'blur'}" [value]="project.costRH | currency:'EUR':'symbol':'1.0-0'">
</form>
What I expected:
I expected the input fields to be formatted as currency (e.g. 56,896€) and for the corresponding properties in the 'projet' object (cost and costRH) to be updated with the parsed value when the input loses focus.
What happened instead:
The value displayed in the input fields is not formatted as currency and the corresponding properties in the object are not being updated as expected.
you can use a directive like
#Directive({
selector: '[format]',
})
export class FormatDirective implements OnInit {
format = 'N0';
digitsInfo = '1.0-0';
#Input() currency = '$';
#Input() sufix = '';
#Input() decimalCharacter = null;
#Input('format') set _(value: string) {
this.format = value;
if (this.format == 'N2') this.digitsInfo = '1.2-2';
const parts = value.split(':');
if (parts.length > 1) {
this.format = value[0];
this.digitsInfo = parts[1];
}
}
#HostListener('blur', ['$event.target']) blur(target: any) {
target.value = this.parse(target.value);
}
#HostListener('focus', ['$event.target']) focus(target: any) {
target.value = this.control.value;
}
ngOnInit() {
setTimeout(() => {
this.el.nativeElement.value = this.parse(this.el.nativeElement.value);
});
}
constructor(
#Inject(LOCALE_ID) private locale: string,
private el: ElementRef,
private control: NgControl
) {}
parse(value: any) {
let newValue = value;
if (this.format == 'C2')
newValue = formatCurrency(value, this.locale, this.currency);
if (this.format == 'N2')
newValue = formatNumber(value, this.locale, this.digitsInfo);
if (this.format == 'N0')
newValue = formatNumber(value, this.locale, this.digitsInfo);
if (this.format == 'NX')
newValue = formatNumber(value, this.locale, this.digitsInfo);
if (this.decimalCharacter)
return (
newValue.replace('.', 'x').replace(',', '.').replace('x', ',') +
this.sufix
);
return newValue + this.sufix;
}
}
Works like
<input format="N0" [(ngModel)]="value"/>
<input format="N2" [(ngModel)]="value"/>
<input format="C2" [(ngModel)]="value"/>
<input format="N2" decimalCharacter='.' sufix=' €' [(ngModel)]="value"/>
The idea of the directive is that, when blur, change the "native Element", but not the value of the ngControl, when focus return the value of the ngControl
The stackblitz
I have used Numberal js for formatting numbers and it was awesome
Your desired format is this one '0,0[.]00 €'
check docs
Related
I'm doing angular project and I want to let the user search/filter from existing tags (mat-chips). I have a searchbox and I can filter a normal list but when I tried to do that for tags, I'm not sure how.
My mat chips inside home.component.html.
<mc-tags [chips] = "tags" ></mc-tags>
My search box inside home.component.html
<input matInput (input)="updateQuery($event.target.value)" class="input" >
the data inside list.ts
export const tags = ['Google', 'Manufacturer'];
home.component.ts file
import { Component, OnInit } from '#angular/core';
import { users, tags } from './users.data';
#Component({
selector: 'mc-explore',
templateUrl: './explore.component.html',
styleUrls: ['./explore.component.scss']
})
export class ExploreComponent{
query: string;
users = users;
tags = tags;
updateQuery(query: string) {
this.query = query;
}
}
This is how it look like right now
Picture
this is how I usually filter normal list/data
<div [hidden]="!query">
<div *ngFor="let name of users | search:query">{{ name }}</div>
</div>
Stackblitz file without mc-tags since it's using from different components
https://stackblitz.com/edit/angular-vcklft
You can do what is described below.
Change this:
<input matInput (input)="updateQuery($event.target.value)" class="input" >
to this:
<input [formControl]="_inputCtrl" matInput class="input" >
And change this:
<mc-tags [chips] = "tags" ></mc-tags>
to this:
<mc-tags [chips]="_filteredTags" ></mc-tags>
Add this code to your component typescript:
_filteredTags = [];
_inputCtrl: FormControl = new FormControl();
private _destroy$ = new Subject<void>();
ngOnInit(): void {
this._filterTags();
this._inputCtrl.valueChanges
.pipe(takeUntil(this._destroy$))
.subscribe((value: string) => {
this._filterTags(value);
this.updateQuery(value);
});
}
ngOnDestroy(): void {
if (this._destroy$ && !this._destroy$.closed) {
this._destroy$.next();
this._destroy$.complete();
}
}
updateQuery(query: string) {
this.query = query;
}
private _filterTags(filterValue?: string) {
if (!filterValue) {
this._filteredTags = [...tags];
return;
}
this._filteredTags = this.tags.filter((v: string) =>
v.toLowerCase().includes(filterValue.toLowerCase().trim())
);
}
[UPDATE]: I've put together this stackblitz demo
I am trying to create a time-picker. The picker will be opened when the user focuses on a text-box. Now, a single page may contain multiple text-boxes, for each of which the picker should be opened. The issue I am facing is, I get the values from time-picker for different text-boxes, but when binding to ngModel, any selected value gets bound to all the text-boxes.
Let me show you my approach:
component.html
<input type="text" [(ngModel)]="pickerData" (focus)="initPicker($event)" id="f-1" />
<input type="text" [(ngModel)]="pickerData" (focus)="initPicker($event)" id="f-2" />
<div #timepicks></div> <!-- Here where the picker will be dynamically Injected -->
component.ts
import { Component, OnInit, ViewChild, Output, EventEmitter, HostListener, ViewContainerRef,
ComponentFactoryResolver } from '#angular/core';
import { TimepickComponent } from './timepick/timepick.component';
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
pickerData: any;
#ViewChild('timepicks', {read: ViewContainerRef, static: false}) timepicks: ViewContainerRef;
constructor(
private _componentFactoryResolver: ComponentFactoryResolver
) {}
ngOnInit() {
}
initPicker = (event) => {
this.timepicks.clear();
let pickerComponentFactory =
this._componentFactoryResolver.resolveComponentFactory(TimepickComponent);
//Dynamically creating the ' TimepickComponent ' component
let pickerComponentRef = this.timepicks.createComponent(pickerComponentFactory);
(<TimepickComponent>(pickerComponentRef.instance)).pickerId = event.target.id; // Passing id
pickerComponentRef.instance.pickData.subscribe(res => {
this.pickerData = res;
pickerComponentRef.destroy();
});
}
}
Timepickcomponent.ts
.....
.....
#Input() pickerId: any;
#Output() pickData = new EventEmitter();
.....
.....
setClose = () => {
this.pickData.emit(this.valueHolder); // Emitting as output
}
Current Output
Screenshot 1
screenshot 2
As it can be seen, screen1 is opening based on text-box id, but in screen2, when I select and set, it gets populated in another text-box. Ideally, the selected picker from a text-box should bind with that particular text-box.
Any help would be appreciated.
I will use Document Service to access the element and set the value for clicked input control:
HTML:
<input type="text" (focus)="initPicker($event)" id="f-1" />
<input type="text" (focus)="initPicker($event)" id="f-2" />
<div #timepicks></div>
Removed Two way data binding
TS:
import { Component, OnInit, ViewChild, Output, EventEmitter, HostListener, ViewContainerRef, ComponentFactoryResolver, Inject } from '#angular/core';
import { TimepickComponent } from './timepick/timepick.component';
// Added Document reference
import { DOCUMENT } from '#angular/common';
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
title = 'timepicker';
isVisible: boolean = false;
pickerData: any;
#ViewChild('timepicks', { read: ViewContainerRef, static: false }) timepicks: ViewContainerRef;
constructor(
#Inject(DOCUMENT) private document: HTMLDocument,
private _componentFactoryResolver: ComponentFactoryResolver
) { }
ngOnInit() {
}
initPicker = (event) => {
this.timepicks.clear();
let pickerComponentFactory = this._componentFactoryResolver.resolveComponentFactory(TimepickComponent);
let pickerComponentRef = this.timepicks.createComponent(pickerComponentFactory);
(<TimepickComponent>(pickerComponentRef.instance)).pickerId = event.target.id;
pickerComponentRef.instance.pickData.subscribe(res => {
console.log(event.target.id);
if (res) {
(<HTMLInputElement>this.document.getElementById(event.target.id)).value = res;
}
pickerComponentRef.destroy();
});
}
}
Import Inject from #angular/core and DOCUMENT from #angular/common
Injected in constructor: #Inject(DOCUMENT) private document: HTMLDocument
Used (<HTMLInputElement>this.document.getElementById(event.target.id)).value = res; to set the value attribute of clicked input element
Working_Demo
The problem here is you were binding both the input boxes with same variable pickerData. So, even if you change any one of them, the change will be reflected for both the values.
Since you have two textboxes you need two variables to store their values, like pickerData1 and pickerData2.
While calling the initPicker() you need to pass one more parameter i,e in which textbox the datepicker is currently being opened and store the date in that respective variable.
Html code
<input type="text" [(ngModel)]="pickerData1" (focus)="initPicker($event, 1)" id="f-1" />
<input type="text" [(ngModel)]="pickerData2" (focus)="initPicker($event, 2)" id="f-2" />
TS code
initPicker = (event, index) => {
this.timepicks.clear();
let pickerComponentFactory =
this._componentFactoryResolver.resolveComponentFactory(TimepickComponent);
//Dynamically creating the ' TimepickComponent ' component
let pickerComponentRef = this.timepicks.createComponent(pickerComponentFactory);
(<TimepickComponent>(pickerComponentRef.instance)).pickerId = event.target.id; // Passing id
pickerComponentRef.instance.pickData.subscribe(res => {
if (index === 1)
this.pickerData1 = res;
elseif (index === 2)
this.pickerData2 = res;
pickerComponentRef.destroy();
});
I am using ngx-bootstrap for date picker, my date picker has input format "dd/MM/yyyy" like 25/07/2019
so I create an component for capturing the date and convert it into my reactive form (because mysql accept date as yyyy-mm-dd). I use ngModel in input and doing value changes everytime user did input value.
the code as follow :
import { environment } from './../../../environments/environment';
import { Component, OnInit, Input } from '#angular/core';
import { BsDatepickerConfig } from 'ngx-bootstrap';
import { FormGroup } from '#angular/forms';
import * as moment from "moment";
#Component({
selector: 'app-input-date',
templateUrl: './input-date.component.html',
styleUrls: ['./input-date.component.css']
})
export class InputDateComponent implements OnInit {
#Input() form: FormGroup;
#Input() fieldName: string;
#Input() fieldDescription: string;
datePickerConfig: Partial<BsDatepickerConfig>;
tempDate: any ;
isInvalid: boolean;
constructor() {
this.datePickerConfig = Object.assign(
{},
{
containerClass: "theme-dark-blue",
dateInputFormat: environment.dateFormat
}
);
}
ngOnInit() {
let theDate = this.form.get(this.fieldName).value;
if( theDate != ""){
this.tempDate = moment(theDate, environment.mysqlDateFormat).format(environment.dateFormat);
}
}
get myField()
{
return this.form.get(this.fieldName);
}
// For date
onDateModelChange(event)
{
if(this.tempDate != ""){
this.form.patchValue({
[this.fieldName]: moment(this.tempDate, environment.dateFormat).format(environment.mysqlDateFormat)
});
}else{
this.form.patchValue({
[this.fieldName]: ""
});
}
console.log(this.form.get(this.fieldName).errors);
}
}
<input type="text" class="form-control mydatepicker"
placeholder="" id="mydatepicker"
[ngClass]="{'has-error': myField.touched && myField.hasError('required')}"
[(ngModel)]="tempDate" (ngModelChange)="onDateModelChange($event)" [ngModelOptions]="{standalone: true}"
[bsConfig]="datePickerConfig" bsDatepicker>
Now, I need to display error if the user forgot to enter the date or the date input was invalid. I set the reactiveform using Validators.required like
this.form = this.formBuilder.group({ dateOfBirth: ["", Validators.required]});
But since I use ngModel it wont validated. I just wonder how to get the field validated everytime user click / entering the input (pristine or dirty)
I tried this code on my InputDateComponent but it always return null
onDateModelChange(event){
console.log(this.form.get(this.fieldName).errors);
}
If you have simple solutions for date format, I would like to know as well..
I would like to build a directive that can mutate values being passed to and from an input, bound with ngModel.
Say I wanted to do a date mutation, every time the model changes, the mutator first gets to change the value to the proper format (eg "2017-05-03 00:00:00" is shown as "2017/05/03"), before ngModel updates the view. When the view changes, the mutator gets to change the value before ngModel updates the model (eg entering "2017/08/03" sets the model to "2017-08-03 00:00:00" [timestamp]).
The directive would be used like this:
<input [(ngModel)]="someModel" mutate="date:YYYY/MM/DD" />
My first instinct was to get a reference to the ControlValueAccessor and NgModel on the Host component.
import { Directive, ElementRef, Input,
Host, OnChanges, Optional, Self, Inject } from '#angular/core';
import { NgModel, ControlValueAccessor,
NG_VALUE_ACCESSOR } from '#angular/forms';
#Directive({
selector: '[mutate]',
})
export class MutateDirective {
constructor(
#Host() private _ngModel: NgModel,
#Optional() #Self() #Inject(NG_VALUE_ACCESSOR)
private _controlValueAccessor: ControlValueAccessor[]
){
console.log('mutute construct', _controlValueAccessor);
}
}
Then I realized that the Angular 2 Forms classes are complicated and I have no idea what I'm doing. Any ideas?
UPDATE
Based on the answer below I came up with the solution: see gist
Usage (requires Moment JS):
<input mutate="YYYY/MM/DD" inputFormat="YYYY-MM-DD HH:mm:ss" [(ngModel)]="someDate">
Short answer: you need to implement ControlValueAccessor in some class and provide it as a NG_VALUE_ACCESSOR for the ngModel with some directive. This ControlValueAccessor and directive can actually be the same class.
TL;DR
It's not very obvious but still not very complicated. Below is the skeleton from one of my date controls. This thing acts as a parser/formatter pair for the angular 1 ng-model.
It all starts with ngModel injecting all NG_VALUE_ACCESSOR's into itself. There are bunch of default providers as well, and they all get injected into ngModel constructor, but ngModel can distinguish between default value accessors and the ones provided by the user. So it picks one to work with. Roughly it looks like this: if there's user's value accessor then it will be picked, otherwise it falls back to choosing from default ones. After that initial setup is done.
Control value accessor should subscribe to the 'input' or some other similar event on input element to process input events from it.
When value is changed externally ngModel calls writeValue() method on value accessor picked during initialization. This method is responsible for rendering display value that will go into an input as string shown to user.
At some point (usually on blur event) control can be marked as touched. This is shown as well.
Please note: code below is not real production code, it has not been tested, it can contain some discrepancies or inaccuracies, but in general it shows the whole idea of this approach.
import {
Directive,
Input,
Output,
SimpleChanges,
ElementRef,
Renderer,
EventEmitter,
OnInit,
OnDestroy,
OnChanges,
forwardRef
} from '#angular/core';
import {Subscription, Observable} from 'rxjs';
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '#angular/forms';
const DATE_INPUT_VALUE_ACCESSOR_PROVIDER = [
{provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => DateInputDirective), multi: true}
];
#Directive({
// [date-input] is just to distinguish where exactly to place this control value accessor
selector: 'input[date-input]',
providers: [DATE_INPUT_VALUE_ACCESSOR_PROVIDER],
host: { 'blur': 'onBlur()', 'input': 'onChange($event)' }
})
export class DateInputDirective implements ControlValueAccessor, OnChanges {
#Input('date-input')
format: string;
model: TimeSpan;
private _onChange: (value: Date) => void = () => {
};
private _onTouched: () => void = () => {
};
constructor(private _renderer: Renderer,
private _elementRef: ElementRef,
// something that knows how to parse value
private _parser: DateParseTranslator,
// something that knows how to format it back into string
private _formatter: DateFormatPipe) {
}
ngOnInit() {
}
ngOnChanges(changes: SimpleChanges) {
if (changes['format']) {
this.updateText(this.model, true);
}
}
onBlur = () => {
this.updateText(this.model, false);
this.onTouched();
};
onChange = ($event: KeyboardEvent) => {
// the value of an input - don't remember exactly where it is in the event
// so this part may be incorrect, please check
let value = $event.target.value;
let date = this._parser.translate(value);
this._onChange(date);
};
onTouched = () => {
this._onTouched();
};
registerOnChange = (fn: (value: Date) => void): void => {
this._onChange = fn;
};
registerOnTouched = (fn: () => void): void => {
this._onTouched = fn;
};
writeValue = (value: Date): void => {
this.model = value;
this.updateText(value, true);
};
updateText = (date: Date, forceUpdate = false) => {
let textValue = date ? this._formatter.transform(date, this.format) : '';
if ((!date || !textValue) && !forceUpdate) {
return;
}
this._renderer.setElementProperty(this._elementRef.nativeElement, 'value', textValue);
}
}
Then in the html template:
<input date-input="DD/MM/YYYY" [(ngModel)]="myModel"/>
You shouldn't have to do anything with Forms here. As an example, I made a credit card masking directive that formats the user input into a credit card string (a space every 4 characters, basically).
import { Directive, ElementRef, HostListener, Input } from '#angular/core';
#Directive({
selector: '[credit-card]' // Attribute selector
})
export class CreditCard {
#HostListener('input', ['$event'])
confirmFirst(event: any) {
let val = event.target.value;
event.target.value = this.setElement(val);
}
constructor(public element: ElementRef) { }
setElement(val) {
let num = '';
var v = val.replace(/\s+/g, '').replace(/[^0-9]/gi, '');
var matches = v.match(/\d{4,16}/g);
var match = matches && matches[0] || '';
var parts = [];
for (var i = 0, len = match.length; i < len; i += 4) {
parts.push(match.substring(i, i + 4));
}
if (parts.length) {
num = parts.join(' ').trim();
} else {
num = val.trim();
}
return num;
}
}
Then I used it in a template like so:
<input credit-card type="text" formControlName="cardNo" />
I am using form control in this example, but it doesnt matter either way. It should work fine with ngModel binding.
The task is simple, it is necessary that the input was entered only numbers below a certain number. I did so:
export class MaxNumber implements PipeTransform{
transform(value, [maxNumber]) {
value = value.replace(/[^\d]+/g,'');
value = value > maxNumber?maxNumber:value;
return value;
}
}
and then in the template called something like:
<input type="text" [ngModel]="obj.count | maxNumber:1000" (ngModelChange)="obj.count=$event" />
But it works very strange click.
I probably misunderstand something. I would be grateful if someone will explain that behavior.
I think that you need rather a custom value accessor. This way you will be able to check the value before setting it in the ngModel. This way you obj.count won't be upper than 1000.
Here is a sample implementation:
const CUSTOM_VALUE_ACCESSOR = new Provider(
NG_VALUE_ACCESSOR, {useExisting: forwardRef(() => MaxNumberAccessor), multi: true});
#Directive({
selector: 'input',
host: {'(input)': 'customOnChange($event.target.value)'},
providers: [ CUSTOM_VALUE_ACCESSOR ]
})
export class MaxNumberAccessor implements ControlValueAccessor {
onChange = (_) => {};
onTouched = () => {};
constructor(private _renderer: Renderer, private _elementRef: ElementRef) {}
writeValue(value: any): void {
var normalizedValue = (value == null) ? '' : value;
this._renderer.setElementProperty(this._elementRef.nativeElement, 'value', normalizedValue);
}
customOnChange(val) {
var maxNumber = 1000;
val = val.replace(/[^\d]+/g,'');
val = val > maxNumber?maxNumber:val;
this.onChange(val);
}
registerOnChange(fn: (_: any) => void): void { this.onChange = fn; }
registerOnTouched(fn: () => void): void { this.onTouched = fn; }
}
There is nothing to do in your component to use it than setting this directive into its directives attribute:
#Component({
selector: 'my-app',
template: `
<div>
<h2>Hello {{name}}</h2>
Number: <input type="text" [(ngModel)]="obj.count" />
<p>Actual model value: {{obj.count}}</p>
</div>
`,
directives: [MaxNumberAccessor]
})
export class App {
(...)
}
Corresponding plunkr: https://plnkr.co/edit/7e87xZoEHnnm82OYP84o?p=preview.