83

On one of my pages I use a FormBuilder to fill a form at initialization. Every input gets a class whenever the input is not empty. This is done by adding a ngClass to every input, and subscribing on the FormGroup's valueChanges.

My problem occurs whenever the form is filled programmatically. Whenever a user changes any value on the form, valueChanges gets called, however, this is not the case when using a FormBuilder.

My question is: How to get the valueChanges event to be fired whenever the FormBuilder is finished.

I have tried using this.FormGroup.updateValueAndValidity(), but this did not result in anything.

4 Answers 4

112

I found a solution which worked for me. I forgot two parameters in the updateValueAndValidity function.

  • onlySelf: will only update this FormControl when true.
  • emitEvent: will cause valueChanges event to be fired when true.

So as result you will get something like: FormGroup.updateValueAndValidity({ onlySelf: false, emitEvent: true });

Sign up to request clarification or add additional context in comments.

4 Comments

I always forget what onlySelf does. An AbstractControl of which FormControl is a subclass has a parent property. And from the docs If onlySelf is true, this change will only affect the validation of this FormControl and not its parent component. This defaults to false. So if you have an event handler at 'this' level of your Form hierarchy it won't cause an event to be raised on the parent.
Note that those are the default options, so you can just call .updateValueAndValidity(). Maybe that wasn't the case in earlier versions of Angular.
There's a catch to this solution though: any errors you have manually set via .setErrors() will be cleared. If you're relying on errors being present, it might be better to manually cast valueChanges to an EventEmitter and emit a noop.
didn't work for me
60

Your question isn't 100% clear, but I think what you're saying it :

My valueChanges works fine when changing something in the UI, but I want to trigger my subscribe function logic as soon as I have finished initializing the FormBuilder object in my constructor to handle the initial conditions.

In that case what I do is quite simple:

    this.searchForm.valueChanges
        .pipe(startWith(initialValues)).subscribe(value =>
        {
            // whatever you want to do here
        });

initialValues is the raw data you've initialized the form with. You could probably just put in searchForm.getRawValue() too.

This just causes the observable to fire immediately.


Important: There is a subtle problem with the original answer if you're using the async pipe to subscribe instead of an explicit subscribe().

In Angular you can subscribe explicitly to an observable (as shown above), but it's equally common to use the async pipe. An example looks like this:

model = {
   firstName: this.searchForm.valueChanges.pipe(startWith(initialValues), map(values => values.firstName.toUpperCase())
};

and then in your template display the form value:

First name: {{ model.firstName | async | json }}

There is a problem with this approach that is quite subtle.

  • The startWith operator will capture the value when you create the observable, which will be correct the first time.
  • However if you re-subscribe to the observable it still be using the original values even if the form has changed. This can manifest itself if the UI is hidden and then displayed again (causing a new subscription).

Possible solutions:

  1. Use defer such that the value of this.searchForm.value will be evaluated each time the observable is subscribed to.

    defer(() => this.searchForm.valueChanges.pipe(startWith(this.searchForm.value), 
                map(values => values.firstName.toUpperCase())
    
  2. Use shareReplay. This only works here with refCount: false and therefore I do not recommend it since it won't get cleaned up properly.

    this.searchForm.valueChanges.pipe(startWith(initialValues), 
                 map(values => values.firstName.toUpperCase(), 
                 shareReplay({ refCount: false, bufferSize: 1 })
    
  3. Hypothetical: If startWith had a lambda then you could use that. Unfortunately it doesn't right now:

    startWith(() => this.searchForm.value)
    

Note: If you're using subscribe explicitly then you don't need to worry about this issue.

2 Comments

And make sure to unsubscribe
I'm just realizing now there may be a way to use defer and concat to get the initial value (instead of using startWith), but I don't have time to test it now.
7

The title and description of your question are a bit misleading. Which is it? (i)

  • Do you want to update the validity status of a field when changing its value programmatically?
  • Or do you want to make sure your subscription to the valueChanges of a field gets called when changing its value programmatically?

Anyways, check out this Plunkr: https://plnkr.co/edit/4V4PUFI1D15ZDWBfm2hb?p=preview

You'll see that when you set the field value programmatically like this:

this.myForm.get('myField').setValue(newValue);

Both the validity and the valueChanges observable for the field are updated. So, it looks like you're trying to recreate a behavior that's already here.

However, the dirty property of the field is NOT updated when changing its value programmatically (as per the doc: "A control is dirty if the user has changed the value in the UI"). Could it be that you are checking the dirty property to indicate field errors and that you have the illusion that the validity status is not updated, when in fact it's just the dirty property that's not updated?

(i) These two things are different. You should NOT subscribe to valueChanges to manually trigger validation. Validation should be enforced by declaring validators in your form model.

3 Comments

I agree with you that my question was quite misleading so I took my time to rewrite the question and answer. I had tried the setValue option with markAsDirty, however, this did not resolve my problem.
Thank you, Max. But if you look at the source code for setValue() you'll see that it already calls updateValueAndValidity(). Besides, the emitEvent param is set to true by default. In other words, the default behavior should already give you everything you want. Maybe you're not using the right syntax to change the field's value programmatically?
My company's policy on consisteny required me to use a FormBuilder to initialize the form's content, so I was required to use the FormBuilder. If I just use a FormGroup.updateValueAndValidity() without supplying it's parameters, the form doesn't invalidate how I need it to. However, if I do supply the parameters, I do get the result I need.
6

I use patchValue to reset the value in the form and trigger the change programmatically:

this.MyForm.controls["MyField"].patchValue(MyField);

Subscribe to changes:

this.MyForm.get("MyField").valueChanges.subscribe(val => {
    if (val) {
        const MyField = this.MyForm.get("MyField");
    }
});

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.