The NgModel within the parent component is expected to mirror the state of the child component

My Angular 10 project includes a library with a wrapper component around a primeng-component. The primeng-component utilizes ngModel. I am trying to set the ngModel in the parent-component accessing the wrapper-component, and I want any changes made to the ngModel within the primeng-component to be reflected back in the parent-component.

This is how the parent-component incorporates the wrapper:

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-component, the [ngModel] is set to a variable named selectedProject, which triggers a change through ngModelChange event calling a custom function:

parent.component.ts

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

The wrapper-component includes the primeng-component as follows:

wrapper.component.html

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

  ...

</p-autoComplete>

The TypeScript portion of the code is structured like this:

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);
  }
}

Although the current setup effectively reflects changes back to the parent-component, it is not the ideal approach for future users. I aim to simplify the model binding process by enabling users to bind the model using [(ngModel)] instead of [ngModel] and (ngModelChange)=....

Any insights on where I might be going wrong would be greatly appreciated. I have looked into the ControlValueAccessor feature, but I struggled to implement it correctly in my code.

Thank you for your help!

Answer №1

If you're looking to implement a custom ControlValueAccessor, I can help guide you through the process. Here's a step-by-step example:

<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);
    }

}


// To use it:

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

  </wrapper-autocomplete>

I have implemented a custom Control Value Processor with 4 methods for handling parent ngModel changes:

  1. writeValue(val: any) - Invoked whenever the parent ngModel updates.
  2. registerOnChange(fn: any) - Called at component initialization to store the function that emits data to the parent.
  3. registerOnTouched(fn: any) and setDisabledState(isDisabled: boolean) - Optional methods for registering touch or disabled state changes.

Another approach is using the Banana in a box trick:

You can replace input/output names instead of ngModel. Here's an example:

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>


Answer №2

A handy solution for handling this kind of problem is to utilize the ControlValueAccessor. In addition, there's a clever workaround that involves using less code.

All you have to do is simply change the name of ngModel in your child component to something like Smth, and as a result, the output will automatically become SmthChange. This allows you to effortlessly bind [(Smth)].

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

What is the reason behind the command "npm-install" placing files directly into the root directory?

After pulling my front-end project from the repository, I noticed that the node_modules folder was missing. My usual approach is to use npm install to get all the dependencies listed in the package.json file installed. However, this time around, I ended u ...

The compatibility issue between the text-mask library and the Angular material datepicker is causing functionality problems

I am currently utilizing the text-mask library (https://www.npmjs.com/package/angular2-text-mask) in an attempt to integrate it with two Angular datepicker components. The functionality works as expected when manually entering the date into the input field ...

How Angular 2's change detection system causes unnecessary DOM refreshing

My application is currently in the live beta stage and I am focused on improving its performance. One issue that has caught my attention is the flickering of images within a table. Upon investigation, I discovered that the DOM is continuously refreshing, e ...

I am uncertain about how to interpret this method signature

Can you help me determine the correct method signature for handleError? The linter tslint is indicating an error message that says expected call-signature: 'handleError' to have a typedef (typedef). Here is the code snippet in question: import ...

The installation of msal-angular is encountering issues with the peer rxjs version "^6.0.0" from @azure/[emailprotected]

I am attempting to incorporate @azure/msal-angular for Azure B2C authentication with Angular as the front end, but encountering errors during package installation. npm ERR! code ERESOLVE npm ERR! ERESOLVE unable to resolve dependency tree npm ERR! npm ...

What is the proper way to compare enum values using the greater than operator?

Here is an example enum: enum Status { inactive = -1, active = 0, pending = 1, processing = 2, completed = 3, } I am trying to compare values using the greater than operator in a condition. However, the current comparison always results in false ...

Make sure the static variable is set up prior to injecting the provider

In our Angular6 application, we utilize a globalcontextServiceFactory to initialize the application before rendering views. This process involves subscribing to get configuration from a back-end endpoint and then using forkJoin to retrieve environment app ...

Using local variables in Angular2 templates

For the specific scenario discussed below, I have assigned the local variable #input to multiple radio buttons. My goal is to select the radio button within the <tr> when it is clicked. Surprisingly, the provided code functions as expected, yet the ...

Angular 6: Utilizing async/await to access and manipulate specific variables within the application

Within my Angular 6 application, I am facing an issue with a variable named "permittedPefs" that is assigned a value after an asynchronous HTTP call. @Injectable() export class FeaturesLoadPermissionsService { permittedPefs = []; constructor() { ...

Creating the upcoming application without @react-google-maps/api is simply not possible

After incorporating a map from the documentation into my component, everything seemed to be functioning correctly in the development version. However, when attempting to build the project, an error arose: Type error: 'GoogleMap' cannot be used as ...

Issue with Cypress TypeScript: Unable to locate @angular/core module in my React application

I am currently in the process of updating my Cypress version from 9.70 to 10.7.0. Although I have fixed almost all the bugs, I have encountered a strange message stating that @angular/core or its corresponding type declarations cannot be found. My applica ...

Avoiding caching of GET requests in Angular 2 for Internet Explorer 11

My rest endpoint successfully returns a list when calling GET, and I can also use POST to add new items or DELETE to remove them. This functionality is working perfectly in Firefox and Chrome, with the additional note that POST and DELETE also work in IE ...

What could be causing my NodeJS Backend to not retrieve the data properly?

For a current project, I am tasked with building a mobile application using Flutter for the frontend and NodeJS for the backend. To facilitate this, I have acquired a VPS from OVHcloud running Ubuntu 20.04. Following various tutorials, I set up a server as ...

Tracking events in Angular 4 using angulartics2 and Adobe Analytics: A step-by-step guide

I've been working on tracking page views in my Angular 4 application, specifically with Adobe Analytics. Currently, I am utilizing angulartics2 for this purpose. First step was adding the necessary script for Adobe Staging in my index.html page: &l ...

The Eslint tool encountered an issue: Parsing error - it seems that the function ts.createWatchCompilerHost is

What could be causing this error message to appear? Here is my current configuration: "parser": "@typescript-eslint/parser", "parserOptions": { "project": "tsconfig.json", "tsconfigRootDir& ...

While attempting to reinstall the admob-free plugin via npm, I encountered an error stating that it was missing a package.json file

While developing an app using Ionic, I encountered an issue with the AdMob plugin not being installed correctly. Trying to resolve this, I attempted to reinstall the plugin multiple times but kept running into errors. Seeking help from various threads, I ...

Connecting a Database with NestJS and TypeORM: A step-by-step guide to establish a connection with TypeORM and ensure easy access to

Could someone please explain how to create a DB instance using TypeORM? I want it to be accessible like this service, but the Connection class is deprecated. import { Inject, Injectable } from '@nestjs/common'; import { Connection, Repository } ...

Is it possible to register multiple mini router apps using app.use() in MEAN?

Within my node/express app.js main file, I have established a mini-app router: var router = express.Router(); I pass this router to my controller functions and then export it. Finally, I register the router by using: app.use('/Link', router); ...

I'm encountering difficulties utilizing ternary operators in TypeScript

I am struggling with ternary operators in TypeScript and need help understanding the issue. Please review the code below: const QuizQuestionContainer = ({ qa }: QuizQuestionContainerPropsType) => { const { question, option1, option2, option ...

Is there a way for the parent class to access the child class in Angular 2?

Seeking guidance on accessing a child class from a parent class in my Angular 2 application with Typescript. The structure of the parent and child classes are as follows: The parent class, AllDataPageComponent: @Component({ selector: 'all-data-p ...