简体   繁体   中英

NgModel in parent-component supposed to reflect state in child-component

I have a library which contains a component that is a wrapper around a primeng-component. Everything is based on Angular 10.

The primeng-component has a ngModel . I want to set the ngModel in the parent-component which accesses the wrapper-component and I want the ngModel to be 'reflected' back to the parent-component when it's changed down in the primeng-component.

The parent includes the wrapper like this:

parent.component.html

    <div class="col">
      <wrapper-autocomplete
        [suggestions]="projects"
        ngDefaultControl
        [ngModel]="selectedProject"
        (ngModelChange)="changeSelectedProject($event)"
        [field]="'name'"
        (completeMethod)="getProjects($event)"
        [forceSelection]="true">
        
        ...

      </wrapper-autocomplete>
    </div>

In the parent I set [ngModel] to a variable with the name selectedProject which is part of the parent-component. The component reacts to changes by using the ngModelChange where I insert my own function:

parent.component.ts

 changeSelectedProject(event: any) {
   this.selectedProject = event;
 }

In the wrapper-component I include the primeng-component like this (abstracted, doesn't show all properties for readability):

wrapper.component.html

<p-autoComplete
  ...
  [ngModel]="ngModel"
  (ngModelChange)="ngModelChangeCallback($event)">

  ...

</p-autoComplete>

And the ts-part of the code looks like this (also cut down for readability):

wrapper.component.ts

@Component({
  selector: 'wrapper-autocomplete',
  templateUrl: './autocomplete-list.component.html',
  styleUrls: ['./autocomplete-list.component.css']
})
export class AutocompleteListComponent {

  @Input() ngModel: any;
  @Output() ngModelChange: EventEmitter<any> = new EventEmitter<any>();

  ngModelChangeCallback(event: any): void {
    this.ngModelChange.emit(event);
  }
}

So, this works and does reflect the changes back to the parent, but it is not really what I want to achieve. The component is supposed to be used by other people in the future and I want them to be able to bind the model with only using [(ngModel)] instead of [ngModel] and (ngModelChange)=... .

Do you have any suggestions on what I am doing wrong? I found out about the ControlValueAccessor which seems to be what I need, but I was unable to properly implement it in my code.

Thanks in advance!

As you already know you can implement custom ControlValueAccessor. so just let me just do it for you here

<p-autoComplete
  ...
  [ngModel]="value"
  (ngModelChange)="ngModelChangeCallback($event)">

  ...

</p-autoComplete>

@Component({
  selector: 'wrapper-autocomplete',
  templateUrl: './autocomplete-list.component.html',
  styleUrls: ['./autocomplete-list.component.css'],
  providers: [
        {
            provide: NG_VALUE_ACCESSOR,
            useExisting: forwardRef(() => AutocompleteListComponent ),
            multi: true,
        }
    ],
  })
export class AutocompleteListComponent implements ControlValueAccessor {

    onValueChange: any; 
    value: any;

    writeValue(val: any): void {
        this.value = val;
    }
    registerOnChange(fn: any): void {
        this.onValueChange = fn;
    }
    registerOnTouched(fn: any): void {}
    setDisabledState?(isDisabled: boolean): void {}

    ngModelChangeCallback(event: any): void {
      this.onValueChange(event);
    }

}


// then in use it like this

  <wrapper-autocomplete
     [suggestions]="projects"
     ngDefaultControl
     [(ngModel)]="selectedProject"
     [field]="'name'"
     (completeMethod)="getProjects($event)"
     [forceSelection]="true">
       
     ...

  </wrapper-autocomplete>

What I did above is implements a custom Control Value Processor. It has 4 methods

  1. writeValue(val: any) --> it will invoke when ever the parent mgModel change/update something
  2. registerOnChange(fn: any) --> it will invoke at component initialization and you can store that function in some variable and then everytime you want to emit something to parent simply pass data to that function like above.
  3. registerOnTouched(fn: any): void {} and setDisabledState?(isDisabled: boolean): void {} You can ignore it for now as it might not be needed right now. but they are helpful when you want to register touch or disabled state.

Another easy way is to use Banana in a box trick

Just replace your input output to other name instead of ngModel

export class AutocompleteListComponent {

  @Input() input: any;
  @Output() inputChange: EventEmitter<any> = new EventEmitter<any>();

  ngModelChangeCallback(event: any): void {
    this.inputChange.emit(event);
  }
}

 <wrapper-autocomplete
   [suggestions]="projects"
   [(input)]="selectedProject">  </wrapper-autocomplete>


Yes, ControlValueAccessor is what you need, but there is another trick for this issues with less code.

Just rename the ngModel in your child component with Smth or anything you want and the output will be SmthChange . Then you can bind [(Smth)] magically.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM