File

src/combobox/combobox.component.ts

Description

ComboBoxes are similar to dropdowns, except a combobox provides an input field for users to search items and (optionally) add their own. Multi-select comboboxes also provide "pills" of selected items.

Implements

OnChanges OnInit AfterViewInit AfterContentInit

Example

Metadata

providers { : , : , : true }
selector ibm-combo-box

Index

Properties
Methods
Inputs
Outputs
HostBindings
HostListeners

Constructor

constructor(elementRef: ElementRef)

Creates an instance of ComboBox.

Parameters :
Name Type Optional Description
elementRef ElementRef

Inputs

disabled

Set to true to disable combobox.

Default value: false

items

List of items to fill the content with.

Example:

items = [
        {
            content: "Abacus",
            selected: false
        },
        {
            content: "Byte",
            selected: false,
        },
        {
            content: "Computer",
            selected: false
        },
        {
            content: "Digital",
            selected: false
        }
];

Type: Array<ListItem>

placeholder

Text to show when nothing is selected.

Default value: Filter...

size

Combo box render size.

Type: "sm" | "md" | "lg"

Default value: md

type

Combo box type (supporting single or multi selection of items).

Type: "single" | "multi"

Default value: single

Outputs

close

emits an empty event when the menu is closed

$event type: EventEmitter<any>
selected

Emits a ListItem

Example:

{
         content: "one",
         selected: true
}
$event type: EventEmitter<ListItem>
submit

Bubbles from n-pill-input when the user types a value and presses enter. Intended to be used to add items to the list.

Emits an event that includes the current item list, the suggested index for the new item, and a simple ListItem

Example:

{
        items: [{content: "one", selected: true}, {content: "two", selected: true}],
        index: 1,
        value: {
            content: "some user string",
            selected: false
        }
}
$event type: EventEmitter<any>

HostBindings

attr.role
attr.role:
Default value : combobox
class
class:
Default value : bx--combo-box bx--list-box
style.display
style.display:
Default value : block

HostListeners

keydown
Arguments : '$event'
keydown(ev: KeyboardEvent)

Methods

_noop
_noop()
Returns : void
Public clearSelected
clearSelected()
Returns : void
Public closeDropdown
closeDropdown()

Closes the dropdown and emits the close event.

Returns : void
ngAfterContentInit
ngAfterContentInit()

Sets initial state that depends on child components Subscribes to select events and handles focus/filtering/initial list updates

Returns : void
ngAfterViewInit
ngAfterViewInit()

Binds event handlers against the rendered view

Returns : void
ngOnChanges
ngOnChanges(changes: )

Lifecycle hook. Updates pills if necessary.

Parameters :
Name Type Optional Description
changes
Returns : void
ngOnInit
ngOnInit()
Returns : void
onBlur
onBlur()
Returns : void
Public onSearch
onSearch(searchString: )

Sets the list group filter, and manages single select item selection.

Parameters :
Name Type Optional Description
searchString
Returns : void
Public onSubmit
onSubmit(ev: )

Bubbles from n-pill-input when the user types a value and presses enter. Intended to be used to add items to the list.

Parameters :
Name Type Optional Description
ev

event from n-pill-input, includes the typed value and the index of the pill the user typed after

Example:

{
    after: 1,
    value: "some user string"
    }
Returns : void
Public openDropdown
openDropdown()

Opens the dropdown.

Returns : void
registerOnChange
registerOnChange(fn: any)
Parameters :
Name Type Optional Description
fn any
Returns : void
registerOnTouched
registerOnTouched(fn: any)
Parameters :
Name Type Optional Description
fn any
Returns : void
Public toggleDropdown
toggleDropdown()

Toggles the dropdown.

Returns : void
Public updatePills
updatePills()

Called by n-pill-input when the selected pills have changed.

Returns : void
Private updateSelected
updateSelected()
Returns : void
writeValue
writeValue(value: any)
Parameters :
Name Type Optional Description
value any
Returns : void

Properties

dropdownMenu
dropdownMenu:
Decorators : ViewChild
Private noop
noop:
Private onTouchedCallback
onTouchedCallback: function
Type : function
Public open
open:
Default value : false
Public pills
pills:

Selected items for multi-select combo-boxes.

Private propagateChangeCallback
propagateChangeCallback: function
Type : function
Public selectedValue
selectedValue:

used to update the displayValue of n-pill-input

view
view: AbstractDropdownView
Type : AbstractDropdownView
Decorators : ContentChild

ContentChild reference to the instantiated dropdown list

import {
	Component,
	OnChanges,
	ContentChild,
	Input,
	Output,
	HostListener,
	ElementRef,
	ViewChild,
	EventEmitter,
	AfterViewInit,
	AfterContentInit,
	HostBinding,
	OnInit
} from "@angular/core";
import { AbstractDropdownView } from "./../dropdown/abstract-dropdown-view.class";
import { ListItem } from "./../dropdown/list-item.interface";
import { NG_VALUE_ACCESSOR } from "@angular/forms";

/**
 * ComboBoxes are similar to dropdowns, except a combobox provides an input field for users to search items and (optionally) add their own.
 * Multi-select comboboxes also provide "pills" of selected items.
 *
 * @export
 * @class ComboBox
 * @implements {OnChanges}
 * @implements {AfterViewInit}
 * @implements {AfterContentInit}
 */
@Component({
	selector: "ibm-combo-box",
	template: `
		<div
			[attr.aria-expanded]="open"
			role="button"
			class="bx--list-box__field"
			tabindex="0"
			type="button"
			aria-label="close menu"
			aria-haspopup="true">
			<div
				*ngIf="type === 'multi' && pills.length > 0"
				(click)="clearSelected()"
				role="button"
				class="bx--list-box__selection bx--list-box__selection--multi"
				tabindex="0"
				title="Clear all selected items">
				{{ pills.length }}
				<svg
					fill-rule="evenodd"
					height="10"
					role="img"
					viewBox="0 0 10 10"
					width="10"
					focusable="false"
					aria-label="Clear all selected items"
					alt="Clear all selected items">
					<title>Clear all selected items</title>
					<path d="M6.32 5L10 8.68 8.68 10 5 6.32 1.32 10 0 8.68 3.68 5 0 1.32 1.32 0 5 3.68 8.68 0 10 1.32 6.32 5z"></path>
				</svg>
			</div>
			<input
				[disabled]="disabled"
				(click)="toggleDropdown()"
				(keyup)="onSearch($event.target.value)"
				[value]="selectedValue"
				class="bx--text-input"
				aria-label="ListBox input field"
				autocomplete="off"
				[placeholder]="placeholder"/>
			<div
				[ngClass]="{'bx--list-box__menu-icon--open': open}"
				class="bx--list-box__menu-icon">
				<svg
					fill-rule="evenodd"
					height="5"
					role="img"
					viewBox="0 0 10 5"
					width="10"
					alt="Close menu"
					aria-label="Close menu">
					<title>Close menu</title>
					<path d="M0 0l5 4.998L10 0z"></path>
				</svg>
			</div>
		</div>
		<div
			#dropdownMenu
			*ngIf="open">
			<ng-content></ng-content>
		</div>
	`,
	providers: [
		{
			provide: NG_VALUE_ACCESSOR,
			useExisting: ComboBox,
			multi: true
		}
	]
})
export class ComboBox implements OnChanges, OnInit, AfterViewInit, AfterContentInit {
	/**
	 * List of items to fill the content with.
	 *
	 * **Example:**
	 * ```javascript
	 * items = [
	 *		{
	 *			content: "Abacus",
	 *			selected: false
	 *		},
	 *		{
	 *			content: "Byte",
	 *			selected: false,
	 *		},
	 *		{
	 *			content: "Computer",
	 *			selected: false
	 *		},
	 *		{
	 *			content: "Digital",
	 *			selected: false
	 *		}
	 * ];
	 * ```
	 *
	 * @type {Array<ListItem>}
	 * @memberof ComboBox
	 */
	@Input() items: Array<ListItem> = [];
	/**
	 * Text to show when nothing is selected.
	 */
	@Input() placeholder = "Filter...";
	/**
	 * Combo box type (supporting single or multi selection of items).
	 */
	@Input() type: "single" | "multi" = "single";
	/**
	 * Combo box render size.
	 */
	@Input() size: "sm" | "md" | "lg" = "md";
	/**
	 * Set to `true` to disable combobox.
	 */
	@HostBinding("attr.aria-disabled") @Input() disabled = false;
	/**
	 * Emits a ListItem
	 *
	 * Example:
	 * ```javascript
	 * {
	 * 		content: "one",
	 * 		selected: true
	 * }
	 * ```
	 */
	@Output() selected: EventEmitter<ListItem> = new EventEmitter<ListItem>();
	/**
	 * Bubbles from `n-pill-input` when the user types a value and presses enter. Intended to be used to add items to the list.
	 *
	 * Emits an event that includes the current item list, the suggested index for the new item, and a simple ListItem
	 *
	 * Example:
	 * ```javascript
	 *	{
	 *		items: [{content: "one", selected: true}, {content: "two", selected: true}],
	 *		index: 1,
	 *		value: {
	 *			content: "some user string",
	 *			selected: false
	 *		}
	 *	}
	 * ```
	 *
	 * @param ev event from `n-pill-input`, includes the typed value and the index of the pill the user typed after
	 *
	 * Example:
	 * ```javascript
	 * {
	 *	after: 1,
	 *	value: "some user string"
	 * }
	 * ```
	 */
	@Output() submit: EventEmitter<any> = new EventEmitter<any>();
	/** emits an empty event when the menu is closed */
	@Output() close: EventEmitter<any> = new EventEmitter<any>();
	/** ContentChild reference to the instantiated dropdown list */
	@ContentChild(AbstractDropdownView) view: AbstractDropdownView;
	@ViewChild("dropdownMenu") dropdownMenu;
	@HostBinding("class") class = "bx--combo-box bx--list-box";
	@HostBinding("attr.role") role = "combobox";
	@HostBinding("style.display") display = "block";

	public open = false;

	/** Selected items for multi-select combo-boxes. */
	public pills = [];
	/** used to update the displayValue of `n-pill-input` */
	public selectedValue = "";

	private noop = this._noop.bind(this);
	private onTouchedCallback: () => void = this._noop;
	private propagateChangeCallback: (_: any) => void = this._noop;

	/**
	 * Creates an instance of ComboBox.
	 * @param {ElementRef} elementRef
	 * @memberof ComboBox
	 */
	constructor(private elementRef: ElementRef) {}

	/**
	 * Lifecycle hook.
	 * Updates pills if necessary.
	 *
	 * @param {any} changes
	 * @memberof ComboBox
	 */
	ngOnChanges(changes) {
		if (changes.items) {
			this.view["updateList"](changes.items.currentValue);
			this.updateSelected();
		}
	}

	ngOnInit() {
		if (this.type === "multi") {
			this.class = "bx--multi-select bx--combo-box bx--list-box";
		}
	}

	/**
	 * Sets initial state that depends on child components
	 * Subscribes to select events and handles focus/filtering/initial list updates
	 */
	ngAfterContentInit() {
		if (this.view) {
			this.view.type = this.type;
			this.view.select.subscribe(event => {
				if (this.type === "multi") {
					this.updatePills();
					this.propagateChangeCallback(this.view.getSelected());
				} else {
					if (event.item && event.item.selected) {
						this.selectedValue = event.item.content;
						this.propagateChangeCallback(event.item);
					} else {
						this.selectedValue = "";
						this.propagateChangeCallback(null);
					}
					// not gaurding these since the nativeElement has to be loaded
					// for select to even fire
					this.elementRef.nativeElement.querySelector("input").focus();
					this.closeDropdown();
				}
				this.selected.emit(event);
				this.view["filterBy"]("");
			});
			this.view["updateList"](this.items);
			// update the rest of combobox with any pre-selected items
			// setTimeout just defers the call to the next check cycle
			setTimeout(() => {
				this.updateSelected();
			});
		}
	}

	/**
	 * Binds event handlers against the rendered view
	 */
	ngAfterViewInit() {
		document.addEventListener("click", ev => {
			if (!this.elementRef.nativeElement.contains(ev.target)) {
				if (this.open) {
					this.closeDropdown();
				}
			}
		});
	}

	/**
	 * Handles `Escape` key closing the dropdown, and arrow up/down focus to/from the dropdown list.
	 * @param {KeyboardEvent} ev
	 */
	@HostListener("keydown", ["$event"])
	hostkeys(ev: KeyboardEvent) {
		if (ev.key === "Escape") {
			this.closeDropdown();
		} else if (ev.key === "ArrowDown" && !this.dropdownMenu.nativeElement.contains(ev.target)) {
			ev.stopPropagation();
			this.openDropdown();
			setTimeout(() => this.view.getCurrentElement().focus(), 0);
		} else if (ev.key === "ArrowUp" && this.dropdownMenu.nativeElement.contains(ev.target) && !this.view["hasPrevElement"]()) {
			this.elementRef.nativeElement.querySelector(".combobox_input").focus();
		}
	}

	/*
	 * no-op method for null event listeners, and other no op calls
	 */
	_noop() {}

	/*
	 * propagates the value provided from ngModel
	 */
	writeValue(value: any) {
		if (value) {
			if (this.type === "single") {
				this.view.propagateSelected([value]);
			} else {
				this.view.propagateSelected(value);
			}
		}
	}

	onBlur() {
		this.onTouchedCallback();
	}

	registerOnChange(fn: any) {
		this.propagateChangeCallback = fn;
	}

	registerOnTouched(fn: any) {
		this.onTouchedCallback = fn;
	}

	/**
	 * Called by `n-pill-input` when the selected pills have changed.
	 */
	public updatePills() {
		this.pills = this.view.getSelected() || [];
		this.propagateChangeCallback(this.view.getSelected());
	}

	public clearSelected() {
		this.items = this.items.map(item => {
			if (!item.disabled) {
				item.selected = false;
			}
			return item;
		});
		this.view["updateList"](this.items);
		this.updatePills();
		// clearSelected can only fire on type=multi
		// so we just emit getSelected() (just in case there's any disabled but selected items)
		this.selected.emit(this.view.getSelected() as any);
	}

	/**
	 * Closes the dropdown and emits the close event.
	 * @memberof ComboBox
	 */
	public closeDropdown() {
		this.open = false;
		this.close.emit();
	}

	/**
	 * Opens the dropdown.
	 * @memberof ComboBox
	 */
	public openDropdown() {
		this.open = true;
	}

	/**
	 * Toggles the dropdown.
	 * @memberof ComboBox
	 */
	public toggleDropdown() {
		if (this.open) {
			this.closeDropdown();
		} else {
			this.openDropdown();
		}
	}

	/**
	 * Sets the list group filter, and manages single select item selection.
	 * @param {string} searchString
	 */
	public onSearch(searchString) {
		this.view["filterBy"](searchString);
		if (searchString !== "") {
			this.openDropdown();
		} else {
			this.selectedValue = "";
		}
		if (this.type === "single") {
			// deselect if the input doesn't match the content
			// of any given item
			const matches = this.view.items.some(item => item.content.toLowerCase().includes(searchString.toLowerCase()));
			if (!matches) {
				const selected = this.view.getSelected();
				if (selected) {
					selected[0].selected = false;
					// notify that the selection has changed
					this.view.select.emit({ item: selected[0] });
					this.propagateChangeCallback(null);
				} else {
					this.view["filterBy"]("");
				}
			}
		}
	}

	/**
	 * Bubbles from `n-pill-input` when the user types a value and presses enter. Intended to be used to add items to the list.
	 *
	 * @param {any} ev event from `n-pill-input`, includes the typed value and the index of the pill the user typed after
	 *
	 * Example:
	 * ```javascript
	 *	{
	 *	after: 1,
	 *	value: "some user string"
	 *	}
	 * ```
	 */
	public onSubmit(ev) {
		let index = 0;
		if (ev.after) {
			index = this.view.items.indexOf(ev.after) + 1;
		}
		this.submit.emit({
			items: this.view.items,
			index,
			value: {
				content: ev.value,
				selected: false
			}
		});
	}

	private updateSelected() {
		const selected = this.view.getSelected();
		if (selected) {
			if (this.type === "multi") {
				this.updatePills();
			} else {
				this.selectedValue = selected[0].content;
				this.propagateChangeCallback(selected[0]);
			}
		}
	}
}
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""