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.

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

55 users liked answer #0dislike answer #055
Thierry Templier profile pic
Thierry Templier

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"
        }] 
3 users liked answer #1dislike answer #13
Janith Widarshana profile pic
Janith Widarshana

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.

2 users liked answer #2dislike answer #22
R15 profile pic
R15

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

ngx-dropdown

1 users liked answer #3dislike answer #31
Dudi profile pic
Dudi

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 users liked answer #4dislike answer #40
Long Field profile pic
Long Field

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

0 users liked answer #5dislike answer #50
Vah Run profile pic
Vah Run

Copyright © 2022 QueryThreads

All content on Query Threads is licensed under the Creative Commons Attribution-ShareAlike 3.0 license (CC BY-SA 3.0).