2

Big question short: Basically what I want to achieve is, to apply readonly attribute to FormControl (textbox), right when it's created programmatically.

Full scenario: I have a Form that has a FormRow whose elements (FormGroups) are partly generated dynamically. I also have given an option to append a row on click of a button.

I want the FormGroup's FormControls (textboxes) that are generated dynamically to be readonly, but the manually added FormControls should be editable.

How can I achieve this?

<div formArrayName="rows">
    <div class="row" *ngFor="let row of detailForm.get('rows').controls; let i = index" [formGroupName]="i">
        <div class="col-xs-1 text-center">
            <input type="checkbox" [(ngModel)]="detailSelected[i]" [ngModelOptions]="{standalone: true}">
        </div>
        <div class="col-xs-2">
            <input type="text" class="form-control" formControlName="account" (change)="getAccountAssociatedDetails(detailForm.get(['rows', i, 'account']), 'account', i)">
        </div>
        <div class="col-xs-2">
            <input type="text" class="form-control" formControlName="detailNo" (change)="getAccountAssociatedDetails(detailForm.get(['rows', i, 'detailNo']), 'detail', i)">
        </div>
        <div class="col-xs-4">
            <input type="text" class="form-control" readonly formControlName="detailName">
        </div>
        <div class="col-xs-3">
            <input type="text" class="form-control" readonly formControlName="detailCountry">
        </div>
    </div>
</div>

On change of textbox value, I'm calling the function getAccountAssociatedDetails, as you can see. If there are some details associated with the value entered, I'm replacing that current FormGroup and adding some more FormGroups to the row.

getAccountAssociatedDetails(control: FormControl, type: string, index: number) {
    let value = control.value;
    if (!!value) {
        let form = this.detailForm;
        control.setErrors({
            'alreadyEntered': null
        });

        if (type === 'detail') {
            if (this.detailAlreadyEnteredManually.detailNo.includes(value)) {
                control.setErrors({
                    'alreadyEntered': true
                });
            }
        } else if (type === 'account') {
            if (this.detailAlreadyEnteredManually.accountNumber.includes(value)) {
                control.setErrors({
                    'alreadyEntered': true
                });
            }
        }

        // if detail or account is already entered dont call service
        if (!control.hasError('alreadyEntered')) {
            this.agreementDetailsService.getdetails(value, type)
                .subscribe(response => {
                        let associatedDetailsArray = < Array < any >> response;
                        if (!!associatedDetailsArray) {
                            let rows = < FormArray > form.get('rows');

                            // if search mode is 'detail'
                            if (type === 'detail') {
                                if (!this.detailAlreadyEnteredManually.detailNo.includes(value)) {
                                    rows.setControl(index, this.builddetailItem(associatedDetailsArray[0].accountNumber.value, value, associatedDetailsArray[0].detailName, associatedDetailsArray[0].detailCountry));
                                    this.detailAlreadyEnteredManually.detailNo.push(value);

                                    for (let i = 1; i < associatedDetailsArray.length; i++)
                                        rows.insert(index + i, this.builddetailItem(associatedDetailsArray[i].accountNumber.value, value, associatedDetailsArray[i].detailName, associatedDetailsArray[i].detailCountry));
                                }

                                //if search mode is 'account'
                            } else if (type === 'account') {
                                if (!this.detailAlreadyEnteredManually.accountNumber.includes(value)) {
                                    rows.setControl(index, this.builddetailItem(value, associatedDetailsArray[0].detailNo.value, associatedDetailsArray[0].detailName, associatedDetailsArray[0].detailCountry));
                                    this.detailAlreadyEnteredManually.accountNumber.push(value);

                                    for (let i = 1; i < associatedDetailsArray.length; i++)
                                        rows.insert(index + i, this.builddetailItem(value, associatedDetailsArray[i].detailNo.value, associatedDetailsArray[i].detailName, associatedDetailsArray[i].detailCountry));
                                }
                            }
                        }
                    },
                    error => console.log(error));
        }
    }
}

The last two fields in the FormGroup are readonly by default. Once the user enters something in either of the first two fields and focuses out, the details associated with that value should be fetched, the current FormGroup is replaced and other FormGroups in the generated list are appended. I want all the FormControls in those newly added FormGroups should be readonly. The user can add more fields and the process repeats.

Cœur
  • 37,241
  • 25
  • 195
  • 267
karthikaruna
  • 3,042
  • 7
  • 26
  • 37

1 Answers1

4

According to this: https://github.com/angular/angular/issues/11447, there isn't a way to add readonly on a form control, only disabled is supported.

I would even myself prefer disabled, since it shows visually (greyed) that user cannot touch it. So when you want to disable your formcontrol, you just use:

formControlNameHere.disable();

or if you need to disable a whole formgroup, you can do it in the same manner with

myFormGroupNameHere.disable()

what we need to remember with this, is that this formcontrol is then excluded from the form object. To get all disabled values you need to call:

this.myForm.getRawValue();

If you do want to add readonly, you'd need to do some workaround for it: https://stackoverflow.com/a/45452289/7741865:

[readonly]="anyBooleanPropertyFromComponent" 

Of course in that answer we use a single boolean value, which would of course not work for you, I then see that you'd need to set an additional boolean formcontrol, which you could then bind with the [readonly]. But that is just then adding a new "unnecessary" property to your form group, so another reason for just going with disabled :)

Hope this helps!

AT82
  • 71,416
  • 24
  • 140
  • 167
  • Thanks a lot for the response man, but `rows.setControl(index, this.builddetailItem(value, associatedDetailsArray[0].detailNo.value, associatedDetailsArray[0].detailName, associatedDetailsArray[0].detailCountry).disable());` won't work as `disable()`'s return type is `void` and doesn't return a disabled `FormGroup`. I'm using `builddetailItem()` to construct and return my `FormGroup`. – karthikaruna Oct 07 '17 at 11:18
  • 1
    Your code is so complex so it's hard to follow, it would be great if you could for example provide a simplified plunker or stackblitz that showcases the issue you are facing now :) – AT82 Oct 07 '17 at 11:26
  • Disabled using the `FormArray#at` method in the loop :) – karthikaruna Oct 07 '17 at 12:04
  • Great that you figured it out! :) – AT82 Oct 07 '17 at 12:16
  • But it makes the whole form invalid, making my submit button always stay disabled :( Any workaround!? – karthikaruna Oct 07 '17 at 12:51
  • When fields are disabled, they are not included in form object, and should therefore not have anything to do with the validation, as they are technically not part of the form. So I don't know what's going on there, it shouldn't affect the validity. Could you create a demo that showcases this? – AT82 Oct 07 '17 at 12:55
  • It works if I use `form.invalid` instead of `!form.valid` on the submit button – karthikaruna Oct 07 '17 at 14:48
  • 2
    Yeah, since not seeing your code for whole form, I could not guess that one. But glad you figured it out! :) – AT82 Oct 07 '17 at 16:20