Stol
Stol

Reputation: 179

Angular2 bind value to structural directive

So I've been trying to create my own custom dropdown menu for inputs, and so I've created a structural directive to create a dropdown list under the input element I want to use it on. Preferably I want to bind a value in the component I am using the directive on, so I can update the form control, and don't have to access the DOM directly.

I feel like there should be an easy and straight forward way of doing this, that I am most likely missing. The * decorator seems to remove the posibility of creating an Output from the directive, and also messes with the Elementref, since it turns the element into an embedded template.

Any help is greatly welcomed, I have tried solving this for awhile now and can't seem to find an answer.

Plunkr: https://embed.plnkr.co/OPxSY7PKTCo1sDpksF8j/

Upvotes: 0

Views: 457

Answers (1)

benshabatnoam
benshabatnoam

Reputation: 7680

I think the best solution for your requirement is to use ControlValueAccessor. With this approach you won't need to mess up with using a directive that creates a component and so on (as we can see from your demo). This way you can create a component that will do all the work you need.

Angular Docs say:

ControlValueAccessor creates a bridge between Angular FormControl instances and native DOM elements.

Here is a working StackBlitz demo of an ControlValueAccessor with your plunker code.

This is how you implement a ControlValueAccessor dropdown:

dropdown.component.ts:

import { Component, HostListener, EventEmitter, ElementRef, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';

const noop = () => { };

const CUSTOM_INPUT_CONTROL_VALUE_ACCESSOR: any = {
  provide: NG_VALUE_ACCESSOR,
  useExisting: forwardRef(() => DropdownComponent),
  multi: true
};

@Component({
  selector: 'appDropdown',
  templateUrl: './dropdown.component.html',
  providers: [CUSTOM_INPUT_CONTROL_VALUE_ACCESSOR]
})
export class DropdownComponent implements ControlValueAccessor {
  private _value = false;
  private isDisabled = false;
  private onTouched: () => void = noop;
  private onChange: (value: any) => void = noop;

  get value() {
    return this._value;
  }

  isShowDropdown = false;

  public clickedOutside: EventEmitter<void> = new EventEmitter<void>();
  rows = [{ name: 'One', value: 1 }, { name: 'Two', value: 2 }, { name: 'Three', value: 3 }];

  constructor(private elementRef: ElementRef) { }

  @HostListener('document:click', ['$event.target'])
  public onDocumentClick(targetElement) {
    if (!this.elementRef.nativeElement.contains(targetElement)) {
      this.isShowDropdown = false;
    }
  }

  onRowSelected(value: any) {
    this.onTouched();
    if (!this.isDisabled) {
      this.writeValue(value);
    }
    this.isShowDropdown = false;
  }

  //#region ControlValueAccessor implement

  writeValue(value: any): void {
    console.log(value);
    this._value = value;
    this.onChange(value);
  }
  registerOnChange(fn: any): void {
    this.onChange = fn;
  }
  registerOnTouched(fn: any): void {
    this.onTouched = fn;
  }
  setDisabledState?(isDisabled: boolean): void {
    this.isDisabled = isDisabled;
  }

  //#endregion ControlValueAccessor implement
}

dropdown.component.html:

<input [value]="value">
<div *ngIf="isShowDropdown" style="position: absolute">
    <h4 style="border: 1px solid grey; padding: 5px; margin: 0px" *ngFor="let row of rows" (click)="onRowSelected(row.name)">
        {{ row.name }}
    </h4>
</div>
<button (click)="isShowDropdown = !isShowDropdown;">*</button>

And finally, using it in app.component.html:

<appDropdown #inp="ngModel" name="inp" [(ngModel)]="startValue"></appDropdown>

Upvotes: 1

Related Questions