0
votes

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..

1

1 Answers

0
votes

I'm afraid that you can not use in ng-content an html tag <option>, but you can create a directive

@Directive(
{
  selector:'[option]'
})
export class OptionDirective implements AfterViewInit{
  @Input() ngValue:any;
  innerHTML:string="";
  constructor(private el:ElementRef)
  {
  }
  ngAfterViewInit()
  {
     this.innerHTML=this.el.nativeElement.innerHTML

  }
}

So your .html becomes like, see that use <div option..>

<dropdown [(ngModel)]="selectedUser" (ngModelChange)="onUserChange($event)">
  <div option *ngFor="let user of users;" [ngValue]="user">
    ID#{{user.id}} - {{user.name}}
  </div>
</dropdown>

Your component has the ng-content in a div with display none

<select [ngClass]="[sizeClass]" [(ngModel)]="value">
  <option *ngFor="let option of options" [ngValue]="option.ngValue">
        {{option.innerHTML}}
   </option>
</select>
<div [style.display]="'none'">
    <ng-content></ng-content>
</div>

And you get the options and get the directive using ContentChildren

 @ContentChildren(OptionDirective) options:QueryList<OptionDirective>

You can see in stackblitz

NOTE: I put in the same file dropdown.component, the directive OptionDirective too (that you need declare in the module)

Update using the directive, we can use as selector of the directive "option"

@Directive(
{
  selector:'option' //<--use 'option'
})
export class OptionDirective implements AfterViewInit{
  ...
}

And then, our .html becomes like

<dropdown [(ngModel)]="selectedUser" (ngModelChange)="onUserChange($event)">
  <option *ngFor="let user of users;" [ngValue]="user">
    ID#{{user.id}} - {{user.name}}
  </option>
</dropdown>