I'd like to create a custom form input that is just a wrapper around the normal input and that displays an error message in case the formControl isn't valid.
So I made a component implementing ControlValueAccessor but now that I want to display the error message I realize I have no hold of the FormControl, thus I need to pass it as #Input(). The problem is... Why implement the ControlValueAccessor in the first place if I can just pass the FormControl as input ? I'm confused.
html
<input
[placeholder]="placeholder"
[type]="type"
[(ngModel)]="value"
(keyup)="onChange()"
(change)="onChange()"
(blur)="onTouched()"
[disabled]="disabled"/>
ts
#Component({
selector: 'input-app',
templateUrl: './input.component.html',
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => InputComponent),
multi: true
}
]
})
export class InputComponent implements ControlValueAccessor {
#Input() type = 'text';
#Input() placeholder = 'placeholder';
#Input() errorMsg = '';
value: string;
disabled: boolean;
private onChangeFn;
private onTouchedFn;
writeValue(val: string | number): void {
this.value = val;
}
registerOnChange(fn: any): void {
this.onChangeFn = fn;
}
registerOnTouched(fn: any): void {
this.onTouchedFn = fn;
}
setDisabledState?(isDisabled: boolean): void {
this.disabled = isDisabled;
}
onChange(){
this.onChangeFn(this.value);
}
onTouched(){
this.onTouchedFn();
}
}
Related
The link to the stackblitz https://stackblitz.com/edit/angular-ivy-csqein?file=src/app/hello.component.html
When I copy-paste a value into the INPUT box it returns the correct data
INPUT - 12345678 OUTPUT - 12,345,678
but when I input values one by one it is not able to format it
the output looks like this 1,234567
Expected OUTPUT
When the input is first loaded it comes with the comma-separated digits.
I want to make it so that when the users add or delete values in the input box, the commas are updated in the very box.
Things that I have tried
Creating custom Pipe and updating the value in the component using valueChanges
You can't transform input value when you using formControlName, because when you want to transform that value, angular give you a formControl that you can use to change that value.
For example in your code, you can do this:
constructor(private fb: FormBuilder, private currencyPipe: CurrencyPipe) {}
profileForm;
ngOnInit() {
this.profileForm = this.fb.group({
name: this.currencyPipe.transform(12345678, '', '','0.0'),
});
// this.changes();
}
Note: Don't forget to provide CurrencyPipe in your module.
The code above it's the only way or solution you can do to change the input value when you use the formControlName.
Or another way you can use is remove your formControlName from your input and it will working fine.
<input
id="number"
[value]="profileForm.get('name').value | number"
maxlength="8"
/>
But the problem is you should have to do it manually to patch value from input to your formControl. You can use (input) like what I do in custom input component below.
Custom Input Component
If you like to using custom input component, then code below maybe can help you to resolve your question:
You can create app-input.ts and put this code below:
import { CurrencyPipe } from '#angular/common';
import {
Component,
forwardRef,
HostListener,
Input,
OnInit,
} from '#angular/core';
import {
ControlContainer,
ControlValueAccessor,
FormControl,
FormGroup,
NG_VALUE_ACCESSOR,
} from '#angular/forms';
#Component({
selector: 'app-input[type=currency]',
templateUrl: './currency.component.html',
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CurrencyComponent),
multi: true,
},
],
})
export class CurrencyComponent implements ControlValueAccessor, OnInit {
#Input() formControlName: string;
value: any;
onChange: () => any;
onTouche: () => any;
public formGroup: FormGroup;
public formControl: FormControl;
constructor(
private controlContainer: ControlContainer,
private currencyPipe: CurrencyPipe
) {}
ngOnInit() {
console.log('Currency Component');
console.log(this.controlContainer);
this.setStateInitialization();
}
private setStateInitialization(): void {
this.formGroup = this.controlContainer.control as FormGroup;
this.formControl = this.formGroup.get(this.formControlName) as FormControl;
}
writeValue(value: any): void {
this.value = value;
}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouche = fn;
}
#HostListener('input', ['$event'])
private _onHostListenerInput(event: InputEvent): void {
const inputElement = event.target as HTMLInputElement;
let value: string | number = inputElement.value;
if (value) value = +inputElement.value.replace(/\D/g, '');
this.formControl.patchValue(value);
inputElement.value = value
? this.currencyPipe.transform(value, '', '', '0.0')
: '';
}
}
Now you can add app-input.html and use code below:
<input
type="text"
[value]="formControl.value ? (formControl.value | currency: '':'':'0.0') : ''"
/>
After that if you want to use this component, you can call:
<app-input type="currency" formControlName="currency"></app-input>
Or whatever name you want, you can change it.
Update:
Live Preview: https://angular-ivy-aqppd6.stackblitz.io
Live Code: https://stackblitz.com/edit/angular-ivy-aqppd6?file=src/app/app.component.ts
I hope it can help you to imagine what you can do to resolve your question.
I am trying to write an accessor component, for an input that has a label associated with it, but I am not sure if I am doing this correctly. When I attempt to do two-way-binding, the value isn't set in the input field even though it is set within the class MyComponent.
Why isn't my two-way-binding working? I would expect to see Something Special inside the <input> that is within <third-party-input>. However, the input is empty.
When using <third-party-input> on its own, two-way-binding works there, just not when I wrap my component around it.
Main Component
<ui-input [(ngModel)]="myValue"></ui-input>
export class MyComponent {
public myValue = 'Something Special';
}
Input With Label Component
<label>{{label}}</label>
<third-party-input [(ngModel)]="value"></third-party-input>
#Component({
selector: 'ui-input',
templateUrl: './input.component.html',
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => InputComponent),
multi: true
}]
})
export class InputComponent extends AccessorComponent {}
#Component({
selector: 'app-accessor',
template: ''
})
export abstract class AccessorComponent implements ControlValueAccessor {
protected _value: any;
public get value() {
return this._value;
}
public set value(val) {
this._value = val;
this.onChange(val);
this.onTouched();
}
protected onChange = (_: any) => { };
protected onTouched: any = () => { };
public registerOnChange(fn: (_: any) => void): void {
this.onChange = fn;
}
public registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
public writeValue(value: any): void {
if (value !== undefined) {
this._value = value;
}
}
}
Currently we are going to port our old application to new technologies and get rid of old design failures and improve the overall UX.
At the moment we are wrapping all standard inputs in own components, so we can change them very easily if needed. All our input components could have a validation but the input components can be nested in an input group of bootstrap, like this:
<form #myForm="ngForm">
<div>
<label>Birth Year</label>
<vng-controls-textbox #user="ngModel" name="user" minlength="5" ngModel ></vng-controls-textbox>
<label>Text</label>
<vng-controls-textbox #text="ngModel" name="text" maxlength="5" ngModel ></vng-controls-textbox>
</div>
<div vng-control-group [icon]="['fa','user']">
<vng-controls-textbox required type="text" #username="ngModel" ngModel name="Username">
</vng-controls-textbox>
<vng-controls-textbox required type="password" #password="ngModel" ngModel name="Passwort">
</vng-controls-textbox>
</div>
</form>
The validation works fine for standalone input components, but we want to deactivate the validation for each nested component in our and get all errors of all possible controls with validations. The desired solution should be a generic approach and no specifics, if possible.
Our base component:
import { Component, OnInit, EventEmitter, Input, Output, forwardRef, AfterViewInit, Self } from '#angular/core';
import { Validator, ControlValueAccessor, FormControl, NgControl } from '#angular/forms';
#Component({
selector: 'app-base'
})
export class BaseComponent implements OnInit, ControlValueAccessor, Validator, AfterViewInit//, IControlBase<any>
{
public ngControl: any;
private parseError: boolean;
private _value: any = '';
/** Input value */
#Input() set value(newValue: any) {
if (newValue != this._value && newValue !== undefined) {
console.log("[BaseComponent]: Set value " + newValue);
this.valueChange.emit(newValue);
this._value = newValue;
}
};
get value(): any {
if (this._value) {
return this._value;
}
}
/** Platzhalter */
#Input() placeholder: any;
/** Disabled */
#Input() disabled: boolean;
/** Name des Controls */
#Input() name: string;
/** Optional: Typ des Controls (Text, Password) */
#Input() type?: string;
#Input() hideErrors: boolean = false;
#Output() valueChange: EventEmitter<any> = new EventEmitter<any>();
// this is the initial value set to the component
public writeValue(obj: any) {
if (obj !== undefined && obj) {
console.log("[BaseComponent] writeValue ", obj)
this._value = obj;
}
}
// registers 'fn' that will be fired wheb changes are made
// this is how we emit the changes back to the form
public registerOnChange(fn: any) {
this.propagateChange = fn;
}
// validates the form, returns null when valid else the validation object
// in this case we're checking if the json parsing has passed or failed from the onChange method
public validate(c: FormControl) {
return null;
}
// not used, used for touch input
public registerOnTouched() { }
// change events from the textarea
private onChange(event) {
if (event.target.value !== undefined) {
console.log("[BaseComponent] "+this.name+" OnChange " + event.target.value)
// get value from text area
this._value = event.target.value;
this.propagateChange(this._value);
}
}
// the method set in registerOnChange to emit changes back to the form
private propagateChange = (_: any) => { };
registerOnValidatorChange?(fn: () => void): void {
console.log("[BaseComponent]: registerOnValidatorChange");
}
setDisabledState?(isDisabled: boolean): void {
console.log("[BaseComponent]: setDisabledState");
}
constructor(#Self() ngControl: NgControl) {
ngControl.valueAccessor = this;
if (ngControl) {
this.ngControl = ngControl;
}
}
ngOnInit() {
}
ngAfterViewInit(): void {
//debugger;
//this.placeholder = this.translateService.instant((this.placeholder ? this.placeholder : this.name))
}
}
Our control group component typescript:
import { Component, OnInit, Input, Host, Self } from '#angular/core';
import { ControlContainer, FormControl, AbstractControl, NgControl } from '#angular/forms';
import { BaseComponent } from '../../_bases/base.component'
import { QueryList, ViewChildren, ContentChildren, TemplateRef, ContentChild } from '#angular/core';
import { TextboxComponent } from '../textbox/textbox.component';
/**
* Komponente für Input-Groups
* #example
* <div vng-control-group [config]="textboxUsernameConfig">
* // Kann ein diverses Control sein, was als CSS-Klasse "form-control besitzt"
* <input class="input form-control" />
* </div>
*
* // oder
*
* <div vng-control-group [icon]="['fa','user']" [label]="Test Label" [position]="'append'">
*/
#Component({
selector: 'div[vng-control-group]',
templateUrl: './control-group.component.html'
})
export class ControlGroupComponent {
private controlContainer: ControlContainer;
public formControl: AbstractControl;
#ContentChild(TemplateRef) template: TemplateRef<any>;
ngAfterContentInit() {
}
/** Konfiguration der Inputgroup als Objekt */
#Input() config: InputGroupConfig;
/** Icon als Attribut (z.B. im HTML) */
#Input() icon?: object;
/** Label als Attribut (z.B. im HTML) */
#Input() label?: string;
/** Position als Attribut (z.B. im HTML) */
#Input() position?: GroupPosition;
constructor(#Host() parent: ControlContainer) {
//this.controlContainer = parent;
//this.formControl = this.controlContainer.control;
}
ngOnInit() {
// Wenn kein Konfig-Objekt übergeben worden ist, setze die Attribute bzw. setze Default-Werte
if (!this.config) {
console.log("[ControlGroupComponent]: Keine Config übergeben")
this.config = {
icon: this.icon || ['fa', 'question'],
label: this.label || '',
position: this.position || GroupPosition.prepend
}
}
}
}
export interface InputGroupConfig {
icon?: object,
label?: string,
position?: GroupPosition
}
export enum GroupPosition {
append = 'append',
prepend = 'prepend'
}
Our control-group html:
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text" id="basic-addon1">#</span>
</div>
<ng-content></ng-content>
</div>
Show error component (ts/html):
// show-errors.component.ts
import { Component, Input } from '#angular/core';
import { AbstractControlDirective, AbstractControl } from '#angular/forms';
#Component({
selector: 'show-errors',
template: `
<ul *ngIf="shouldShowErrors()">
<li style="color: red" *ngFor="let error of listOfErrors()">{{error}}</li>
</ul>
`,
})
export class ShowErrorsComponent {
private static readonly errorMessages = {
'required': () => 'This field is required',
'minlength': (params) => 'The min number of characters is ' + params.requiredLength,
'maxlength': (params) => 'The max allowed number of characters is ' + params.requiredLength,
'pattern': (params) => 'The required pattern is: ' + params.requiredPattern,
'years': (params) => params.message,
'countryCity': (params) => params.message,
'uniqueName': (params) => params.message,
'telephoneNumbers': (params) => params.message,
'telephoneNumber': (params) => params.message
};
#Input()
private control: AbstractControlDirective | AbstractControl;
shouldShowErrors(): boolean {
return this.control &&
this.control.errors &&
(this.control.dirty || this.control.touched);
}
listOfErrors(): string[] {
return Object.keys(this.control.errors)
.map(field => this.getMessage(field, this.control.errors[field]));
}
private getMessage(type: string, params: any) {
return ShowErrorsComponent.errorMessages[type](params);
}
}
How can we achieve that? I'm pretty stuck since 3 days and can't get it working.I really don't know how to disable the error messages in the content of and print all errors messages of all nested inputs under the input group.
Besides that, our nested inputs within the control group are looking pretty bad:
Bad bootstrap format on nested inputs
I've created a stackblitz example which demonstrates the behavior of both problems:
https://angular-ng-bootstrap-khjkkhhjkhk.stackblitz.io
Editor:
https://stackblitz.com/edit/angular-ng-bootstrap-khjkkhhjkhk
Do you have any idea how can we fix that too? I guess it's because of the which wraps the controls in an additional div which causes the strange looking inputs. Preferably we want to style them in the manner of bootstrap.
Maybe it's not the case but I see you're not declaring the providers in #Component decorator using ControlValueAccessor.
Like this:
#Component({
selector: 'app-input',
templateUrl: './input.component.html',
styleUrls: ['./input.component.scss'],
providers: [
{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => InputComponent), multi: true },
{ provide: NG_VALIDATORS, useExisting: forwardRef(() => InputComponent), multi: true }
]
})
I'm not sure if there's a better way to do this, I assume there should be. Basically I have a component I want to treat as a standalone form control. This control will always have some sort of special validation attached and I would like it to bubble up to the form whenever the component is used.
I've attached a plunker. Is there a way to for the form to be marked invalid if the component/formControl is invalid? I know I could add the validator to the form itself, but I would like to make things easy and more predictable for future use of this component. I'm also open to better ideas of doing this.
#Component({
selector: 'my-app',
template: `
<form [formGroup]="form">
<my-component-control formControlName="myComponent"></my-component-control>
</form>
<div>Form Value: {{form.value | json}}</div>
<div>Form Valid: {{form.valid}}</div>
`,
})
export class App {
constructor(fb: FormBuilder) {
this.form = fb.group({
myComponent: ''
});
}
}
#Component({
selector: 'my-component-control',
template: `
<div>Control Errors: {{control.errors | json}}</div>
<input [formControl]="control">
`,
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: forwardRef(() => MyComponentComponent),
}
]
})
export class MyComponentComponent implements ControlValueAccessor {
control: FormControl;
onChange: any = () => { };
onTouched: any = () => { };
constructor() {
this.control = new FormControl('', (control) => {
return control.value ? null : {shouldHaveSomething: true};
});
this.control.valueChanges.subscribe((value) => {
this.onChange(value);
this.onTouched();
});
}
writeValue (obj: any): void {
this.control.setValue(obj);
}
registerOnChange (fn: any): void {
this.onChange = fn;
}
registerOnTouched (fn: any): void {
this.onTouched = fn;
}
setDisabledState?(isDisabled: boolean): void {
throw new Error('Method not implemented.');
}
}
One solution could be using setValidators method on host AbstractControl
To do so I'm going to get reference to AbstractControl through NgControl. We can't just inject NgControl in constructor because we'll get issue with instantiating cyclic dependency.
constructor(private injector: Injector) {
...
}
ngOnInit() {
Promise.resolve().then(() => {
const hostControl = this.injector.get(NgControl, null);
if (hostControl) {
hostControl.control.setValidators(this.control.validator);
hostControl.control.updateValueAndValidity();
}
});
}
Ng-run Example
I'm trying to figure out how to switch a variable in a group of child components
I have this component for a editable form control which switches between view states
import {
Component,
Input,
ElementRef,
ViewChild,
Renderer,
forwardRef,
OnInit
} from '#angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '#angular/forms';
const INLINE_EDIT_CONTROL_VALUE_ACCESSOR = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => InlineEditComponent),
multi: true
};
#Component({
selector: 'inline-edit',
templateUrl: 'inline-edit.html',
providers: [INLINE_EDIT_CONTROL_VALUE_ACCESSOR],
})
export class InlineEditComponent implements ControlValueAccessor, OnInit {
#ViewChild('inlineEditControl') inlineEditControl: ElementRef;
#Input() label: string = '';
#Input() type: string = 'text';
#Input() required: boolean = false;
#Input() disabled: boolean = false;
private _value: string = '';
private preValue: string = '';
public editing: boolean = false;
public onChange: any = Function.prototype;
public onTouched: any = Function.prototype;
get value(): any {
return this._value;
}
set value(v: any) {
if (v !== this._value) {
this._value = v;
this.onChange(v);
}
}
writeValue(value: any) {
this._value = value;
}
public registerOnChange(fn: (_: any) => {}): void {
this.onChange = fn;
}
public registerOnTouched(fn: () => {}): void {
this.onTouched = fn;
}
constructor(element: ElementRef, private _renderer: Renderer) {
}
ngOnInit() {
}
}
<div>
<div [hidden]="!editing">
<input #inlineEditControl [required]="required" [name]="value" [(ngModel)]="value" [type]="type" [placeholder]="label" />
</div>
<div [hidden]="editing">
<label class="block bold">{{label}}</label>
<div tabindex="0" class="inline-edit">{{value}} </div>
</div>
</div>
I'm trying to create a simple directive to consume these components and change the editing flag to true
export class EditForm {
//I want to do something like this:
public toggleEdit(fn: () => {}): void {
var editableFormControls = $('#selector: 'inline-edit');
editableFormControls.forEach(control => control.editing = true)
}
}
I want to grab all of the ediiitable form controls and set the editing flag in all of them to true, how can I do this?
You might need to implement a service that keeps the state and all child component subscribe to the state and parent push changes there.
import {Component, NgModule, VERSION, Input} from '#angular/core'
import {BrowserModule} from '#angular/platform-browser'
import {BehaviorSubject} from 'rxjs/BehaviorSubject';
export class EditableService {
subject = new BehaviorSubject(true);
getAsObservable() {
return this.subject.asObservable();
}
}
#Component({
selector:'editable',
template: '<div>i am editable {{ x | async}}</div>'
})
export class Editable {
constructor(private editableService: EditableService) {
this.x = editableService.getAsObservable();
}
}
#Component({
selector: 'my-app',
template: `
<editable></editable>
<editable></editable>
<hr/>
<button (click)="change()">change</button>
`,
providers: [EditableService]
})
export class App {
change() {
this.editableService.subject.next(false);
}
constructor(private editableService: EditableService) {
this.name = `Angular! v${VERSION.full}`;
}
}