1

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 ?

sdsd
  • 447
  • 3
  • 20
  • Instead of using `ng-template`, do you think writing a custom control component implementing `ControlValueAccessor` could be a better solution? – JackySky Apr 13 '21 at 07:36
  • @JackySky can you please provide an example how this can be done with ControlValueAccessor – sdsd Apr 13 '21 at 13:22
  • I have added an answer, feel free to check if it matches your case. – JackySky Apr 14 '21 at 04:26

1 Answers1

1

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;
}
JackySky
  • 303
  • 1
  • 8
  • Sky thank you i accepter your answer. But there is bug. When i write something in the input i don't get printent the onChange event and after the objects lose focus i get [Object event] in the object insted the value... Can you please fix – sdsd Apr 14 '21 at 05:34
  • Ohh i get the whole event inside - so if i want to get the value i do just event.target.value .... – sdsd Apr 14 '21 at 05:38
  • I get also i cannot ready get of undefined with the disabled property --- i cannot find a way to fix this – sdsd Apr 14 '21 at 05:51
  • @sdsd Can you explain on the disabled issue? Like how do you bind the value and is the value correctly received? – JackySky Apr 14 '21 at 05:57
  • hello Jacky.Can you please edit your answer - with example how can i validate thhe inputs on the custom component with control value accessor ? I need thinks like validator required, check if it is touched etc – sdsd Apr 22 '21 at 13:33
  • Check for pristine and valid status of the input itself – sdsd Apr 22 '21 at 13:35
  • @sdsd You should not be doing it at the input - the validation as well as the pristine checking - are all part of the Angular Form. As we have already `propagateTouch()`, as long as you have correctly setup the form, Angular should be able to take care of the rest. However, one thing that we should do in our component is to style our control accordingly. I have added a example scss for reference. – JackySky Apr 23 '21 at 04:27
  • @JackySky, if you are not inside a FormGroup you needn't use `[ngModelOptions]="{standalone:true}"` – Eliseo Apr 23 '21 at 06:33
  • @JackySky i understand you jacky. I've setup everything correct as you suggested. It works really great. Thank you for you recommendation. From parent i am sending the controlName, CVA does the magic and after that in the parent i have the form group propagted with the values, if it is valid or not.The problem is that i need somehow to check dynamycally if EACH input is valid - not in the parent but in the child component. There i want to show the error messages in my reusable component connected with the `control value accessor` – sdsd Apr 23 '21 at 06:36
  • How can i detect that the input that is dynamycally created in the custom component connected to the control value accessor - that is invalid - but to detect that in the reusable component and show the message there under the input - For exame first name is required because the user didn't typed anything. – sdsd Apr 23 '21 at 06:47
  • @sdsd https://stackoverflow.com/questions/45556839/inheriting-validation-using-controlvalueaccessor-in-angular You could check if this solve your problem. – JackySky Apr 23 '21 at 07:01
  • @JackySky Thank you. I solved that. I posted new question here please take a look when you can. https://stackoverflow.com/questions/67230637/how-can-i-send-my-nested-formgroupname-to-my-input-component-connected-to-contro?noredirect=1#comment118837535_67230637 – sdsd Apr 23 '21 at 15:43
  • This control value accessor has lot of configuration for a lot of things.... – sdsd Apr 23 '21 at 15:43