Building a Custom Dropdown Select List with FormControlName and ControlValueAccessor in Angular 8

Does anyone have a working solution for creating a Material dropdown wrapper (mat-select dropdown) that can be used with formControlName? If so, could you please share a Stackblitz demo of your implementation?

Here are the requirements:

  • Should work seamlessly with formControlName within a parent component form that uses formBuilder and its validators. The parent form may contain multiple form fields.
  • Must display a red error message if the data entered does not meet the validation criteria set by the parent formBuilder.
  • a) Should work with formControlName/patchValue (which should work with the entire class). b) Optionally, should also support inputting data through @Input() SelectedValueId Id number.

I've been attempting to achieve this but haven't had much success yet. Any help in fixing this issue would be greatly appreciated!

In this case, the ID refers to sourceOfAddressId.

export class SourceOfAddressDto implements ISourceOfAddressDto {
    sourceOfAddressId: number | undefined;  // This ID should be supported
    sourceOfAddressCode: string | undefined;
    sourceOfAddressDescription: string | undefined;

Typescript:

@Component({
    selector: 'app-address-source-dropdown',
    templateUrl: './address-source-dropdown.component.html',
    styleUrls: ['./address-source-dropdown.component.scss'],
    providers: [
        {
            provide: NG_VALUE_ACCESSOR,
            useExisting: forwardRef(() => AddressSourceDropdownComponent),
            multi: true
        }
    ]
})
export class AddressSourceDropdownComponent implements OnInit, OnChanges {

    dataList: any[] = []; 
    @Input() Label = 'Address Source';
    @Input() sourceOfAddressDefaultItem: SourceOfAddressDto = SourceOfAddressDefault;
    @Input() selectedSourceOfAddress: any;
    @Input() TxtValue = 'sourceOfAddressId';
    @Input() TxtField = 'sourceOfAddressDescription';
    @Input() Disabled: boolean;
    @Input() valuesToExclude: number[] = [];
    @Input() Hint = '';
    @Input() styles: string;
    @Input() defaultSourceOfAddressCode: any;
    @Output() addressSourceChange = new EventEmitter<any>();

    private _selectedValueId: number;

    @Input() set selectedValueId(value: number) {
        this._selectedValueId = value;

        let outputData: any;
        if (this.selectedValueId == this.sourceOfAddressDefaultItem[this.TxtValue]) {
            outputData = null;
        } else {
            outputData = this.dataList.find(x => x[this.TxtValue] == this.selectedValueId);
        }

        this.onChange(outputData);
    }
    get selectedValueId(): any {
        return this._selectedValueId;
    }
    @Input() errors: any = null;
    disabled: boolean;
    control: FormControl;
    writeValue(value: any) {
        this.selectedValueId = value ? value : '';
    }
    onChange = (_: any) => { };
    onTouched: any = () => { };
    registerOnChange(fn: any) { this.onChange = fn; }
    registerOnTouched(fn: any) { this.onTouched = fn; }
    setDisabledState(isDisabled) { this.disabled = isDisabled; }

    constructor(
        public injector: Injector,
        private AddressService: AddressServiceProxy,
    ) { }

    ngOnInit() {
        this.loadDataList();
    }

    ngOnChanges() { }

    loadDataList() {
        this.AddressService.getSourceOfAddressAll().subscribe(res => {
            this.dataList = res.body.filter(q => q.sourceOfAddressId !== -1);
        });
    }

}

HTML:

<div class="dropdown-cont">
  <mat-form-field appearance="outline">
    <mat-label>{{Label}}</mat-label>
    <mat-select 
      disableOptionCentering 
      [disabled]="Disabled" 
      [ngStyle]="styles" 

      (ngModelChange)="selectedValueId=$event"
        required>
      <mat-option [value]="sourceOfAddressDefaultItem[TxtValue]">{{sourceOfAddressDefaultItem[TxtField]}}</mat-option>
      <mat-option *ngFor="let item of dataList" [value]="item[TxtValue]">
        {{item[TxtField]}}
      </mat-option>
    </mat-select>
    <mat-hint>{{Hint}}</mat-hint>
  </mat-form-field>
</div>
  • The implementation should ideally handle default values even when there are delays in API responses, ensuring the default value is inserted first into the @Input SelectedValueId field.

Answer №1

Some have pointed out the importance of explicitly implementing the ControlValueAccessor interface. While this is a recommended practice, TypeScript does not mandate it as anything that satisfies an interface implicitly implements it. In your case, you are satisfying the interface with your writeValue, registerOnChange, and registerOnTouched methods (and optionally with

setDisabledState</code).</p>

<p>The key issue lies in the specifics of your implementation. Angular heavily relies on this interface for the seamless 2-way binding of <code>formControlName
(and
formControl</code) between parent and child components.</p>

<p>Your <code>writeValue
and registerOnTouched functions seem to be in order, but there's a problem with your registerOnChange. This method should handle changes locally within your component, allowing you to hook into Angular's valueChanges event function.

A common approach using a form control:

control = new FormControl('');

registerOnChange(fn: (value: string) => void) {
    this.control.valueChanges
        .subscribe(fn);
}

By implementing something similar, you can establish bidirectional communication between parent and child components.

To meet all your requirements, additional custom code will be needed. I've previously implemented a similar solution which I recreated in a StackBlitz demo.

I hope this information helps you make progress.

Answer №2

It's recommended to include the ControlValueAccessor interface in your component to indicate to Angular that you intend to utilize reactive forms.

export class AddressSourceDropdownComponent implements OnInit, OnChanges, ControlValueAccessor { ...

Answer №3

To enable the control to function within a mat-form-field, it is necessary to take an additional step by implementing the MatFormFieldControl interface. The official material documentation provides a detailed guide and code example on how to accomplish this, which can be found here: Creating a custom form field control. Please note that the provided sample does not include the implementation of ControlValueAccessor, which is also required.

Answer №4

Recently, I tackled the task of implementing an Angular control for CKEditor. To make this work smoothly with FormControl, FormControlName, as well as ngModel (and even with old AngularJS ng-model), you'll need to use the ControlValueAccessor. The implementation is quite straightforward - simply call your designated function (in my case, it was placed in the DoAnyCodeYouNeedToDoWhenTheValueChanges placeholder) whenever the value is set. In my scenario, I updated the value to CKEditor whenever that function was called. Subsequently, when CKEditor made changes, I would update this._Value and invoke the this.onChangeCallback to notify other components about the change.

// Initialization of callbacks provided later by Control Value Accessor
private onTouchedCallback: () => void = noop;
private onChangeCallback: (_: any) => void = noop;

// Getter method for value
get value(): any {
    //console.warn("Get Value", this._Value);
    return this._Value;
};

// Setter method for value including invoking onchange callback
set value(v: any) {
    //console.warn("Set Value", v);
    if (!this.destroyInitiated) { 
        if (v !== this._Value) {
            this._Value = v || "";
            this.DoAnyCodeYouNeedToDoWhenTheValueChanges(this._Value)
            this.onChangeCallback(v); 
        }
    }
}

// Set touched on blur
onBlur() {
    //console.warn("onBlur");
    this.onTouchedCallback();
}

// Implementation of writeValue from ControlValueAccessor interface
writeValue(value: any) {
    //console.warn("Write Value", value);
    if (!this.destroyInitiated) {
        this._Value = value || "";
        this.DoAnyCodeYouNeedToDoWhenTheValueChanges(this._Value)
    }
}

// Implementation of registerOnChange from ControlValueAccessor interface
registerOnChange(fn: any) {
    //console.warn("register on change", fn);
    this.onChangeCallback = fn;
}

// Implementation of registerOnTouched from ControlValueAccessor interface
registerOnTouched(fn: any) {
    // console.warn("register on touched", fn);
    this.onTouchedCallback = fn;
}

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

Display issue with React TypeScript select field

I am working with a useState hook that contains an array of strings representing currency symbols such as "USD", "EUR", etc. const [symbols, setSymbols] = useState<string[]>() My goal is to display these currency symbols in a select field. Currently ...

Using Angular material to display a list of items inside a <mat-cell> element for searching

Can I use *ngFor inside a <mat-cell> in Angular? I want to add a new column in my Material table and keep it hidden, using it only for filtering purposes... My current table setup looks like this: <ng-container matColumnDef="email"> < ...

transform json array into a consolidated array by merging identical IDs

I need to transform an array into a different format based on the values of the ID and class properties. Here is the initial array: const json = [{ "ID": 10, "Sum": 860, "class": "K", }, { "ID": 10, "Sum": 760, "class": "one", }, { "ID": ...

Is it possible to enable password authentication on Firebase even if the user is currently using passwordless sign-on?

In my frontend JS project, I have integrated Firebase for web and am utilizing the passwordless (email link) authentication method for users. I am now interested in implementing password sign-on for an existing user who is currently using passwordless si ...

What is the recommended TypeScript type for the NextJS _app.tsx Component and pageProps?

Take a look at the default _app.tsx code snippet from NextJS: function MyApp({ Component, pageProps }) { return ( <Component {...pageProps} /> ) } The issue arises when transitioning to TypeScript, as ES6Lint generates a warning indicating t ...

Is it possible for Angular version 15 to function without needing to migrate to material

Can anyone clarify whether material migration is necessary when upgrading from Angular v14 to v15? The Angular upgrade guide mentions that old(v14) material modules can still be used by utilizing legacy modules, so is it mandatory to migrate? "In the new ...

Unable to connect to the directive even after adding it as an input

In the error message below, it seems that suggestion 1 might be applicable to my situation. My Angular component has a GcUser input, but I have confirmed that it is part of the module (both the component behind the HTML and the user-detail component import ...

"Overcoming obstacles in managing the global state of a TypeScript preact app with React/Next signals

Hello, I recently attempted to implement a global app state in Preact following the instructions provided in this documentation. However, I kept encountering errors as this is my first time using useContext and I suspect that my configuration might be inco ...

Imitate a required component in a service

I am currently facing an issue with mocking a dependency in a service. I am not sure what is causing the problem. It's not the most ideal class to test, but I am mainly focused on code coverage. Below is the code for the service: @Injectable() export ...

Troubleshooting: Angular route transition animation error due to missing module

While setting up a route animation in Angular within a single component, I encountered an error message: "core.js:6210 ERROR Error: Found the synthetic property @routeAnimations. Please include either "BrowserAnimationsModule" or "No ...

Navigating through a multidimensional array in Angular 2 / TypeScript, moving both upwards and downwards

[ {id: 1, name: "test 1", children: [ {id: 2, name: "test 1-sub", children: []} ] }] Imagine a scenario where you have a JSON array structured like the example above, with each element potenti ...

Error in TypeScript React: "Could not locate module: Unable to locate 'styled-components'"

Even though I have installed the @types/styled-components package and compiled my Typescript React app, I am consistently encountering this error: Module not found: Can't resolve 'styled-components' I have double-checked that 'style ...

Transitioning a JavaScriptIonicAngular 1 application to TypescriptIonic 2Angular 2 application

I am currently in the process of transitioning an App from JavaScript\Ionic\Angular1 to Typescript\Ionic2\Angular2 one file at a time. I have extensively researched various guides on migrating between these technologies, completed the A ...

Access Azure-Active Directory through cypress tests

Currently, I'm faced with the task of creating automated tests for an application that requires login to Azure Active Directory. These tests are being written using Cypress and TypeScript. In search of a solution, I am seeking advice on how to execute ...

Angular4 + Universal + ng-bootstrap triggering an 'Unexpected token import' error

I recently made the leap to upgrade my angular version from 2+ to 4+ in order to take advantage of the universal package for server-side rendering, specifically for SEO purposes. Following the necessary steps and configurations outlined at https://github.c ...

Make sure to wait for the loop to complete before moving on to the next line

I am currently leveraging the capabilities of the GitHub API to fetch a list of repositories. Subsequently, I iterate over each repository and initiate another HTTP request to obtain the most recent commit date. How can I orchestrate the iteration process ...

The array containing numbers or undefined values cannot be assigned to an array containing only numbers

Currently facing an issue with TypeScript and types. I have an array of IDs obtained from checkboxes, which may also be empty. An example of values returned from the submit() function: const responseFromSubmit = { 1: { id: "1", value: "true" }, 2: ...

regex execution and testing exhibiting inconsistent behavior

The regex I am using has some named groups and it seems to match perfectly fine when tested in isolation, but for some reason, it does not work as expected within my running application environment. Below is the regex code that works everywhere except in ...

Guide to accessing a menu through Long press or Right click in Angular2

I recently started experimenting with angular 2 and I am trying to figure out how to create a menu that opens with multiple options on both mobile and desktop devices. What I'm looking for is a way to trigger the opening of a menu when a long hold or ...

The Sourcemap is not correctly aligning with the expected line number

Currently working with the angular2-webpack-starter technology and utilizing VSCode along with Chrome debugger. After numerous attempts, I was able to successfully set a breakpoint, but it appears that the line mapping is incorrect. The issue persists in ...