I mocked up a very small example of my problem here: https://github.com/lovefamilychildrenhappiness/AngularCustomComponentValidation
I have a custom component, which encapsulates an input field. The formControl associated with this input field has Validators.required (it is a required field). Inside the custom component, I have an onChange event which is fired when text is entered. I check if field is empty; if so, I add css class using ngClass. I also have set the registerOnChange of NG_VALUE_ACCESSOR, so I notify the form when the input changes. Finally, I implement NG_VALIDATORS interface to make the formControl invalid or valid.
My problem is I have a button that is clicked (it's not the submit button). When this button is clicked, I need to check if the custom component is blank or not, and if it is, change the css class and make the form invalid. I think the validate method of NG_VALIDATORS is doing that. But I need to change the css class of customComponent so background turns red. I spend severals hours on this and cannot figure it out:
// my-input.component.html
<textarea
[value]="value"
(input)="onChange($event.target.value)"
[ngClass]="{'failed-validation' : this.validationError }">
</textarea>
// my-input.component.ts
validate(control: FormControl): ValidationErrors | null {
if(!this.validationError){
return null
} else {
return { required: true };
}
}
private onChange(val) {
if(val.length > 0) {
this.value = val
this.validationError = false;
} else {
this.validationError = true;
}
// update the form
this.propagateChange(val);
}
// app.component.html
<form [formGroup]="reactiveForm">
<app-my-input formControlName="result"></app-my-input>
<input
value="Submit"
(click)="nextStep($event)"
type="button">
</form>
// app.component.ts
private nextStep(event){
// How do I dynamically change the class of the form control so I can change the style if formControl invalid when clicking the nextStep button
// pseudocode:
// if( !this.reactiveForm.controls['result'].valid ){
// this.reactiveForm.controls['result'].addClass('failed-validation');
// }
}
How can I get the css of the formControl to change in another component?
Since you using reactive form I have modified your custom form control. Here I have Use Injected NgControl Which is base class for all FormControl-based directives extend.
Try this:
import { Component, Input, forwardRef, OnInit } from "#angular/core";
import {
ControlValueAccessor,
NG_VALUE_ACCESSOR,
NgControl,
NG_VALIDATORS,
FormControl,
ValidationErrors,
Validator
} from "#angular/forms";
#Component({
selector: "app-my-input",
templateUrl: "./my-input.component.html",
styleUrls: ["./my-input.component.scss"]
})
export class MyInputComponent implements ControlValueAccessor, OnInit {
private propagateChange = (_: any) => {};
value = "";
onTouch: () => void;
constructor(public controlDir: NgControl) {
controlDir.valueAccessor = this;
}
writeValue(value) {
this.value = value;
}
registerOnChange(fn) {
this.propagateChange = fn;
}
registerOnTouched(fn) {
this.onTouch = fn;
}
onChange(value) {
this.propagateChange(value);
}
ngOnInit() {
const control = this.controlDir.control;
control.setValidators([control.validator ? control.validator : null]);
control.updateValueAndValidity();
}
}
Example
For More Information Forms Check this
Related
I have an angular reactive form with default Validation.required and a CustomValidation.
Inside the CustomValidation I intended to check if the control is touched, but somehow this is not working.
import {
CustomValidation
} from './CustomValidator';
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
})
export class AppComponent implements OnInit {
customForm: FormGroup;
constructor(private fb: FormBuilder) {}
ngOnInit() {
this.customForm = this.fb.group({
customInput: [
'', [Validators.required, CustomValidation.customEmailValidation()],
],
});
}
}
// CustomValidation.ts
import {
AbstractControl,
ValidationErrors,
ValidatorFn
} from '#angular/forms';
export class CustomValidation {
static customEmailValidation(): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
if (control.touched && control.value.length === 0) {
console.log(control);
return {
customError: {
hasError: true,
errorType: 'empty field', // this can be any name
errorLabel: 'test',
},
};
}
return null;
};
}
}
<form [formGroup]="customForm">
<input formControlName="customInput" />
<button [disabled]="this.customForm.invalid">Submit</button>
</form>
I am expecting that the console.log inside the static method customEmailValidation will log the control object when the field will be touched. By touch I mean, I only click on the input.But that is not happening.
I also tried using updateOn
customInput: ['', {validators:[CustomValidation.customEmailValidation(),Validators.required],updateOn:"blur"}]
Please help me in understanding why it is not working.
Stackblitz Demo
At first, before you touch it, form.touched is false.
So thats the first value.
That value is being taken for the first time as form.touched
So you will get touched property as false.
If you intend to make it touched, just go with
this.form.markAsTouched() and then do the logic.
You are describing the focusin event, not the touched state.
From the Angular docs:
touched: boolean Read-Only True if the control is marked as touched.
A control is marked touched once the user has triggered a blur event
on it.
You are expecting this:
By touch I mean, I only click on the input.But that is not happening.
Also, the custom validator should be agnostic to the form input state. You can achieve what you want by binding to the focusin event and using that to flip the touched state. I don't understand why you would want to do this from a UX perspective, but hey.
Template
<form [formGroup]="customForm">
<input (focusin)="onFocusIn($event)" formControlName="customInput" />
<button [disabled]="this.customForm.invalid">Submit</button>
</form>
<span *ngIf="this.customForm.invalid && this.customForm.touched"
>VERY SERIOUS ERROR</span
>
Component
onFocusIn(event) {
this.customForm.markAsTouched();
console.log('focus');
}
Validator
static customEmailValidation(): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
if (control.value.length === 0) {
console.log(control);
return {
// ...control.errors,
customError: {
hasError: true,
errorType: 'empty field', // this can be any name
errorLabel: 'test',
},
};
}
return null;
};
}
Stackblitz here.
You can read more about custom form controls here from the Angular docs.
I'm creating a reusable component which can be shown from any external component, but can be hidden using a function in same component, but somehow the property change in parent component is not updating child.
Here is the stackblitz for the same.
https://stackblitz.com/edit/angular-hfjkmu
I need "Show" button should show the component all the time and I can hide the component using "hide" button any time.
you need sync value from child to parent using Output
#Input()
show = false;
#Output()
showChange = new EventEmitter<boolean>();
constructor() { }
ngOnInit() {
}
hide(){
this.show = false;
this.showChange.emit(this.show);
}
<app-show-hide [(show)]="show"></app-show-hide>
The show property from child do not pointing to same prop in the parent comp, because it's primitive value.
I don't recommend to modify data that not belong to child component (reference type, eg: object, array), it can lead to unexpected behavior.
Online demo with reference type (be careful when modify ref type): https://stackblitz.com/edit/angular-vhxgpo?file=src%2Fapp%2Fshow-hide-obj%2Fshow-hide-obj.component.tsenter link description here
You have the problem because your child component modify Input value within your child component scope so no way parent component know the data is change
Your child component
export class ShowHideComponent implements OnInit {
#Input('show') show: boolean;
#Output() updateShowValue: EventEmitter<any> = new EventEmitter<
any
>();
constructor() { }
ngOnInit() {
console.log(this.show);
}
hide() {
this.updateShowValue.emit(!this.show);
}
}
In the app.component.html
<app-show-hide [show]="show" (updateShowValue)="update($event)"></app-show-hide>
And app.component.ts
export class AppComponent implements OnInit {
show:boolean = false;
ngOnInit() {
this.show = false;
console.log(this.show)
}
showComp(){
this.show = !this.show;
}
update(event) {
this.show = event;
}
}
You need to add an #Output in your child component, when you click the hide button (in the child component) you need to notify your parent component and change the value of show variable to false, this is done with the EventEmitter.
Changes to made are :
ShowHideComponent.ts
import { Component, OnInit, Input, Output, EventEmitter } from '#angular/core';
#Component({
selector: 'app-show-hide',
templateUrl: './show-hide.component.html'
})
export class ShowHideComponent {
#Input('show') show : boolean;
#Output('') hideEE = new EventEmitter();
constructor() { }
hide(){
this.hideEE.emit(false);
}
}
AppComponent.ts
import { Component,OnInit } from '#angular/core';
#Component({
selector: 'my-app',
templateUrl: './app.component.html'
})
export class AppComponent {
show:boolean = false;
}
appComponent.html
<button type="button" (click)="show = true">Show</button>
<app-show-hide [show]="show" (hideEE)="show = $event"></app-show-hide>
stackblitz Link
I am trying to use the ngx-mat-select-search component to put a mat-select style dropdown menu with a search bar in my application.
https://www.npmjs.com/package/ngx-mat-select-search
I have the dropdown working fine, but I am trying to turn it into a custom directive that I can then call and reuse on multiple pages through out the app.
So far I have this: site-dropdown-component.ts
import {AfterViewInit, Component, OnDestroy, OnInit, ViewChild} from '#angular/core';
import {FormControl} from '#angular/forms';
import {ReplaySubject, Subject} from 'rxjs';
import {MatSelect} from '#angular/material';
import {take, takeUntil} from 'rxjs/operators';
#Component({
selector: 'app-site-dropdown',
template: `
<mat-form-field class="w-100">
<mat-select [formControl]="siteCtrl" placeholder="Site" #singleSelect>
<mat-option>
<ngx-mat-select-search [formControl]="siteFilterCtrl" [placeholderLabel]="'Search Sites...'"></ngx-mat-select-search>
</mat-option>
<mat-option *ngFor="let site of filteredSites | async" [value]="site">{{site.name}}</mat-option>
</mat-select>
</mat-form-field>
`
})
export class SiteDropdownComponent implements OnInit, OnDestroy, AfterViewInit {
/** list of sites */
protected sites: Site[] = SITES;
/** control for the selected site */
public siteCtrl: FormControl = new FormControl();
/** control for the MatSelect filter keyword */
public siteFilterCtrl: FormControl = new FormControl();
/** list of sites filtered by search keyword */
public filteredSites: ReplaySubject<Site[]> = new ReplaySubject<Site[]>(1);
#ViewChild('singleSelect') singleSelect: MatSelect;
/** Subject that emits when the component has been destroyed. */
protected onDestroy = new Subject<void>();
constructor() { }
ngOnInit(): void {
// set initial selection
this.siteCtrl.setValue(this.sites);
// load the initial site list
this.filteredSites.next(this.sites.slice());
// listen for search field value changes
this.siteFilterCtrl.valueChanges
.pipe(takeUntil(this.onDestroy))
.subscribe(() => {
this.filterSites();
});
}
ngAfterViewInit(): void {
this.setInitialValue();
}
ngOnDestroy(): void {
this.onDestroy.next();
this.onDestroy.complete();
}
/**
* Sets the initial value after the filteredBanks are loaded initially
*/
protected setInitialValue() {
this.filteredSites
.pipe(take(1), takeUntil(this.onDestroy))
.subscribe(() => {
// setting the compareWith property to a comparison function
// triggers initializing the selection according to the initial value of
// the form control (i.e. _initializeSelection())
// this needs to be done after the filteredBanks are loaded initially
// and after the mat-option elements are available
this.singleSelect.compareWith = (a: Site, b: Site) => a && b && a.id === b.id;
});
}
protected filterSites() {
if (!this.sites) {
return;
}
// get the search keyword
let search = this.siteFilterCtrl.value;
if (!search) {
this.filteredSites.next(this.sites.slice());
return;
} else {
search = search.toLowerCase();
}
// filter the sites
this.filteredSites.next(
this.sites.filter(site => site.name.toLowerCase().indexOf(search) > -1)
);
}
}
export interface Site {
id: string;
name: string;
}
export const SITES: Site[] = [
{id: 'site1', name: 'Site 1'},
{id: 'site2', name: 'Site 2'},
{id: 'site3', name: 'Site 3'},
];
For the component im trying to use it in, i have:
<app-site-dropdown formControlName="site"></app-site-dropdown>
And inside the component class I have a form:
this.mySearchForm = this.formBuilder.group( {
site: []
});
I can see and interact with the dropdown just fine, but when i submit my form, I cannot get the value of the selected option. It just always returns null when i try mySearchForm.controls['site'].value
What am I missing to be able to inject my custom dropdown component, and retrieve its value upon form submission?
UPDATE:
I was able to make it work by doing the following:
Inside site-dropdown.component.ts, I changed
protected siteCtrl: FormControl;
to
#Input() siteCtrl: FormControl;
And inside my html using the custom dropdown, i added:
<app-site-dropdown [siteCtrl]="myForm.get('site')"></app-site-dropdown>
This allowed me to save the selected value into my form on submission.
you can get the value of the selected option by having your SiteDropdownComponent implement the ControlValueAccessor interface as follows, resulting in your SiteDropdownComponent behaving as a form control and allowing to access the value with e.g. <app-site-dropdown formControlName="site"></app-site-dropdown>:
...
import { forwardRef } from '#angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '#angular/forms';
#Component({
selector: 'app-site-dropdown',
template: ...
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => SiteDropdownComponent),
multi: true
}
],
})
export class SiteDropdownComponent implements OnInit, OnDestroy, AfterViewInit, ControlValueAccessor {
...
onChange: Function = (_: any) => {};
onTouched: Function = (_: any) => {};
constructor() { }
ngOnInit() {
...
// call this.onChange to notify the parent component that the value has changed
this.siteCtrl.valueChanges
.pipe(takeUntil(this.onDestroy))
.subscribe(value => this.onChange(value))
}
writeValue(value: string) {
// set the value of siteCtrl when the value is set from outside the component
this.siteCtrl.setValue(value);
}
registerOnChange(fn: Function) {
this.onChange = fn;
}
registerOnTouched(fn: Function) {
this.onTouched = fn;
}
}
See e.g. https://github.com/bithost-gmbh/ngx-mat-select-search/blob/d7ea78d511bbec45143c58c855f013a44d0d5055/src/app/mat-select-search/mat-select-search.component.ts#L134
I would like to create a directive that when it's given an variable with value of TRUE and if the user change the input value it won't write out that new value to the NgModel.
Example of use:
Directive selector: d-avoid-change
<input type="text" name="surname"
[(ngModel)]="model.surname"
[d-avoid-change]="true">
If the surname model that came from the server is "Foo" and the user change it to "Bar", the model.surname stays "Foo" and I can give a message to the user.
I was trying with another approach with the directive, that was to remove the input and click EventListener so the user would not be able to click, but that would seem like a bug.
I want to use it instead of the disabled property of HTML, because if I just use [disabled]="true" the user can open browser HTML inspector and change the value and save it, also I don't want to validate those permissions on the server. I've searched alot about this and couldn't find any suggestion, does anyone know how I could do that?
Found a way to build a directive that saves the original NgModel in a private variable and then if it receives the Input parameter as TRUE it will ignore the changing and put the original NgModel instead.
Also it overrides the native disabled so I don't need to use the directive and also the native disabled.
Directive code:
import {
Directive,
ElementRef,
AfterViewInit,
Input,
AfterContentInit,
ViewContainerRef,
Renderer2
} from '#angular/core';
import { NgModel } from '#angular/forms';
import { Observable } from 'rxjs/Observable';
declare let $;
#Directive({
selector: '[d-disabled]',
providers: [NgModel]
})
export class DisabledDirective implements AfterViewInit {
#Input('d-disabled')
set disabled(disabled: boolean) {
if (disabled) {
this.renderer.setAttribute(this.el.nativeElement, 'disabled', 'true');
} else {
this.renderer.removeAttribute(this.el.nativeElement, 'disabled');
}
this._disabled = disabled;
}
_disabled: boolean;
originalModel: any;
constructor(private el: ElementRef,
private ngModel: NgModel,
private renderer: Renderer2) {
this.ngModel.valueAccessor.registerOnChange = this.registerOnChange;
this.ngModel.valueAccessor.registerOnTouched = this.registerOnTouched;
this.originalModel = this.ngModel;
}
ngAfterViewInit() {
Observable.fromEvent(this.el.nativeElement, 'input')
.map((n: any) => n.target.value)
.subscribe(n => {
if (this._disabled) {
this.ngModel.viewToModelUpdate(this.originalModel.value);
this.ngModel.control.patchValue(this.originalModel.value);
this.ngModel.control.updateValueAndValidity({ emitEvent: true });
} else {
this.onChangeCallback(n);
}
});
}
private onChangeCallback: (_: any) => void = (_) => { };
private onTouchedCallback: () => void = () => { };
registerOnChange = (fn: (_: any) => void): void => { this.onChangeCallback = fn; };
registerOnTouched = (fn: () => void): void => { this.onTouchedCallback = fn; };
}
How to use it:
<input type="text" name="surname"
[(ngModel)]="model.surname"
[d-disabled]="true">
If anyone can help me to improve in any way this method, but this is working as I wanted.
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.