21
votes

I want to create a dropdown menu using Angular 2, but I'm not sure how to do it in the "Angular 2 way".

I could create a dropdown component that is used like this:

<dropdown>
    <li (click)="action('item 1')">Item 1</li>
    <li (click)="action('item 2')">Item 2</li>
</dropdown>

This seems nice, but then the action method needs to be defined on the component that contains the <dropdown> and the <li> elements don't get styles applied from the styles in the <dropdown> component, which is kind of odd.

Another option is to create components that are used like this:

<dropdown>
    <dropdown-item (click)="action('item 1')">Item 1</dropdown-item>
    <dropdown-item (click)="action('item 2')">Item 2</dropdown-item>
<dropdown>

This is more verbose, the dropdown-item component handles the click action, and the styles of the items get defined by the dropdown-item component as well.

Is there a more canonical way to do this in Angular 2?

Edit: I'm not talking about a custom select input for a form. More like a menu with options, or a right click context menu.

6
Just rendering the <li> inside the dropdown component's template is not an option? - Langley
That's an option but then it isn't a very reusable component since the <li>s would be hard coded. - Ryan
You can us an ngFor in the <li> and set the properties from a list you don't have to hardcode each of them - Langley
You can just initialize a list of strings as a property of the component like options:string[] = [ 'option1', 'option2' ] and bind them to the <li> like: <li *ngFor="#option of options" (click)="select(option)">{{value.label}}</li> or you can pass the list from the parent component like thierry's example. - Langley
it depends on what you want to do, if you want to reuse the whole dropdown yea, just pass the options array as an input of the component and use that to render each of the <li> inside - Langley

6 Answers

55
votes

I would say that it depends on what you want to do.

If your dropdown is a component for a form that manages a state, I would leverage the two-way binding of Angular2. For this, I would use two attributes: an input one to get the associated object and an output one to notify when the state changes.

Here is a sample:

export class DropdownValue {
  value:string;
  label:string;

  constructor(value:string,label:string) {
    this.value = value;
    this.label = label;
  }
}

@Component({
  selector: 'dropdown',
  template: `
    <ul>
      <li *ngFor="let value of values" (click)="select(value.value)">{{value.label}}</li>
    </ul>
  `
})
export class DropdownComponent {
  @Input()
  values: DropdownValue[];

  @Input()
  value: string[];

  @Output()
  valueChange: EventEmitter;

  constructor(private elementRef:ElementRef) {
    this.valueChange = new EventEmitter();
  }

  select(value) {
    this.valueChange.emit(value);
  }
}

This allows you to use it this way:

<dropdown [values]="dropdownValues" [(value)]="value"></dropdown>

You can build your dropdown within the component, apply styles and manage selections internally.

Edit

You can notice that you can either simply leverage a custom event in your component to trigger the selection of a dropdown. So the component would now be something like this:

export class DropdownValue {
  value:string;
  label:string;

  constructor(value:string,label:string) {
    this.value = value;
    this.label = label;
  }
}

@Component({
  selector: 'dropdown',
  template: `
    <ul>
      <li *ngFor="let value of values" (click)="selectItem(value.value)">{{value.label}}</li>
    </ul>
  `
})
export class DropdownComponent {
  @Input()
  values: DropdownValue[];

  @Output()
  select: EventEmitter;

  constructor() {
    this.select = new EventEmitter();
  }

  selectItem(value) {
    this.select.emit(value);
  }
}

Then you can use the component like this:

<dropdown [values]="dropdownValues" (select)="action($event.value)"></dropdown>

Notice that the action method is the one of the parent component (not the dropdown one).

3
votes

Hope this will help to someone. Works fine in Angular 6 with reactive forms. Can operate by keyboard too.

dropdown.component.html

<div class="dropdown-wrapper {{className}} {{isFocused ? 'focus':''}}" [ngClass]="{'is-open':isOpen, 'disabled':isReadOnly}" *ngIf="options" (contextmenu)="$event.stopPropagation();">
  <div class="box" (click)="toggle($event)">
    <ng-container>
      <div class="dropdown-selected" *ngIf="isSelectedValue" l10nTranslate><span>{{options[selected]}}</span></div>
      <div class="dropdown-selected" *ngIf="!isSelectedValue" l10nTranslate><span>{{placeholder}}</span></div>
    </ng-container>
  </div>

  <ul class="dropdown-options" *ngIf="options">
    <li *ngIf="placeholder" (click)="$event.stopPropagation()">{{placeholder}}</li>
    <ng-container>
      <li id="li{{i}}"
        *ngFor="let option of options; let i = index"
        [class.active]="selected === i"
        (click)="optionSelect(option, i, $event)"
        l10nTranslate
      >
        {{option}}
      </li>
    </ng-container>

  </ul>
</div>

dropdown.component.scss

@import "../../../assets/scss/variables";

// DROPDOWN STYLES
.dropdown-wrapper {
    display: -webkit-box;
    display: -ms-flexbox;
    display: flex;
    border: 1px solid #DDDDDD;
    border-radius: 3px;
    cursor: pointer;
    position: relative;
    &.focus{
        border: 1px solid #a8a8a8;
    }
    .box {
        display: -webkit-box;
        display: -ms-flexbox;
        display: flex;
        width: 100%;
    } 

    // SELECTED
    .dropdown-selected {
        height: 30px;
        position: relative;
        padding: 10px 30px 10px 10px;
        display: -webkit-box;
        display: -ms-flexbox;
        display: flex;
        -webkit-box-align: center;
            -ms-flex-align: center;
                align-items: center;
        width: 100%;
        font-size: 12px;
        color: #666666;
        overflow: hidden;
        background-color: #fff;
        &::before {
            content: "";
            position: absolute;
            top: 50%;
            right: 5px;
            -webkit-transform: translateY(-50%);
                    transform: translateY(-50%);
            width: 22px;
            height: 22px;
            background: url('/assets/i/dropdown-open-selector.svg');
            background-size: 22px 22px;
        }
        span {
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
        }
    } 

    // DROPDOWN OPTIONS
    .dropdown-options {
        display: none;
        position: absolute;
        padding: 8px 6px 9px 5px;
        max-height: 261px;
        overflow-y: auto;
        z-index: 999;
        li {
            padding: 10px 25px 10px 10px;
            font-size: $regular-font-size;
            color: $content-text-black;
            position: relative;
            line-height: 10px;
            &:last-child {
                border-bottom: none;
            }
            &:hover {
                background-color: #245A88;
                border-radius: 3px;
                color: #fff;
                border-bottom-color: transparent;
            }
            &:focus{
                background-color: #245A88;
                border-radius: 3px;
                color: #fff;
            }
            &.active {
                background-color: #245A88;
                border-radius: 3px;
                color: #fff;
                border-bottom-color: transparent;
            }
            &:hover {
                background-color: #7898B3
            }
            &.active {
                font-weight: 600;
            }
        }
    }
    &.is-open {
        .dropdown-selected {
            &::before {
                content: "";
                position: absolute;
                top: 50%;
                right: 5px;
                -webkit-transform: translateY(-50%);
                        transform: translateY(-50%);
                width: 22px;
                height: 22px;
                background: url('/assets/i/dropdown-close-selector.svg');
                background-size: 22px 22px;
            }
        }
        .dropdown-options {
            display: -webkit-box;
            display: -ms-flexbox;
            display: flex;
            -webkit-box-orient: vertical;
            -webkit-box-direction: normal;
                -ms-flex-direction: column;
                    flex-direction: column;
            width: 100%;
            top: 32px;
            border-radius: 3px;
            background-color: #ffffff;
            border: 1px solid #DDDDDD;
            -webkit-box-shadow: 0px 3px 11px 0 rgba(1, 2, 2, 0.14);
                    box-shadow: 0px 3px 11px 0 rgba(1, 2, 2, 0.14);
        }
    }
    &.data-input-fields {
        .box {
            height: 35px;
        }
    }
    &.send-email-table-select {
        min-width: 140px;
        border: none;
    }
    &.persoanal-settings {
        width: 80px;
    }
}

div.dropdown-wrapper.disabled
{
  pointer-events: none;
  background-color: #F1F1F1;
  opacity: 0.7;
}

dropdown.component.ts

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

const noop = () => {
};

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

@Component({
  selector: 'app-dropdown',
  templateUrl: './dropdown.component.html',
  styleUrls: ['./dropdown.component.scss'],
  providers: [CUSTOM_INPUT_CONTROL_VALUE_ACCESSOR]
})
export class DropdownComponent implements OnInit, ControlValueAccessor {

  @Input() options: Array<string>;
  @Input() selected: number;
  @Input() className: string;
  @Input() placeholder: string;
  @Input() isReadOnly = false;
  @Output() optSelect = new EventEmitter();
  isOpen = false;
  selectedOption;


  private onTouchedCallback: () => void = noop;
  private onChangeCallback: (_: any) => void = noop;
  isSelectedValue: boolean;
  key: string;
  isFocused: boolean;

  /**
   *Creates an instance of DropdownComponent.
   * @memberof DropdownComponent
   */

  ngOnInit() {
    // Place default value in dropdown
    if (this.selected) {
      this.placeholder = '';
      this.isOpen = false;
    }
  }

  @HostListener('focus')
  focusHandler() {
    this.selected = 0;
    this.isFocused = true;
  }

  @HostListener('focusout')
  focusOutHandler() {
    this.isFocused = false;
  }

  @HostListener('document:keydown', ['$event'])
  keyPressHandle(event: KeyboardEvent) {

    if (this.isFocused) {
      this.key = event.code;
      switch (this.key) {
        case 'Space':
          this.isOpen = true;
          break;
        case 'ArrowDown':
          if (this.options.length - 1 > this.selected) {
            this.selected = this.selected + 1;
          }
          break;
        case 'ArrowUp':
          if (this.selected > 0) {
            this.selected = this.selected - 1;
          }
          break;
        case 'Enter':
          if (this.selected > 0) {
            this.isSelectedValue = true;
            this.isOpen = false;
            this.onChangeCallback(this.selected);
            this.optSelect.emit(this.options[this.selected]);
          }
          break;
      }
    }

  }

  /**
  * option selection
  * @param {string} selectedOption - text
  * @param {number} idx - current index of item
  * @param {any} event - object
  */
  optionSelect(selectedOption: string, idx, e: any) {
    e.stopPropagation();
    this.selected = idx;
    this.isSelectedValue = true;
    // this.placeholder = '';
    this.isOpen = false;
    this.onChangeCallback(this.selected);
    this.optSelect.emit(selectedOption);
  }

  /**
  * toggle the dropdown
  * @param {any} event object
  */
  toggle(e: any) {
    e.stopPropagation();
    // close all previously opened dropdowns, before open
    const allElems = document.querySelectorAll('.dropdown-wrapper');
    for (let i = 0; i < allElems.length; i++) {
      allElems[i].classList.remove('is-open');
    }
    this.isOpen = !this.isOpen;
    if (this.selected >= 0) {
      document.querySelector('#li' + this.selected).scrollIntoView(true);
    }
  }

  /**
  * dropdown click on outside
  */
  @HostListener('document: click', ['$event'])
  onClick() {
    this.isOpen = false;
  }

  /**
   * Method implemented from ControlValueAccessor and set default selected value
   * @param {*} obj
   * @memberof DropdownComponent
   */
  writeValue(obj: any): void {
    if (obj && obj !== '') {
      this.isSelectedValue = true;
      this.selected = obj;
    } else {
      this.isSelectedValue = false;
    }
  }

  // From ControlValueAccessor interface
  registerOnChange(fn: any) {
    this.onChangeCallback = fn;
  }

  // From ControlValueAccessor interface
  registerOnTouched(fn: any) {
    this.onTouchedCallback = fn;
  }

  setDisabledState?(isDisabled: boolean): void {

  }

}

Usage

<app-dropdown formControlName="type" [options]="types" [placeholder]="captureData.type" [isReadOnly]="isReadOnly">
</app-dropdown>

Options must bind an array as follows. It can change based on the requirement.

types= [
        {
            "id": "1",
            "value": "Type 1"
        },
        {
             "id": "2",
             "value": "Type 2"
        },
        {
              "id": "3",
              "value": "Type 3"
        }] 
1
votes

If you want to use bootstrap dropdowns, I will recommend this for angular2:

ngx-dropdown

1
votes

This is the code to create dropdown in Angular 7, 8, 9

.html file code

<div>
<label>Summary: </label>
<select (change)="SelectItem($event.target.value)" class="select">
  <option value="0">--All--</option>
  <option *ngFor="let item of items" value="{{item.Id.Value}}">
    {{item.Name}}
  </option>
</select>
</div>

.ts file code

SelectItem(filterVal: any)
{
    var id=filterVal;
    //code
}

items is an array which should be initialized in .ts file.

0
votes

This might not exactly you want but I've used jquery smartmenu(https://github.com/vadikom/smartmenus) to build a ng2 dropdown menu.

 $('#main-menu').smartmenus();

http://plnkr.co/edit/wLqLUoBQYgcDwOgSoRfF?p=preview https://github.com/Longfld/DynamicaLoadMultiLevelDropDownMenu

0
votes

If you want something with a dropdown (some list of values) and a user specified value that can be filled into the selected input as well. This custom dropdown in angular also has a filter dropdown list on key value entered. Please check this stackblitzlink -> https://stackblitz.com/edit/angular-l9guzo?embed=1&file=src/app/custom-textarea.component.ts