import { Component, Element, Event, EventEmitter, Listen, Method, Prop, State, Watch } from '@stencil/core'; import { ActionSheetButton, ActionSheetOptions, AlertOptions, CssClassMap, Mode, PopoverOptions, SelectInputChangeEvent, SelectInterface, SelectPopoverOption, StyleEvent } from '../../interface'; import { deferEvent, renderHiddenInput } from '../../utils/helpers'; import { hostContext } from '../../utils/theme'; @Component({ tag: 'ion-select', styleUrls: { ios: 'select.ios.scss', md: 'select.md.scss' }, shadow: true }) export class Select { private childOpts: HTMLIonSelectOptionElement[] = []; private inputId = `ion-sel-${selectIds++}`; private labelId?: string; private overlay?: HTMLIonActionSheetElement | HTMLIonAlertElement | HTMLIonPopoverElement; @Element() el!: HTMLIonSelectElement; @Prop({ connect: 'ion-action-sheet-controller' }) actionSheetCtrl!: HTMLIonActionSheetControllerElement; @Prop({ connect: 'ion-alert-controller' }) alertCtrl!: HTMLIonAlertControllerElement; @Prop({ connect: 'ion-popover-controller' }) popoverCtrl!: HTMLIonPopoverControllerElement; @State() isExpanded = false; @State() keyFocus = false; @State() text?: string; /** * The mode determines which platform styles to use. * Possible values are: `"ios"` or `"md"`. */ @Prop() mode!: Mode; /** * If true, the user cannot interact with the select. Defaults to `false`. */ @Prop() disabled = false; /** * The text to display on the cancel button. Default: `Cancel`. */ @Prop() cancelText = 'Cancel'; /** * The text to display on the ok button. Default: `OK`. */ @Prop() okText = 'OK'; /** * The text to display when the select is empty. */ @Prop() placeholder?: string; /** * The name of the control, which is submitted with the form data. */ @Prop() name: string = this.inputId; /** * The text to display instead of the selected option's value. */ @Prop() selectedText?: string; /** * If true, the select can accept multiple values. */ @Prop() multiple = false; /** * The interface the select should use: `action-sheet`, `popover` or `alert`. Default: `alert`. */ @Prop() interface: SelectInterface = 'alert'; /** * Any additional options that the `alert`, `action-sheet` or `popover` interface * can take. See the [AlertController API docs](../../alert/AlertController/#create), the * [ActionSheetController API docs](../../action-sheet/ActionSheetController/#create) and the * [PopoverController API docs](../../popover/PopoverController/#create) for the * create options for each interface. */ @Prop() interfaceOptions: any = {}; /** * the value of the select. */ @Prop({ mutable: true }) value?: any | null; /** * Emitted when the value has changed. */ @Event() ionChange!: EventEmitter; /** * Emitted when the selection is cancelled. */ @Event() ionCancel!: EventEmitter; /** * Emitted when the select has focus. */ @Event() ionFocus!: EventEmitter; /** * Emitted when the select loses focus. */ @Event() ionBlur!: EventEmitter; /** * Emitted when the styles change. */ @Event() ionStyle!: EventEmitter; @Watch('disabled') disabledChanged() { this.emitStyle(); } @Watch('value') valueChanged() { // this select value just changed // double check the select option with this value is checked if (this.value === undefined) { // set to undefined // ensure all that are checked become unchecked this.childOpts.filter(o => o.selected).forEach(selectOption => { selectOption.selected = false; }); this.text = ''; } else { let hasChecked = false; const texts: string[] = []; this.childOpts.forEach(selectOption => { if ((Array.isArray(this.value) && this.value.includes(selectOption.value)) || (selectOption.value === this.value)) { if (!selectOption.selected && (this.multiple || !hasChecked)) { // correct value for this select option // but this select option isn't checked yet // and we haven't found a checked yet // so CHECK IT! selectOption.selected = true; } else if (!this.multiple && hasChecked && selectOption.selected) { // somehow we've got multiple select options // with the same value, but only one can be checked selectOption.selected = false; } // remember we've got a checked select option button now hasChecked = true; } else if (selectOption.selected) { // this select option doesn't have the correct value // and it's also checked, so let's uncheck it selectOption.selected = false; } if (selectOption.selected) { texts.push(selectOption.textContent || ''); } }); this.text = texts.join(', '); } // emit the new value this.ionChange.emit({ value: this.value, text: this.text }); this.emitStyle(); } @Listen('ionSelectOptionDidLoad') optLoad(ev: CustomEvent) { const selectOption = ev.target as HTMLIonSelectOptionElement; this.childOpts = Array.from(this.el.querySelectorAll('ion-select-option')); if (this.value != null && (Array.isArray(this.value) && this.value.includes(selectOption.value)) || (selectOption.value === this.value)) { // this select has a value and this // option equals the correct select value // so let's check this select option selectOption.selected = true; } else if (Array.isArray(this.value) && this.multiple && selectOption.selected) { // if the value is an array we need to push the option on this.value.push(selectOption.value); } else if (this.value === undefined && selectOption.selected) { // this select does not have a value // but this select option is checked, so let's set the // select's value from the checked select option this.value = selectOption.value; } else if (selectOption.selected) { // if it doesn't match one of the above cases, but the // select option is still checked, then we need to uncheck it selectOption.selected = false; } } @Listen('ionSelectOptionDidUnload') optUnload(ev: CustomEvent) { const index = this.childOpts.indexOf(ev.target as HTMLIonSelectOptionElement); if (index > -1) { this.childOpts.splice(index, 1); } } @Listen('ionSelect') onSelect(ev: CustomEvent) { // ionSelect only come from the checked select option this.childOpts.forEach(selectOption => { if (selectOption === ev.target) { this.value = selectOption.value; } else { selectOption.selected = false; } }); } componentWillLoad() { if (!this.value) { this.value = this.multiple ? [] : undefined; } } componentDidLoad() { this.ionStyle = deferEvent(this.ionStyle); const label = this.getLabel(); if (label) { this.labelId = label.id = this.name + '-lbl'; } if (this.multiple) { // there are no values set at this point // so check to see who should be selected const checked = this.childOpts.filter(o => o.selected); (this.value as string[]).length = 0; checked.forEach(o => { // doing this instead of map() so we don't // fire off an unecessary change event (this.value as string[]).push(o.value); }); this.text = checked.map(o => o.textContent).join(', '); } else { const checked = this.childOpts.find(o => o.selected); if (checked) { this.value = checked.value; this.text = checked.textContent || ''; } } this.emitStyle(); } @Method() open(ev?: UIEvent): Promise { let selectInterface = this.interface; if ((selectInterface === 'action-sheet' || selectInterface === 'popover') && this.multiple) { console.warn(`Select interface cannot be "${selectInterface}" with a multi-value select. Using the "alert" interface instead.`); selectInterface = 'alert'; } if (selectInterface === 'popover' && !ev) { console.warn('Select interface cannot be a "popover" without passing an event. Using the "alert" interface instead.'); selectInterface = 'alert'; } if (selectInterface === 'popover') { return this.openPopover(ev!); } if (selectInterface === 'action-sheet') { return this.openActionSheet(); } return this.openAlert(); } private getLabel() { const item = this.el.closest('ion-item'); if (item) { return item.querySelector('ion-label'); } return null; } private async openPopover(ev: UIEvent) { const interfaceOptions = this.interfaceOptions; const popoverOpts: PopoverOptions = { ...interfaceOptions, component: 'ion-select-popover', cssClass: ['select-popover', interfaceOptions.cssClass], event: ev, componentProps: { header: interfaceOptions.header, subHeader: interfaceOptions.subHeader, message: interfaceOptions.message, value: this.value, options: this.childOpts.map(o => { return { text: o.textContent, value: o.value, checked: o.selected, disabled: o.disabled, handler: () => { this.value = o.value; // tslint:disable-next-line:no-floating-promises this.close(); } } as SelectPopoverOption; }) } }; const popover = this.overlay = await this.popoverCtrl.create(popoverOpts); await popover.present(); this.isExpanded = true; return popover; } private async openActionSheet() { const actionSheetButtons = this.childOpts.map(option => { return { role: (option.selected ? 'selected' : ''), text: option.textContent, handler: () => { this.value = option.value; } } as ActionSheetButton; }); actionSheetButtons.push({ text: this.cancelText, role: 'cancel', handler: () => { this.ionCancel.emit(); } }); const interfaceOptions = this.interfaceOptions; const actionSheetOpts: ActionSheetOptions = { ...interfaceOptions, buttons: actionSheetButtons, cssClass: ['select-action-sheet', interfaceOptions.cssClass] }; const actionSheet = this.overlay = await this.actionSheetCtrl.create(actionSheetOpts); await actionSheet.present(); this.isExpanded = true; return actionSheet; } private async openAlert() { const label = this.getLabel(); const labelText = (label) ? label.textContent : null; const interfaceOptions = this.interfaceOptions; const inputType = (this.multiple ? 'checkbox' : 'radio'); const alertOpts: AlertOptions = { ...interfaceOptions, header: interfaceOptions.header ? interfaceOptions.header : labelText, inputs: this.childOpts.map(o => { return { type: inputType, label: o.textContent, value: o.value, checked: o.selected, disabled: o.disabled }; }), buttons: [ { text: this.cancelText, role: 'cancel', handler: () => { this.ionCancel.emit(); } }, { text: this.okText, handler: (selectedValues: any) => { this.value = selectedValues; } } ], cssClass: ['select-alert', interfaceOptions.cssClass, (this.multiple ? 'multiple-select-alert' : 'single-select-alert')] }; const alert = this.overlay = await this.alertCtrl.create(alertOpts); await alert.present(); this.isExpanded = true; return alert; } /** * Close the select interface. */ private close(): Promise { // TODO check !this.overlay || !this.isFocus() if (!this.overlay) { return Promise.resolve(false); } const overlay = this.overlay; this.overlay = undefined; this.isExpanded = false; return overlay.dismiss(); } onKeyUp() { this.keyFocus = true; } onFocus() { this.ionFocus.emit(); } onBlur() { this.keyFocus = false; this.ionBlur.emit(); } hasValue(): boolean { if (Array.isArray(this.value)) { return this.value.length > 0; } return (this.value != null && this.value !== undefined && this.value !== ''); } private emitStyle() { this.ionStyle.emit({ 'interactive': true, 'select': true, 'has-value': this.hasValue(), 'interactive-disabled': this.disabled, 'select-disabled': this.disabled }); } hostData() { return { class: { 'in-item': hostContext('.item', this.el), 'select-disabled': this.disabled, 'select-key': this.keyFocus } }; } render() { renderHiddenInput(this.el, this.name, parseValue(this.value), this.disabled); let addPlaceholderClass = false; let selectText = this.selectedText || this.text; if (selectText === undefined && this.placeholder !== undefined) { selectText = this.placeholder; addPlaceholderClass = true; } const selectTextClasses: CssClassMap = { 'select-text': true, 'select-placeholder': addPlaceholderClass }; return [
{selectText}
, , ]; } } function parseValue(value: any) { if (value == null) { return undefined; } if (Array.isArray(value)) { return value.join(','); } return value.toString(); } let selectIds = 0;