I have a directive to build dynamic Input components for a template driven form.
The default value is set by the Input component itself.
The problem is that setting a default value causes that the form is marked as dirty.
How is it possible to archieve setting a default value from inside of the Directive without marking the form as dirty?
#Directive({
selector: '[myFormInputFactory]',
providers: [
{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => MyFormFactoryDirective), multi: true }
]
})
export class MyFormFactoryDirective implements OnChanges, OnDestroy, ControlValueAccessor {
#Input() myFormInputFactory: DialogItem;
private componentRef: any;
private value: any;
private valueSubscription: Subscription;
private disabled = false;
constructor(
private viewContainerRef: ViewContainerRef,
private componentFactoryResolver: ComponentFactoryResolver,
private _renderer: Renderer,
private _elementRef: ElementRef
) { }
onChange = (_: any) => { };
onTouched = () => { };
registerOnChange(fn: (_: any) => void): void { this.onChange = fn; }
registerOnTouched(fn: () => void): void { this.onTouched = fn; }
ngOnChanges(changes: SimpleChanges) {
if ('myFormInputFactory' in changes) {
const config = changes['myFormInputFactory'].currentValue as IConfigItem;
const factories = Array.from(this.componentFactoryResolver['_factories'].values());
const comp = factories.find((x: any) => x.selector === config.selector) as ComponentFactory<{}>;
const componentRef = this.viewContainerRef.createComponent(comp);
if (this.componentRef) {
this.componentRef.destroy();
}
this.componentRef = componentRef;
this.valueSubscription = this.componentRef._component.valueChange.subscribe(value => {
this.value = value;
this.onChange(this.value);
});
}
}
ngOnDestroy() {
if (this.valueSubscription) {
this.valueSubscription.unsubscribe();
}
}
writeValue(value: string): void {
if (this.value !== null) {
this.onChange(this.value);
}
if (value !== undefined && value !== null) {
this.value = value;
}
}
}
UPDATE
I have created a StackBlitz
Could you create StackBlitz post for better debugging?
I think part of the problem might be accessing the input rather than accessing the FormControl itself. Accessing the input itself directly then triggers the onChange event and marks the input as dirty which, in my opinion, might be a problem.
How are you using the directive? Would it be possible to do the following?
Create FormGroup in parent component
When using myFormInputFactory directive, pass appropriate FormControl reference to the directive and assign the value to the control itself:
this.formgroup.setValue({
key: value
},{emitEvent: false})
For what it's worth, I think the issue is because you call this.onChange within writeValue.
writeValue is used to notify the control that value has been changed externally by the form, so calling onChange to notify the form about the the change is needless.
Related
I have a custom slide toggle component created using Angular Material. I followed this guide: https://material.angular.io/guide/creating-a-custom-form-field-control
Everything seems to be working fine except when I dynamically disable the custom component like this:
<custom-slide-toggle
[toggleClass]="'enable_user'"
[value]="userFormGroup.get('activeUser').value"
formControlName="activeUser"
[toggleText]="'enable user'"
(selectionChange)="statusChange()"
[isChecked]="userFormGroup.get('activeUser').value"
[required]="false"
[disabled]="true"
></custom-slide-toggle>
The component is disabled but I get the console warning It looks like you're using the disabled attribute with a reactive form directive. ....
To solve it I tried setting disabled the recommended way like this: activeUser: new FormControl([{value:false, disabled: true}]) in the parent component but the custom component was not disabled.
I also tried the same thing but in the custom component itself but didn't have any effect on making the field disabled or not.
UPDATE:
I tried adding a formGroup binding to my custom component per #DKidwell suggestion but I still get the same warning It looks like you're using the disabled attribute.... I added the formGroup using FormBuilder to more closely match the Angular Material example.
UPDATE 2:
The solution I found was to create a custom directive in conjunction with adding the FormGroup binding per #DKidwell's answer. The custom directive I created was based on this post: https://netbasal.com/disabling-form-controls-when-working-with-reactive-forms-in-angular-549dd7b42110
I implemented the custom directive like so and removed the [disabled] decorator:
<custom-slide-toggle
[toggleClass]="'enable_user'"
[value]="userFormGroup.get('activeUser').value"
formControlName="activeUser"
[toggleText]="'enable user'"
(selectionChange)="statusChange()"
[isChecked]="userFormGroup.get('activeUser').value"
[required]="false"
[disableControl]="isEditable"
></custom-slide-toggle>
Here's my custom component typescript:
#Component({
selector: 'custom-slide-toggle',
templateUrl: './custom-slide-toggle.component.html',
styleUrls: ['./custom-slide-toggle.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: forwardRef(() => CustomSlideToggleComponent)
},
{
provide: MatFormFieldControl,
useExisting: CustomSlideToggleComponent
}
],
host: {
'[id]': 'id'
},
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CustomSlideToggleComponent implements OnInit, OnDestroy, DoCheck, ControlValueAccessor, MatFormFieldControl<boolean> {
private static nextId = 0;
private _placeholder: string;
private _disabled = false;
private _required = false;
private _readlonly = false;
public stateChanges = new Subject<void>();
public errorState = false;
public focused = false;
public ngControl: NgControl;
public toggleFormGroup: FormGroup;
#HostBinding() public id = `custom-slide-toggle-${CustomSlideToggleComponent.nextId++}`;
#HostBinding('class.floating')
get shouldLabelFloat() {
return this.focused || !this.empty;
}
#HostBinding('attr.aria-describedby') describedBy = '';
setDescribedByIds(ids: string[]) {
this.describedBy = ids.join(' ');
}
#Input() public toolTip: string = '';
#Input() public isChecked: boolean;
#Input() public toggleText: string = '';
#Input() public tabNumber: number = null;
#Input() public toggleId: string = '';
#Input() public toggleClass: string;
#Input()
public get disabled(): boolean {
return this._disabled;
}
public set disabled(value: boolean) {
console.log('set disabled trigged');
this._disabled = coerceBooleanProperty(value);
this._disabled ? this.toggleFormGroup.disable() : this.toggleFormGroup.enable();
this.stateChanges.next();
}
#Input()
public get required() {
return this._required;
}
public set required(req: boolean) {
this._required = coerceBooleanProperty(req);
this.stateChanges.next();
}
#Input()
public get readonly(): boolean {
return this._readlonly;
}
public set readonly(value: boolean) {
this._readlonly = coerceBooleanProperty(value);
this._readlonly ?
this.toggleFormGroup.get('toggleFormControl').disable() :
this.toggleFormGroup.get('toggleFormControl').enable();
this.stateChanges.next();
}
#Input()
public get value(): boolean {
let n = this.toggleFormGroup.value;
if (n.toggleFormControl !== null){
return n.toggleFormControl.value;
}
return null;
}
public set value(val: boolean) {
this.toggleFormGroup.setValue({toggleFormControl: val});
this.stateChanges.next();
this.onTouched();
}
#Input()
public get placeholder(): string {
return this._placeholder;
}
public set placeholder(value: string) {
this._placeholder = value;
this.stateChanges.next();
}
#Output() selectionChange: EventEmitter<boolean> = new EventEmitter<boolean>();
public constructor(private injector: Injector, fb: FormBuilder) {
this.toggleFormGroup = fb.group({
'toggleFormControl': ''
});
}
ngOnInit(): void {
this.ngControl = this.injector.get(NgControl);
if (this.ngControl != null) { this.ngControl.valueAccessor = this; }
}
ngOnDestroy(): void {
this.stateChanges.complete();
}
ngDoCheck(): void {
if(this.ngControl) {
this.errorState = this.ngControl.invalid && this.ngControl.touched;
this.stateChanges.next();
}
}
public toggleClick($event: MatSlideToggleChange) {
this.onChange($event.checked);
this.selectionChange.emit($event.checked);
}
public onChanged = (val: boolean) => {};
public onTouched = () => {};
writeValue(value: any) {
console.log('writeValue triggered, incoming value is: ' + value);
if (value !== this.inputControl.value) {
this.inputControl.setValue(value);
this.onChanged(value);
this.stateChanges.next();
}
}
get empty() {
if (this.inputControl?.pristine || this.inputControl?.untouched) return true;
else return false;
}
onBlur() {
this.onTouched();
}
onChange(val: boolean) {
this.writeValue(val);
}
public registerOnChange(fn: any): void {
this.onChange = fn;
}
public registerOnTouched(fn: any): void {
this.onTouched = fn;
}
public setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}
}
Here is my custom component's html:
<mat-slide-toggle
[formControl]="inputControl"
[id]="toggleId"
[class]="toggleClass"
color="primary"
labelPosition="after"
[checked]="isChecked"
[disabled]="disabled"
[required]="required"
(change)="toggleClick($event)"
[tabIndex]="tabNumber"
[matTooltip]="toolTip"
>{{toggleText}}</mat-slide-toggle>
How do I dynamically disable my custom component without getting the warning?
You need to add a formGroup binding to your custom component,
<div [formGroup]="yourFormGroup">
<mat-slide-toggle ...>
{{toggleText}}
</mat-slide-toggle>
</div>
You will also need to define that formGroup in your component,
FormGroup yourFormGroup = new FormGroup({
inputControl: new FormControl()
});
Once that is setup properly you shouldn't need to bind to the [disabled] property in your custom control's template.
There is such structure of components:
Desired Behavior
child1_component - is a header.
child2_component - is a body.
There is a button inside child1_component.
Clicking on that button I want to invoke a method inside child2_component.
Question
What is the best way to implement this?
One way to approach this would be to use a service with rxjs subjects and observables.
When the user clicks on the button in child1_component then it calls a method that in turn calls a method inside the shared service.
When the method in the service is called it can emit a value as an observable via a subject.
child2_component then subscribes to the observable within the shared service and can operate some logic based on when it receives data from the service.
More on services here: https://angular.io/tutorial/toh-pt4
Great tutorial on subjects and rxjs: https://blog.angulartraining.com/rxjs-subjects-a-tutorial-4dcce0e9637f
On your general.component.html :
<app-child1 (clicked)="app1Clicked($event)"></app-child1>
<app-child2 #child2></app-child2>
On your general.component.ts:
#ViewChild('child2', {static: true}) child2: Child2Component;
app1Clicked($event) {
this.child2.doSomething()
}
On the child1.components.ts:
#Output() clicked = new EventEmitter<any>();
onClick() {
this.clicked.emit();
}
Finally on the child2.component.ts:
doSomething() {
alert('ok');
}
There are 2 ways to do it:
1.Service:
export class ActionService {
private someAction = new Subject();
someActionEmitted$(): Observable<unknown> {
return this.someAction.asObservable();
}
emitSomeAction(): void {
this.someAction.next();
}
}
//childComponent1
export class ChildComponent1 {
constructor(private actionService: ActionService) {
}
emitAction(): void {
this.actionService.emitSomeAction();
}
}
//childComponent2
export class ChildComponent2 implements OnInit, OnDestroy {
private destroy$ = new Subject();
constructor(private actionService: ActionService) {
}
ngOnInit(): void {
this.actionService.someActionEmitted$()
.pipe(takeUntil(this.destroy$)) // dont forget to unsubscribe, can cause memory leaks
.subscribe(() => this.doSomething());
}
doSomething(): void {
// your logic here
}
ngOnDestroy(): void {
this.destroy$.next();
}
}
2. Using Parent Component
<child-component1 (btnClicked)="childComponentBtnClick()"></child-component1>
<child-component2 [clickBtnSubject]="childBtnClicked"></child-component1>
Ts logic:
export class ParentComponent {
childBtnClicked = new Subject();
childComponentBtnClick(): void {
this.childBtnClicked.next();
}
}
//childComponent1
export class ChildComponent1 {
#Output() btnClicked = new EventEmitter();
emitAction(): void {
this.btnClicked.emit(); // you can pass value to emit() method
}
}
//childComponent2
export class ChildComponent2 implements OnInit, OnDestroy {
#Input() clickBtnSubject: Subject;
ngOnInit(): void {
this.clickBtnSubject
.pipe(takeUntil(this.destroy$)) // dont forget to unsubscribe, can cause memory leaks
.subscribe(() => this.doSomething());
}
doSomething(): void {
// your logic here
}
ngOnDestroy(): void {
this.destroy$.next();
}
}
I have a custom <data-input-text> component which has two modes: regular and disabled. Here is the template (I've simplified it a bit for demo case):
<label *ngIf="!disabled"
class="field-label"
[ngClass]="{'focused' : isFocused, 'with-errors' : errors}">
<input class="field-value"
[type]="type"
[required]="required"
(focus)="onFocus()"
(blur)="onBlur()"
[(ngModel)]="value"
#fieldInput="ngModel">
</label>
<div class="field-label" *ngIf="disabled">
<span class="field-value">{{ value }}</span>
<span class="field-name">{{ label }}</span>
</div>
In the parent form, I use this component in the following way:
<form #profileForm="ngForm">
<data-text-input
label="lastName"
[required]="true"
[disabled]="userIsRegistered"
name="lastName"
ngModel></data-text-input>
</form>
userIsRegistered returns a boolean, which should switch between the input field or spans within the component. It all works fine until here.
I set the form in the parent component to match the BehaviorSubject like this:
this._sub = this.dl.selectedEmployee.subscribe( u => {
if ( u.id ) {
this.isLoading = false;
setTimeout( () => {
this.profileForm.setValue(u);
this.profileForm.control.markAsPristine();
}, 10);
}
});
Here is the custom ControlValueAccessor component:
import { Component, Input, ViewChild, forwardRef,
AfterViewInit, OnInit, OnChanges,
NgModule } from '#angular/core';
import { NG_VALUE_ACCESSOR, NG_VALIDATORS,
ControlValueAccessor, FormControl,
Validator, NgForm } from '#angular/forms';
#Component({
selector: 'data-text-input',
template: `
<label *ngIf="!disabled"
class="field-label">
<input class="field-value"
[type]="type"
[required]="required"
(blur)="onBlur()"
[(ngModel)]="value"
#fieldValue="ngModel">
<span class="field-name">{{ label }}</span>
</label>
<div class="field-label" *ngIf="disabled">
<span class="field-value">{{ value }}</span>
<span class="field-name">{{ label }}</span>
</div>
`,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef( ()=> DataTextInputComponent ),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef( ()=> DataTextInputComponent ),
multi: true
}
]
})
export class DataTextInputComponent implements OnChanges, ControlValueAccessor, Validator {
#Input() public disabled: boolean = false;
#Input() public label: string;
#Input() public required: boolean = false;
#Input() public type: string = 'text';
#ViewChild('fieldValue') public fieldValue: FormControl;
// infrastructure
public registerOnChange(fn: any) { this.propagateChange = fn; }
public registerOnTouched(fn: any) { this.propagateTouch = fn; }
private propagateChange = (_: any) => { };
private propagateTouch = (_: any) => { };
/**
* inner value
*/
private innerValue: any = null;
/**
* on changes hook
*/
public ngOnChanges(): void {
if ( this.disabled ) {
this.propagateChange(this.innerValue);
}
}
/**
* input events
*/
public onBlur(): void {
this.propagateChange(this.innerValue);
this.propagateTouch(true);
}
/**
* value accessor setter and getter
*/
public get value(): any {
return this.innerValue;
};
public set value(value: any) {
if ( value !== 'undefined' ) {
this.innerValue = value;
this.propagateChange(value);
this.propagateTouch(true);
}
}
/**
* value accessor implementation
* #param value
*/
public writeValue(value: any): void {
if (value !== this.innerValue) {
this.innerValue = value;
}
}
/**
* validation
* #param c
*/
public validate(c: FormControl) {
return this.errors = (this.disabled) ? null : this.customValidate(c);
}
private customValidate(c: FormControl): {} {
if ( c.touched ) {
// some validation logic which is not relevant here;
return null;
}
return null;
}
}
There are other components used in the form, too (like a color picker and a ng-select).
So the weird part is this. The form value is set alright. No errors. The values are displayed correctly (for both, disabled and !disabled) within data-text-input components, as well as other components in the form). The weird part is that when I inspect the this.profileForm object with the debugger, the controls property has all of the controls with their respective values, but the value property of the form misses those, where disabled property (aka no input field) is set to true.
Here is the Plunker: https://plnkr.co/edit/nbWQZzQjhGae622CanGa?p=preview
Any ideas?
Well, this was not obvious, until I have traced down the way of setting of a value down to AbstractControl.prototype.updateValueAndValidity and it turned out, that using the variable name disabled was a bad idea here:
<form #profileForm="ngForm">
<data-text-input
label="lastName"
[required]="true"
[disabled]="userIsRegistered"
name="lastName"
ngModel></data-text-input>
</form>
I have renamed the disabled property to isReadOnly – 'cause readonly is also an attribute which might be checked elsewhere and also a TypeScript interface – and, tada, it works.
I'm developing an Angular 2 SPA. My application is composed by:
One component
One directive
I've builded one directive that format text input using onfocus and onblur events. On focus event remove dots to text value, on blur event add thousand dots to text value.
Following component's code:
<div>
<input id="input" [(ngModel)]="numero" InputNumber />
</div>
Following component's TypeScript code:
import { Component } from '#angular/core';
#Component({
selector: 'counter',
templateUrl: './counter.component.html'
})
export class CounterComponent {
numero: number;
public incrementCounter() {
}
ngOnInit() {
this.numero = 100100100;
}
}
Following directive's TypeScript code:
import { Directive, HostListener, ElementRef, OnInit } from "#angular/core";
#Directive({ selector: "[InputNumber]" })
export class InputNumber implements OnInit, OnChanges {
private el: HTMLInputElement;
constructor(private elementRef: ElementRef) {
this.el = this.elementRef.nativeElement;
}
ngOnInit(): void {
// this.el.value is empty
console.log("Init " + this.el.value);
this.el.value = this.numberWithCommas(this.el.value);
}
ngOnChanges(changes: any): void {
// OnChanging value this code is not executed...
console.log("Change " + this.el.value);
this.el.value = this.numberWithCommas(this.el.value);
}
#HostListener("focus", ["$event.target.value"])
onFocus(value: string) {
this.el.value = this.replaceAll(value, ".", "");
}
#HostListener("blur", ["$event.target.value"])
onBlur(value: string) {
this.el.value = this.numberWithCommas(value);
}
private numberWithCommas(x) {
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ".");
}
private escapeRegExp(str) {
return str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1");
}
private replaceAll(str, find, replace) {
return str.replace(new RegExp(this.escapeRegExp(find), 'g'), replace);
}
}
The following code works except that I need lost focus for show my number like "100.100.100". How can I perform this action on init data loading?
I add one example at this link: Plnkr example
Thanks
You can do this by using a Pipe which takes a boolean parameter that represents your focus/no focus action.
import { Pipe, PipeTransform } from '#angular/core';
#Pipe({name: 'dots'})
export class DotsPipe implements PipeTransform {
transform(value: number, hasFocus:boolean): any {
if(hasFocus){
return value.toString().replace(/\./g,'');
}else{
return value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ".");
}
}
}
Then you have to apply the Pipe on your [ngModel] and use Angular events (focus) and (focusout) to change your variable.
<input [ngModel]="numero | dots : hasFocus" (focus)="hasFocus=true" (focusout)="hasFocus=false" (ngModelChange)="numero=$event" />
I think that your directive should implement ControlValueAccessor interface https://angular.io/docs/ts/latest/api/forms/index/ControlValueAccessor-interface.html
It is needed for writing model in your directive. ControlValueAccessor interface has writeValue(value: any) method that will be initially called.
So your writeValue method will be something like this:
private onChange: (_: any) => {};
...
writeValue(val) {
const editedValue = this.numberWithCommas(val);
this._onChange(val);
}
registerOnChange(fn: any) : void {
this._onChange = fn;
}
I do want to create a custom control which does not include any input. Whenever the control changes, I do want to save the complete form.
Our current approach uses the form-changed-event like this:
<form #demoForm="ngForm" (change)="onChange()">
<custom-input name="someValue" [(ngModel)]="dataModel">
</custom-input>
</form>
As you can see, we use the "change"-event to react to any change in the form.
This works fine as long as we have inputs, checkboxes, ... as controls.
But our custom control does only exist out of a simple div we can click on. Whenever I click on the div the value of the control is increased by 1. But the "change"-event of the form is not fired. Do I somehow have to link my custom control to the form? Or are there any events which need to be fired?
import { Component, forwardRef } from '#angular/core';
import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '#angular/forms';
#Component({
selector: 'custom-input',
template: `<div (click)="update()">Click</div>`,
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CustomInputComponent),
multi: true
}]
})
export class CustomInputComponent implements ControlValueAccessor {
private onTouchedCallback: () => void = () => {};
private onChangeCallback: (_: any) => void = () => {};
update(){
this.value++;
}
get value(): any {
return this.innerValue;
};
set value(v: any) {
console.log("Change to");
if (v !== this.innerValue) {
this.innerValue = v;
this.onChangeCallback(v);
}
}
writeValue(value: any) {
if (value !== this.innerValue) {
this.innerValue = value;
}
}
registerOnChange(fn: any) {
this.onChangeCallback = fn;
}
registerOnTouched(fn: any) {
this.onTouchedCallback = fn;
}
}
I've created a plunker to demonstrate the problem:
https://plnkr.co/edit/ushMfJfcmIlfP2U1EW6A
Whenever you click on "Click" the model-value is increased, but there is no output on the console, as the change-event is not fired... (There is a console.log linked to the change-event)
Thanks for your replies.
Finally I found the following solution to this problem:
As Claies mentioned in the comment, my custom component does not fire the change event. Therfore the form does never know about the change. This has nothing todo with angular, but as said is the expected behaviour of a input/form.
The easiest solution is to fire the change-event in the customcontrol when a change happens:
constructor(private element: ElementRef, private renderer: Renderer) {
}
public triggerChanged(){
let event = new CustomEvent('change', {bubbles: true});
this.renderer.invokeElementMethod(this.element.nativeElement, 'dispatchEvent', [event]);
}
That's it, whenever I called "onControlChange(..)" in my custom component, then I fire this event afterward.
Be aware, that you need the Custom-Event-Polyfill to support IE!
https://www.npmjs.com/package/custom-event-polyfill
You need to emit the click event of div to its parent. so that you can handle the event.
Plunker Link
Parent component:
import { Component, forwardRef, Output, EventEmitter } from '#angular/core'; // add output and eventEmitter
import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '#angular/forms';
#Component({
selector: 'custom-input',
template: `<div (click)="update($event)">Click</div>`,
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CustomInputComponent),
multi: true
}]
})
export class CustomInputComponent implements ControlValueAccessor {
private onTouchedCallback: () => void = () => {};
private onChangeCallback: (_: any) => void = () => {};
#Output() clickEvent = new EventEmitter(); // add this
update(event){
this.value++;
this.clickEvent.emit(event); // emit the event on click event
}
get value(): any {
return this.innerValue;
};
}
child component:
//our root app component
import {Component} from '#angular/core'
#Component({
selector: 'demo-app',
template: `
<p><span class="boldspan">Model data:</span> {{dataModel}}</p>
<form #demoForm="ngForm">
<custom-input name="someValue"
[(ngModel)]="dataModel" (clickEvent) = onChange()> // handling emitted event here
Write in this wrapper control:
</custom-input>
</form>`
})
export class AppComponent {
dataModel: string = '';
public onChange(){
console.log("onChangeCalled");
}
}
Thanks Stefan for pointing me in the right direction.
Unfortuantely Renderer (which has invokeElementMethod()) has recently been deprecated in favor or Renderer2 (which does not have that method)
So the following worked for me
this.elementRef.nativeElement.dispatchEvent(new CustomEvent('change', { bubbles: true }));
It seems that change event is not fired on form when you call ControlValueAccessor onChange callback (callback passed in registerOnChange function), but valueChanges observable (on the whole form) is triggered.
Instead of:
...
<form (change)="onChange()">
...
you can try to use:
this.form.valueChanges
.subscribe((formValues) => {
...
});
Of course, you must get proper form reference in your component.