Ludevik
Ludevik

Reputation: 7264

writeValue with invalid value in custom form control

When I want to create custom form control in Angular I need to implement the ControlValueAccessor which defines writeValue(value:any): void function.

Now imagine that the custom component only accepts strings which contains only numbers because of its inner working. It cannot accept nothing else as it cannot do anything meaningful with it.

Questions when someone calls formControl.setValue() with invalid value:

Upvotes: 5

Views: 5040

Answers (4)

Darren Ruane
Darren Ruane

Reputation: 2505

Given that the ControlValueAccessor component in question requires that its value be a string with the conditions you mentioned, I fail to see how anything other than throwing an exception would be an acceptable response in the case of invalid input.

If somebody is calling formControl.SetValue() on your FormControl and their input is invalid, they should be informed immediately. Setting the control's value to null or otherwise will only hide the issue and potentially cause new issues in the future (and they'll likely be hard to track down).

At the end of the day, the only people who can call this method are the developers working with the FormControl, unless you are creating some kind of public API, in which case I suggest providing a strongly-typed 'wrapper' method.

As for your second question of:

Should writeValue call onChange callback with null in case of invalid input, so that Angular knows that the value was not accepted?

Well, firstly I would recommend going with my suggestion and not setting the value at all in this exceptional case. But just as a side note, you should not be calling the onChange callback inside of writeValue at all.

WriteValue is the method that Angular calls when you programmatically set the FormControl value through formControl.SetValue(). In other words, it already knows the value has changed, because it is the cause of that change.

You should be calling the onChange callback when you set the value through your ControlValueAccessor so that the Angular FormControl can be aware of this change and update its internal value. This should not be happening in the writeValue method though, as again, it is the method that Angular will call, not you.

I suspect, based on the wording of your question, that you knew this already though. But there is one other reason why you shouldn't that you might not be aware of:

Invoking the onChange callback inside of writeValue will not even make a difference, nor will it do what you think it will. Here is an explanation for how Angular Forms setup the bindings to/from FormControls and ControlValueAccessors.

You'll see from the link that the onChange callback simply calls formControl.setValue() except with an emitModelToViewChange: false flag. This flag is used inside of the formControl.setValue() method, and when it is false (as it is in this case) no subscribers to onChange will be notified.

Upvotes: 1

waterplea
waterplea

Reputation: 3661

My thoughts on the matter is you should not alter the form control value in your ControlValueAccessor if the value that came from control is invalid. The idea for ControlValueAccessor and custom form control is:

  1. Provide a way for a user to see the value from control
  2. Provide a way for a user to write value into control

If you change control value on your own, without user input, that goes against the paradigm of the thing.

Here's how I would do it:

  1. I would provide a fallback value to be used in calculations/displayed if value is incorrect, so that there will be no errors trying to manipulate the wrong value (cannot read X of null/undefined etc. — in case you would need it for more complex types of data).

  2. I would write a function that checks if value in control is compliant to the restrictions of control and if not — display an error in console, perhaps with console.assert as it is kind of like breaking the API contract of you custom control.

Here's my implementation of combobox in a nutshell (omitted a lot and simplified it, of course):

@Input()
items: ReadonlyArray<T>;

@Input()
stringify = (item: T) => !!item
 ? item.toString()
 : ''; // <- or any other conversion to string

@Input()
search: string

@Output()
searchChange = new EventEmitter<string>();

onNestedModelChange(search: string) {
  this.updateSearch(search);
}

onSelect(item: T) {
  this.onChange(item);
  this.updateSearch(this.stringify(item));
}

writeValue(value: T) {
  this.updateSearch(this.stringify(value));
}

private updateSearch(search: string) {
  this.search = search;
  this.searchChange.emit(search);
}

Template:

<input [ngModel]="search" (ngModelChange)="onNestedModelChange($event)">
<div class="dropdown">
  <div *ngFor="let item of items | filterPipe: search"
       class="option"
       (click)="onSelect(item)">
    {{item}}
  </div>
</div>

Usage:

<combobox
  [formControl]="control" 
  [items]="items$ | async"
  (searchChange)="queryNewItems($event)"
></combobox>

This way you have a separate string input search which you use to fill nested input tag, when you write to that input — you filter all available items using some filtering pipe (say stringify(item).indexOf(search)). You also emit what you write through searchChange so you can use this input to request new items from the server if you have a lot of them and prefer to filter them on the backend.

Upvotes: 2

T04435
T04435

Reputation: 14022

You can set a pattern validator, then angular will know that the value is invalid when the pattern does not match.

const numberOnlyControl = new FormControl('', Validators.pattern('[0-9]+'))

Upvotes: 0

Mino
Mino

Reputation: 994

I had similar problem. I my case I tried to set not-existing value to our select component.

I looked how native select.value attribute and jQuery's val function deals with this problem:

It turns out that: el.value returns '' and jEl.val() returns null. (https://codepen.io/mino123456/pen/VRGwYx)

So I think value should return null.

As @Ludevik points out, problem is that calling onChange in writeValue will set value to null, but it will also trigger valueChange event. This is undesirable, especially in:

formControl.writeValue('invalidValue', {emitEvent:false});

I looked in source code, and I found way to set value without triggering event. But I must say, that it feels really hacked.

ngOnInit(): any {
  this._boundFormControl = this._injector.get(NgControl, null);
}

setControlValue(value: any): void {
  const dir = this._boundFormControl as any;
  const control = dir._parent.form.get(dir.path);
  control.setValue(value, {
    emitModelToViewChange: false,
    emitEvent: false
  });
}

Basic idea is get formControl from parent, and call setValue with emitEvent: false. I would love if angular would provide some better way.

Upvotes: 2

Related Questions