I'm trying to create a custom dropdown component which implements the ControlValueAccessor, but I want to be able to define the options from the parent component instead of the custom dropdown.
Ideally I would like to implement it using ngModel in my select and ngValue in my options, but for some reason when I use ngValue in the options, my ngModel is not receiving the object which is associated with the ngValue for the currently selected option.
Below is a simplified example of the usage I'm trying to accomplish:
Note: I'm aware in this case I can just use the id field and go about my way, but I have cases where I would like to use ngModel and ngValue.
User: { id: string, name: string, address string }
<!-- This does NOT work like I expected, and selectedUser receives the label in between the options tags -->
<custom-dropdown [(ngModel)]="selectedUser" (change)="onUserChange($event)">
<option *ngFor="let user of users;" [ngValue]="user">
ID#{{user.id}} - {{user.name}}
</option>
</custom-dropdown>
Printing the user output before I make any changes is a user object and is expected. However, as soon as I change the options, the selectedUser object's value is the string in between the option tag of the selected option (i.e. ID#... - ...) instead of the object.
If I use [value]="user.id"
instead of [ngValue]="user"
, everything works as expected:
<!-- This works as expected and selectUser gets the user object that ngValue is associated with -->
<custom-dropdown [(ngModel)]="selectedUser" (change)="onUserChange($event)">
<option *ngFor="let user of users;" [value]="user.id">
ID#{{user.id}} - {{user.name}}
</option>
</custom-dropdown>
If I use a regular select instead of the custom-dropdown
, everything works as expected:
<!-- This works as expected and selectUser gets the user object that ngValue is associated with -->
<select [(ngModel)]="selectedUser" (change)="onUserChange($event)">
<option *ngFor="let user of users;" [ngValue]="user">
ID#{{user.id}} - {{user.name}}
</option>
</select>
Here are the dropdown.ts and dropdown.html implementations:
dropdown.html
<select [ngClass]="[sizeClass]" [(ngModel)]="value">
<ng-content></ng-content>
</select>
dropdown.ts
@Component({
selector: 'dropdown',
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DropdownComponent),
multi: true
}
],
templateUrl: './dropdown.component.html',
styleUrls: ['./dropdown.component.scss']
})
export class DropdownComponent implements ControlValueAccessor {
private innerValue: any;
get value(): any {
return this.innerValue;
}
set value(value: any) {
if (this.innerValue !== value) {
this.innerValue = value;
this.onChange(value);
}
}
writeValue(value: any) {
this.innerValue = value;
}
onChange = (_) => {};
onTouched = () => {};
registerOnChange(fn: (_: any) => void): void { this.onChange = fn; }
registerOnTouched(fn: () => void): void { this.onTouched = fn; }
}
If you would like to see the full implementation, I have a simple stackblitz setup:
https://stackblitz.com/edit/angular-6v8ajp
I'm not sure what exactly I'm dong wrong and any help is appreciated..