34

I have an input field mapped to an entity in my controller with a ngModel 2-way binding:

<input type="text" [(ngModel)]="entity.one_attribute" />

When I initialize my controller, I have this entity:

{ one_attribute: null }

If a user starts to fill in the field but does not submit the form immediately and empties the field, my entity is updated to become:

{ one_attribute: "" }

Is it possible to define that empty string should be changed to null automatically?

2
  • I don't think there is anything that would do this automatically. You could however split ngModel into separate bindings for value property and input event and simply set the model to null if value is empty string. Commented Jul 22, 2016 at 13:59
  • 1
    What about create a component that internally uses the input, and implement the logic inside it? That way you wouldn't need to write it every time. Commented Jul 22, 2016 at 14:56

7 Answers 7

31

After viewing a bunch of answers about ValueAccessor and HostListener solutions, I made a working solution (tested with RC1):

import {NgControl} from "@angular/common";
import {Directive, ElementRef, HostListener} from "@angular/core";

@Directive({
  selector: 'input[nullValue]'
})
export class NullDefaultValueDirective {
  constructor(private el: ElementRef, private control: NgControl) {}

  @HostListener('input', ['$event.target'])
  onEvent(target: HTMLInputElement){
    this.control.viewToModelUpdate((target.value === '') ? null : target.value);
  }
}

Then use it that way on your input fields:

<input [(ngModel)]="bindedValue" nullValue/>
Sign up to request clarification or add additional context in comments.

4 Comments

What would an equivalent be for reactive forms where we are not supposed to use ngModel?
@miso did you find a solution yet?
@miso export class NullDefaultValueDirectiveDirective { constructor() { } @Output('EmptyToNull') response = new EventEmitter<string>(); @HostListener('keyup', ['$event.target']) onEvent(target: HTMLInputElement) { this.response.emit(target.value === '' ? null : target.value); } }
For me this didn't work with controls managed by FormControl. If, like me, you need a solution which works for all input elements, then I would recommend to check the answer from @user1928596. That worked like a charm in my case.
10

There is a way to fix this for all your components that doesn't conflict with other ValueAccessor and doesn't require adding a directive everywhere. TLDR: add this to your AppModule:

    DefaultValueAccessor.prototype.registerOnChange = function (fn: (_: string | null) => void): void {
      this.onChange = (value: string | null) => {
        fn(value === '' ? null : value);
      };
    };

To summarize, this is a known issue is angular that reported here: https://github.com/angular/angular/issues/45317 And can't be fixed unless you use this little hack because of much older issue explained here: https://github.com/angular/angular/issues/3009 That prevent overriding the default behavior of angular globally. Hope it helps. The answer from @taylor-buchanan put me on the right track

1 Comment

This solution is just perfect. Using the directive was working only on elements using ngModel in my case and couldn't find a way to make it work with reactive-forms too. Also, the directive had to be set on every element. With this simple solution it just works! Thanks!
5

I just formed this solution after much research. It's kind of hacky since the Angular team doesn't recommend extending DefaultValueAccessor, but it automagically works for every input without having to mark each one individually.

import { Directive, forwardRef, Renderer2, ElementRef, Input } from '@angular/core';
import { DefaultValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';

export const VALUE_ACCESSOR: any = {
    provide: NG_VALUE_ACCESSOR,
    useExisting: forwardRef(() => InputExtensionValueAccessor),
    multi: true
};

@Directive({
    selector: 'input:not([type]),input[type=text],input[type=password],input[type=email],input[type=tel],textarea',
    host: {
        '(input)': '_handleInput($event.target.value)',
        '(blur)': 'onTouched()',
        '(compositionstart)': '_compositionStart()',
        '(compositionend)': '_compositionEnd($event.target.value)'
    },
    providers: [VALUE_ACCESSOR]
})
export class InputExtensionValueAccessor extends DefaultValueAccessor  {
    // HACK: Allows use of DefaultValueAccessor as base
    // (https://github.com/angular/angular/issues/9146)
    static decorators = null;

    constructor(renderer: Renderer2, elementRef: ElementRef) {
        super(renderer, elementRef, (null as any));
    }

    registerOnChange(fn: (_: any) => void): void {
        super.registerOnChange(value => {
            // Convert empty string from the view to null for the model
            fn(value === '' ? null : value);
        });
    }
}

This is working great for me on Angular 4.4.5.

5 Comments

This is not needed as of Angular 6. Can anyone confirm please?
@aycanadal: Why it's not needed in Angular 6?
@AlexanderAbakumov because angular 6 sets ngModel bound field to null for empty string values or whatever the reason.
@aycanadal I don't think this is true. Check this. It's still set to an empty string in Angular 6.
This does not work with FormControl, as it throws error : More than one custom value accessor matches form control [...]
2

Although in the accepted answer @jobou solution makes sense, I think still need to remember to add 'nullValue' to every field, which is a quite big overhead.

I went for a different direction and checked the data in Interceptor service.

intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
  ...
   request = this.nullValueService.setNullValues();
  ...
}

I have created a service which check every value in the body of the request, and if found one which is an empty string it sets to Null.

@Injectable()
export class NullValueService {
    ...
    setNullValues(request) {
        for (let key in request.body) {
            if (request.body[key] === '') {
                request.body[key] = null;
            }
        }
    }

}

This solution works fine with my backend and I think should be alright for most of situations.

Comments

0

All of this value accessor stuff makes my head spin.

Instead I extended FormControl with setValue overriden:

export class StringFormControl extends FormControl
{
    constructor(formState?: any, validatorOrOpts?: ValidatorFn | ValidatorFn[] | AbstractControlOptions | null, asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[] | null)
    {
        super(formState, validatorOrOpts, asyncValidator)
    }

    setValue(value: any, options?: {
        onlySelf?: boolean;
        emitEvent?: boolean;
        emitModelToViewChange?: boolean;
        emitViewToModelChange?: boolean;
    })
    {
        if (value === '') 
        { 
            value = null;
        }
        
        super.setValue(value, options)
    }
}

The signatures are just copied from the FormControl class.

This won't work with FormBuilder, but if you create your form with new FormControl() you can use new StringFormControl() instead.

At this time I don't see a need to add patchValue too, but you could if you want to be complete. You could also add a config option to enable the feature if needed.

Comments

0

Note: This does work for number fields with a caveat

Currently the NumberValueAccessor (source) sets the value to null when the field is empty.

However, be cautious of this issue https://github.com/angular/angular/issues/55002

If you happen to be setting the input type dynamically:

<input [type]="'number'" />

instead of

<input type="number" />

then the NumberValueAccessor won't get triggered (because it can't match the selector statically at compile time) so the DefaultValueAccessor will apply. In fact in that case ALL numbers will get set as strings to your ngModel / formControl

Comments

-5

I have done it globally. But its not 100%. I could not find the method where angular 4 call JSON.stringify on the body. I'm hoping someone could help out here. Until the new HttpClient in 4.3 is out I continue to use a wrapper class for the Http service. I do this because no interceptors has been present in Angular2 and forward. My wrapper looks something like this.

@Injectable()
export class MyDao {
constructor(private http: Http) {
}
public get(options?:MyRequestOptions){...}
public post(url:string,body,options?:MyRequestOptions){
   let reqOptions = new BaseRequestOptions();
   reqOptions.url = url;
   reqOptions.params= this.processParams(options);
   reqOptions.header= this.processHeaders(options); //ex. add global headers
   reqOptions.body=this.removeEmptyStringsInBody(body);
   this.http.post(options);
}

So far so good. But I have not found any good transformRequest as in AngularJS, so until i find it I have implemented transformEmptyStringAsNull as this:

 private removeEmptyStringsInBody(body:any) {
    let bodyTemp= JSON.stringify(body, function (key, value) {
        return value === "" ? null : value
    });
    return JSON.parse(bodyTemp);
}

I know it's ugly in the way that I will do an extra stringify back to parse again. But I don't need to do anything in the rest of the application.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.