Search code examples
angularangular-forms

How can i make my ngModel to work automatically in my ng-template?


I have a lot of form-groups with labels and inputs.I wanted to make ng-template which will be reusable

So i had

  <div class="form-group">
      <label class="form-control-label" for="address">Address</label>
       <input [disabled]="isTheLoggedInUserAdmin" id="address" class="form-control form- 
       control-alternative"
       [(ngModel)]="sharedService.tempUser.address" name="address" type="text">
  </div>

and with ng-template it is converted to

 <ng-template [ngTemplateOutlet]="formGroup"
      [ngTemplateOutletContext]="{data: {inputId:'address', label:'Address', ngModel: 
      sharedService.tempUser.address }}"
></ng-template>


<ng-template #formGroup let-data="data">
 <div class="form-group">
    <label class="form-control-label" [for]="data.inputId">{{data.label}}</label>
    <input [disabled]="isTheLoggedInUserAdmin" [id]="data.inputId" class="form-control form-control- 
    alternative"
    [(ngModel)]="sharedService.tempUser.address" [name]="data.inputId" type="text">
  </div>
</ng-template>

so i am passing here inputId, label name automatically and for now the ngModel is hardcoded it is pointing to sharedService.tempUser.address

But my ng-template needs to be dynamic so with ng-template call i should pass argument like label for example - tthe argument shpuld point to different ngModel variables in my typescript files

But when i do that

 <div class="form-group">
    <label class="form-control-label" [for]="data.inputId">{{data.label}}</label>
    <input [disabled]="isTheLoggedInUserAdmin" [id]="data.inputId" class="form-control form-control-alternative"
      [(ngModel)]="data.ngModel" [name]="data.inputId" type="text">
  </div>

now data.ngModel is sended from the ng-template call - which is sharedService.tempUser.address, i get the actual value from sharedService.tempUser.address but *THE PROBLEM IS THAT ngModel does not work here`

when i type something it is not updated

How can i solve this ?


Solution

  • You might want to write an implementation of ControlValueAccessor as an alternative. It's an Angular official way to create reusable component compatible with Form API. A minimum example will be something like this:

    my-custom-control.component.html

    <div class="form-group">
       <label class="form-control-label" for="{{data.field}}">{{data.label}}</label>
       <input [disabled]="disabled" id="{{data.field}}" class="form-control form-control-alternative"
              [value]="value" name="{{data.field}}" type="text"
              (change)="onChange($event)" (blur)="onBlur($event)">
    </div>
    

    my-custom-control.component.ts

    /**
        import things....
    **/
    
    @Component({
      selector: 'MyControl',
      templateUrl: './my-custom-control.component.html',
      styleUrls: ['./my-custom-control.component.scss'],
      providers: [{
        provide: NG_VALUE_ACCESSOR,
        useExisting: forwardRef(() => MyControlComponent),
        multi: true
      }]
    })
    export class MyControlComponent implements OnInit, ControlValueAccessor {
    
      @Input() value: any;
      
      @Input() data: { 'field': string, 'label': string } =  { field: "default", label: "default" };
      
      @Input() disabled: boolean = false;
      setDisabledState(isDisabled: boolean) {
        this.disabled = isDisabled;
      }
    
      propagateChange = (_: any) => { };
      propagateTouch = () => { };
      registerOnChange(fn) {
        this.propagateChange = fn;
      }
      registerOnTouched(fn) {
        this.propagateTouch = fn;
      }
      
      constructor() {
      }
    
      ngOnInit() {
          
      }
      
      // value changed from ui
      onChange(event: any){
        this.value = event.target.value;
        // tell angular that the control value is changed
        this.propagateChange(this.value);
      }
      
      // received value from form api
      writeValue(newFormValue: any) {
        this.value = newFormValue;
      }
      
      // tell angular that this form control is touched
      onBlur(event: any) {
        this.propagateTouch();      
      }
    }
    

    After that, you could reuse this component like:

    <!-- With FormGroup -->
    <form [formGroup]="myForm">
      <MyControl formControlName="address" [disabled]="isTheLoggedInUserAdmin" [data]="{ field: 'address', label: 'Address' }">
      <MyControl formControlName="title" [disabled]="isTheLoggedInUserAdmin" [data]="{ field: 'title', label: 'Title' }">
      <MyControl formControlName="surname" [disabled]="isTheLoggedInUserAdmin" [data]="{ field: 'surname', label: 'Surname' }">
      <!-- other controls... -->
    </form>
    
    <!-- Without FormGroup -->
    <div>
      <MyControl [(ngModel)]="myModel.address" [ngModelOptions]="{standalone: true}" [disabled]="isTheLoggedInUserAdmin" [data]="{ field: 'address', label: 'Address' }">
      <MyControl [(ngModel)]="myModel.title"   [ngModelOptions]="{standalone: true}" [disabled]="isTheLoggedInUserAdmin" [data]="{ field: 'title', label: 'Title' }">
      <MyControl [(ngModel)]="myModel.surname" [ngModelOptions]="{standalone: true}" [disabled]="isTheLoggedInUserAdmin" [data]="{ field: 'surname', label: 'Surname' }">
      <!-- other controls... -->
    </div>
    

    which can also be easily written into an ngFor if you have a model description array.

    Edit: Add a sample scss styling.

    my-custom-control.component.scss

    @import "variable.scss";
    
    :host(.ng-dirty.ng-invalid, .ng-touched.ng-invalid) input {
      border-color: $c-alert !important;
      border-width: 2px;
      border-style: solid;
      color: $c-alert;
    }