Angular: Call markAsDirty() in Custom Input Component from NgForm - javascript

I implemented a custom component which is a wrapper for an input with NgModel. I connected them with a ControlValueAccessor. It works well, I can easily access values from my parent component.
But if I try to call markAsDirty() the touched flag is only changing on my component, it has no effect to my input inside the component. I will give you an example:
// Parent Component
onSubmit(form: NgForm) {
this.form.controls.registerEmail.markAsDirty();
}
// Thats how the component looks like in my form:
<form #form="ngForm" (ngSubmit)="onSubmit(form)" [ngClass]="{'has-error': !form.valid}">
<form-text label="E-Mail" name="registerEmail" email required placeholder="" [(ngModel)]="eMail"></form-text>
</form>
// Result
<form-text label="E-Mail" name="registerEmail" class="ng-untouched ng-invalid ng-dirty">
<label for="form-text-2">E-Mail</label>
<input class="input-control invalid ng-untouched ng-pristine ng-invalid" type="text" id="form-text-2">
</form-text>
As you can see the form-text has the "ng-dirty" class, the input inside remains pristine.
To implement my custom component I used one the many instructions you find on the web. Here is the one I used: angular2 custom form control with validation json input
I want to mark every input field as dirty when the submit button is pressed. Because my validation shows up, when you blur the input.
I figured out that there is the problem my component inherits from ControlValueAccessor. The only connection between my component and my NgForm is over its NgModel. The NgForm can use my component as FormControl because it has its own NgModel. Over events it's possible to pass values in two directions. But it's not possible with methods like markAsDirty() or markAsTouched(). Inside the component it's no problem. But my NgForm has no real access to components. Only to NgModel.
Is there any way to implement that? I thought it's not that hard to figure it out, but I am struggling for a long time with that. My only solution for the moment is to iterate over every input with jQuery to fire a focus. There must be a cleaner solution for that.
Thx

You can pass as input in the component which implements ControlValueAccessor the dirty property of the form and then update the state of your inner input using ReactiveFormsModule and FormControl.
The component which holds your form:
<form #myForm="ngForm" (submit)="onSubmit($event)">
<my-input name="my-input" [(ngModel)]="myInput" [formDirty]="myForm.dirty"></my-input>
<button type="submit">Submit</button>
</form>
Then in the component which implements ControlValueAccessor:
ngOnChanges({ formDirty }: SimpleChanges) {
if (formDirty.currentValue) {
this.inputCtrl.markAsDirty();
} else {
this.inputCtrl.markAsPristine();
}
}
Here you can find the relevant snippet.

You need to call onTouched() (this._onTouchedCallback) from inside your component when you want the controls status set to touched. Same for this._onChangeCallback
For example by adding (ngModelChange)="onTouched(value)" to the input tag in my-custom-input
Copied from:
https://github.com/angular/angular/issues/10151

The problem is that there is no easy way to get any notification when dirty state changes. See https://github.com/angular/angular/issues/10887.
Helper directive:
#Directive({
selector: 'my-input'
})
export class MagickDirective implements DoCheck {
constructor(private control:NgModel){
}
ngDoCheck(): void {
//you can do whatever you want
if(this.control.dirty) {
(this.control.valueAccessor as MyInputComponent).setDirty(true);
}
}
}

Related

How can I pass a reference to an array field to a child component, so I can connect it with an input field through two-way binding?

I'm currently implementing a form with an undo-function in Angular and I can't figure out how to connect the #Input values of the child component to fields of an array in a seperate 'dataService'. Initially I just implemented the whole form in one component with Angular's FormGroup class which worked fine. Whenever I pressed the undo button the array would be set to a previous state and the changes would be reflected in the input fields. I then decided it would be cleaner and more future proof to move the inputs to a seperate child component, so I could customize them better. The problem is now, that the array field that I pass as an Input to the child component is always passed as a primitive string and looses its connection to the actual array. How can I pass it in a way that I can connect it to the inputs value via two-way data binding so the changes in the input get reflected in the array and vice versa?
The code looks like the following:
Parent template:
...
<input-field id="name" name="name" [(value)]="dataService.projectData.info.projectDetails.name"></input-field>
...
Child component:
...
export class InputFieldComponent implements OnInit {
#Input() value: any;
#Output() valueChange: EventEmitter<any> = new EventEmitter<any>();
#Input() id: string;
#Input() name: string;
#Input() class: string;
...
onBlur() {
this.valueChange.emit(this.value);
}
Child template:
...
<input [(ngModel)]="value" (blur)="onBlur()" type="text" id="{{id}}" class="{{class}}">
...
The way it behaves right now is that the default string value from the array is passed to the child component and displayed in the input field. When I type something in the input only the 'value' attribute inside the child component is being changed (probably because it's not bound to the actual array). The content in the array stays the same and the undo function doesn't work.
The way I would want it is that once the input field looses focus (onBlur) the value in the array is updated and once I click on 'undo' the changes in the array get reflected in the input field.
Thanks in advance for any suggestions!
Angular version: 14.0.7

Angular state not updating until click anywhere

Very new to Angular, and just trying to get a feel for it. I have an input component:
import { Component } from '#angular/core';
#Component({
selector: 'app-input',
templateUrl: './input.component.html',
})
export class InputComponent {
q = '';
queryChange(value: string) {
this.q = value;
}
}
It's html:
<div>
<input #query type="text" (change)="queryChange(query.value)" />
<button>Search</button>
<div>{{ q }}</div>
</div>
When I type into the input, the {{ q }} doesn't update until I click anywhere on the screen, or hit enter. Almost like refocusing. Coming from React I'm confused as to why this happens with the Angular's (change) rather than updating as I type.
My first thought was that maybe because I'm passing the value of the input to queryChange(query.value) instead of passing the event value like I would usually do in React.
I think the problem is about the DOM and not Angular. You should use (input) instead of (change) if you want the event to trigger every time you type.
<input #query type="text" (input)="queryChange(query.value)" />
See this StackBlitz, as well as change and input MDN references. Specifically, MDN says about change:
Unlike the input event, the change event is not necessarily fired for each alteration to an element's value.

Angular 4 Readonly input change detection

I have following code with readonly input. am trying to change the value if this readonly input from TS File, but, I am unable to detect change from any function. Below is the example.
<input type="text" class="form-control" id="field1" name="field1" readonly [(ngModel)]="field1" (ngModelChange)="onChange()">
ngModelChange is not working in this senario.
You can try to use the (change) instead of (ngModelChange).
Here is some explanation to help you understand this.
Use input like this:
(input)="handle($event)"
And Handle event in your typescript accordingly.
FormControl to the rescue!!
FormControls and Reactive forms are great and it will save your day! :)
Mark ReactiveFormsModule in your app.module, import FormControl to your component aaand... For demo I used a simple timeout to change the value to trigger the valueChanges:
myControl = new FormControl('')
constructor() { }
ngOnInit() {
setTimeout(() => {
this.myControl.setValue('something');
}, 2000);
this.myControl.valueChanges.subscribe((data) => {
console.log('changed!')
});
}
And template:
<input type="text" readonly [formControl]="myControl">
StackBlitz
P.S if this is input is part of a template driven form, this wouldn't work, since ngModel and formControl shouldn't be used together. So you would be notified of that. So in that case I suggest you go reactive way ;)

Angular: How to force uppercase in an input field

I'm working with angular (typescript) and I have a modelform in html where the user has to insert a code field and a description field.
The code field must always be entered by the user, always in uppercase.
I found and followed this question:
How to convert input value to uppercase in angular 2 (value passing to ngControl)
but the last letter that the user inserts remains lowercase however ..
The fundamental thing is that the database always comes in all uppercase (I also put a limit of 4 characters that works properly)
this is my code now, but as written above it does not work properly:
<input type="text" id="code" #code class="form-control" formControlName="code" maxlength="4"
(input)="code.value=$event.target.value.toUpperCase()">
has anyone found a quick, functional and fast solution?
thank you!
You can simply add oninput="this.value = this.value.toUpperCase()" in your <input> tag and it will instantly convert any input in your input field to Uppercase.
If you're working with ngModel, you can use ngModelChange to do it this way with JavaScript's .ToUpperCase().
<input [ngModel]="person.Name" (ngModelChange)="person.Name = $event.toUpperCase()">
Based on #Ali Heikal answer it should be:
<input #code formControlName="code" (input)="code.value = code.value.toUpperCase()" type="text">
Or use .toLocaleUpperCase() if necessary.
This worked for me. I just used keyup and whenever a user adds a character it automatically converts it to Uppercase, not just in the UI but in the model as well.
(keyup)="form.patchValue({name: $event.target.value.toUpperCase()})"
A bit late, but I'm surprised to don't see the css alternative.
Add on the input field:
.input-upper{
text-transform: uppercase;
}
You could create an Angular Directive which transforms all user input to uppercase.
The Directive extends DefaultValueAccessor:
#Directive({
selector: 'input[toUppercase]',
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: forwardRef(() => UpperCaseInputDirective),
},
],
})
export class UpperCaseInputDirective extends DefaultValueAccessor {
#HostListener('input', ['$event']) input($event: InputEvent) {
const target = $event.target as HTMLInputElement;
const start = target.selectionStart;
target.value = target.value.toUpperCase();
target.setSelectionRange(start, start);
this.onChange(target.value);
}
constructor(renderer: Renderer2, elementRef: ElementRef) {
super(renderer, elementRef, false);
}
}
Use the Directive like this on the input:
<input name="myTdfInput" type="text" [(ngModel)]="value" toUppercase>
The solution works in both Reactive Forms and Template Driven Forms.
See working demo on Stackblitz: https://stackblitz.com/edit/angular-input-field-to-accept-only-numbers-czivy3?file=src/app/to-uppercase.directive.ts
Credits for the solution go to: https://twitter.com/BartBurgov (See his tweet here: https://twitter.com/BartBurgov/status/1582394428748009477?s=20&t=RiefRcZ1B8IF42wp2rWzaQ)
You can use a pattern attribute to limit the allowed input. You may also want to assist the user by upper-casing their input.
<input type="text" pattern="[A-Z]*" name="example" />

How to get LEVEL 4's select element's values in LEVEL 3's form submit?

The actual complex code is abstracted to make it more readable.
In our Angular 2 project, we have got a component <top-component> (LEVEL 1) like this:
<top-component>
</top-component>
Which has the following template : <some-form> (LEVEL 2):
<some-form>
</some-form>
Which has the following template (LEVEL 3):
<form #f="ngForm" (submit)="handleFormSubmit(f)" >
<input name="Label" value="Label" />
<input name="Value" value="Value" />
<some-select></some-select>
<button> Cancel </button>
<button> Save </button>
</form>
The template of <some-select> (LEVEL 4) is:
<select name="selectData" ngDefaultControl [(ngModel)]="selectData">
<option *ngFor="let option of options" [ngValue]="option.value">{{option.label}}</option>
</select>
The problem is that when we submit #f="ngForm" to handleFormSubmit(f),
the f.value values does not have the values from the some-select's select element.
I fixed this by using a shared service.
Long story short:
Create a component for your form element (select, in this case). (you already have this)
Create a shared service between the form's component and the form fields components.
Inside your formfield component, on model change, you send the new model to the service.
In your form component, you subscribe to any changes on the form fields models and you update all the models.
On submit, you will have an object with the models of your form fields components that you updated each thanks to the shared service that was called by each one of the form fields components.
With this, you can make it work. And you can have a very dynamic form.
If you need an example tell me and when I can I will make you a simple example here.
Hope it helped! Cheers!
Update:
Let's go from child to parent here.
SelectField Component:
#Component({
selector: 'select-field',
template: `
<select class="col-md-12 form-control input-xs" name="ibos-newsletter" [(ngModel)]="fieldset.value" (ngModelChange)="onChange($event)">
<option *ngFor="let option of options" [ngValue]="option.id"
[selected]="option.id === condition">{{option.name}}
</option>
</select>
`
})
export class SelectField implements OnInit {
private fieldset = {};
private options = <any>[];
private fieldName = 'SelectField';
constructor(private sharedService: SharedService) {
// we get data from our shared service. Maybe the initial data is gathered and set up in the Form service.
// So if the Form service just calls this method we are subscribed and we get the data here
this.sharedService.updateFieldsetsListener$.subscribe((fieldset) => {
if (fieldset.field=== this.fieldName) {
this.selectModel = fieldset;
}
}
);
}
ngOnInit() {
}
onChange() {
// fieldset.value (our model) is automatically updated, because this is binded with (ngModelChange). This means that we are sending an updated fieldset object
this.sharedService.updateFieldsets(this.fieldset);
}
}
SharedService service:
#Injectable()
export class SharedService {
updateFieldsetsListener$: Observable<any>;
private updateFieldsetsSubject = new Subject<any>();
constructor(private jsonApiService: JsonApiService) {
this.updateFieldsetsListener$ = this.updateFieldsetsSubject .asObservable();
}
updateFieldsets(fieldset) {
this.updateFieldsetsSubject .next(fieldset);
}
}
Form Component:
Inside your constructor you should have the subscribe:
this.sharedService.updateFieldsetsListener$.subscribe((fieldset) => {
// Here you have a full object of your field set. In this case the select.
// You can add it to a form object that contains all the fieldsets objects...
// You can use these formfields objects (instead of sending / receiving only the model) to dynamically: select an option, disable an input, etc...
}
);
Personal thoughts:
I like this approach because you can manage locally each element of your form. Becomes handy when you have multiselect external libraries, datepickers, etc...
I send / receive objects and not just the model because through objects I have the change to decide dynamically de behavior of the element. Imagine you get data from DB in your Form's component, you loop it and invoke the SharedService method to update the fields... They will show up like you tell them to: highlighted, selected, disabled, etc...
Eventually, if you render dynamically each element of the form... You will have a form that has the potential to be 100% dynamic and abstract. I managed to make it and now I just have a vector that says which elements of the form have to be rendered and how.

Categories