From 5f11bb9dd8a9d93b09f48bba5fed4280ee7d3078 Mon Sep 17 00:00:00 2001 From: Gery Hirschfeld Date: Wed, 2 Nov 2022 14:43:19 +0100 Subject: [PATCH] fix(select): typeahead + remote accept initial value. Closes #603, #819 --- .../bal-select-option/bal-select-option.tsx | 10 +- .../components/form/bal-select/bal-select.tsx | 237 +++++++++++------- .../bal-select/stories/bal-select.stories.ts | 4 +- 3 files changed, 158 insertions(+), 93 deletions(-) diff --git a/packages/components/src/components/form/bal-select/bal-select-option/bal-select-option.tsx b/packages/components/src/components/form/bal-select/bal-select-option/bal-select-option.tsx index be179c9e9c..8734c1e1ee 100644 --- a/packages/components/src/components/form/bal-select/bal-select-option/bal-select-option.tsx +++ b/packages/components/src/components/form/bal-select/bal-select-option/bal-select-option.tsx @@ -1,9 +1,17 @@ import { Component, ComponentInterface, h, Host, Prop } from '@stencil/core' +import { Loggable, Logger, LogInstance } from '../../../../utils/log' @Component({ tag: 'bal-select-option', }) -export class SelectOption implements ComponentInterface { +export class SelectOption implements ComponentInterface, Loggable { + log!: LogInstance + + @Logger('bal-select-option') + createLogger(log: LogInstance) { + this.log = log + } + /** * Label will be shown in the input element when it got selected */ diff --git a/packages/components/src/components/form/bal-select/bal-select.tsx b/packages/components/src/components/form/bal-select/bal-select.tsx index f257123658..32f176ab91 100644 --- a/packages/components/src/components/form/bal-select/bal-select.tsx +++ b/packages/components/src/components/form/bal-select/bal-select.tsx @@ -1,4 +1,17 @@ -import { Component, h, Host, State, Prop, Watch, EventEmitter, Event, Method, Element, Listen } from '@stencil/core' +import { + Component, + h, + Host, + State, + Prop, + Watch, + EventEmitter, + Event, + Method, + Element, + Listen, + ComponentInterface, +} from '@stencil/core' import isNil from 'lodash.isnil' import isArray from 'lodash.isarray' import { debounce, deepReady, findItemLabel, isDescendant } from '../../../utils/helpers' @@ -28,6 +41,7 @@ import { BalOptionValue } from './utils/bal-option.type' import { Props, Events } from '../../../types' import { stopEventBubbling } from '../../../utils/form-input' import { BEM } from '../../../utils/bem' +import { Loggable, Logger, LogInstance } from '../../../utils/log' export interface BalOptionController extends BalOptionValue { id: string @@ -38,9 +52,7 @@ export interface BalOptionController extends BalOptionValue { @Component({ tag: 'bal-select', }) -export class Select { - @Element() private el!: HTMLElement - +export class Select implements ComponentInterface, Loggable { private inputElement!: HTMLInputElement private nativeSelectEl!: HTMLSelectElement private popoverElement!: HTMLBalPopoverElement @@ -52,6 +64,15 @@ export class Select { private mutationO?: MutationObserver private initialValue?: string | string[] = [] + log!: LogInstance + + @Logger('bal-select') + createLogger(log: LogInstance) { + this.log = log + } + + @Element() private el!: HTMLElement + @State() hasFocus = false @State() inputValue = '' @State() focusIndex = 0 @@ -60,6 +81,11 @@ export class Select { @State() labelToScrollTo = '' @State() labelToSelectTo = '' + /** + * PUBLIC PROPERTY API + * ------------------------------------------------------ + */ + /** * The name of the control, which is submitted with the form data. */ @@ -231,6 +257,55 @@ export class Select { */ @Event() balKeyPress!: EventEmitter + /** + * LIFECYCLE + * ------------------------------------------------------ + */ + + connectedCallback() { + const debounceUpdateOptions = debounce(() => this.updateOptions(), 0) + + this.initialValue = this.value + + debounceUpdateOptions() + + this.mutationO = watchForOptions(this.el, 'bal-select-option', () => { + debounceUpdateOptions() + }) + } + + componentWillLoad() { + this.waitForOptionsAndThenUpdateRawValues() + + if (!isNil(this.rawValue) && this.options.size > 0 && length(this.rawValue) === 1) { + const firstOption = this.options.get(this.rawValue[0]) + if (!isNil(firstOption)) { + this.inputValue = firstOption.label + } + } + } + + componentDidLoad() { + this.updateRawValue(false) + + if (!this.multiple) { + this.inputElement.value = this.inputValue + } + this.didInit = true + } + + disconnectedCallback() { + if (this.mutationO) { + this.mutationO.disconnect() + this.mutationO = undefined + } + } + + /** + * LISTENERS + * ------------------------------------------------------ + */ + @Listen('click', { capture: true, target: 'document' }) listenOnClick(event: UIEvent) { if (this.disabled && event.target && event.target === this.el) { @@ -301,88 +376,10 @@ export class Select { } } - connectedCallback() { - const debounceUpdateOptions = debounce(() => this.updateOptions(), 0) - - this.initialValue = this.value - - debounceUpdateOptions() - - this.mutationO = watchForOptions(this.el, 'bal-select-option', () => { - debounceUpdateOptions() - }) - } - - disconnectedCallback() { - if (this.mutationO) { - this.mutationO.disconnect() - this.mutationO = undefined - } - } - - waitForOptionsAndThenUpdateRawValuesTimer?: NodeJS.Timer - async waitForOptionsAndThenUpdateRawValues() { - clearTimeout(this.waitForOptionsAndThenUpdateRawValuesTimer) - await deepReady(this.el) - const hasOptions = this.options.size > 0 - - if (hasOptions) { - this.updateRawValue(false) - } else { - this.waitForOptionsAndThenUpdateRawValuesTimer = setTimeout(() => this.waitForOptionsAndThenUpdateRawValues(), 10) - } - } - - componentWillLoad() { - this.waitForOptionsAndThenUpdateRawValues() - - if (!isNil(this.rawValue) && this.options.size > 0 && length(this.rawValue) === 1) { - const firstOption = this.options.get(this.rawValue[0]) - if (!isNil(firstOption)) { - this.inputValue = firstOption.label - } - } - } - - componentDidLoad() { - this.updateRawValue(false) - - if (!this.multiple) { - this.inputElement.value = this.inputValue - } - this.didInit = true - } - - private updateOptions() { - const optionElements = this.getChildOpts() - const options = new Map() - for (let index = 0; index < optionElements.length; index++) { - const element = optionElements[index] - options.set(element.value, { - value: element.value, - label: element.label, - disabled: element.disabled, - id: element.for, - textContent: element.textContent, - innerHTML: element.innerHTML, - }) - } - if (!this.selectionOptional && Array.isArray(this.rawValue)) { - for (let index = 0; index < this.rawValue.length; index++) { - const val = this.rawValue[index] - if (!options.has(val)) { - this.rawValue = removeValue(this.rawValue, val) - } - } - } - this.options = new Map(options) - this.syncNativeInput() - if (!this.remote && this.didInit) { - this.validateAfterBlur() - } - } - - private setFocusTimer?: NodeJS.Timer + /** + * PUBLIC METHODS + * ------------------------------------------------------ + */ /** * Sets the focus on the input element @@ -460,9 +457,61 @@ export class Select { } } - /******************************************************** + /** + * PRIVATE METHODS + * ------------------------------------------------------ + */ + + private waitForOptionsAndThenUpdateRawValuesTimer?: NodeJS.Timer + private async waitForOptionsAndThenUpdateRawValues() { + clearTimeout(this.waitForOptionsAndThenUpdateRawValuesTimer) + await deepReady(this.el) + const hasOptions = this.options.size > 0 + + if (hasOptions) { + this.updateRawValue(false) + } else { + this.waitForOptionsAndThenUpdateRawValuesTimer = setTimeout(() => this.waitForOptionsAndThenUpdateRawValues(), 10) + } + } + + private updateOptions() { + const optionElements = this.getChildOpts() + const options = new Map() + for (let index = 0; index < optionElements.length; index++) { + const element = optionElements[index] + options.set(element.value, { + value: element.value, + label: element.label, + disabled: element.disabled, + id: element.for, + textContent: element.textContent, + innerHTML: element.innerHTML, + }) + } + if (!this.selectionOptional && Array.isArray(this.rawValue)) { + for (let index = 0; index < this.rawValue.length; index++) { + const val = this.rawValue[index] + if (!options.has(val)) { + this.rawValue = removeValue(this.rawValue, val) + } + } + } + this.options = new Map(options) + if (!this.remote) { + this.syncNativeInput() + if (this.didInit) { + this.validateAfterBlur() + } + } + } + + private setFocusTimer?: NodeJS.Timer + + /** * GETTERS - ********************************************************/ + * ------------------------------------------------------ + */ private get optionArray() { const options = Array.from(this.options, ([_, value]) => value) @@ -733,9 +782,10 @@ export class Select { } } - /******************************************************** - * EVENT HANDLERS - ********************************************************/ + /** + * EVENT BINDING + * ------------------------------------------------------ + */ private handleClick = (event: MouseEvent) => { if (this.disabled || this.readonly) { @@ -842,6 +892,11 @@ export class Select { this.focusIndex = index } + /** + * RENDER + * ------------------------------------------------------ + */ + render() { const labelId = this.inputId + '-lbl' const label = findItemLabel(this.el) diff --git a/packages/components/src/components/form/bal-select/stories/bal-select.stories.ts b/packages/components/src/components/form/bal-select/stories/bal-select.stories.ts index ef2809a76b..4dcbd815eb 100644 --- a/packages/components/src/components/form/bal-select/stories/bal-select.stories.ts +++ b/packages/components/src/components/form/bal-select/stories/bal-select.stories.ts @@ -112,7 +112,7 @@ Typeahead.parameters = { } export const TypeaheadRemote = args => ({ - components: { ...component.components }, + components: { ...component.components, BalField, BalFieldLabel, BalFieldControl }, setup: () => { const cantons = [ 'Zürich', @@ -197,6 +197,8 @@ TypeaheadRemote.args = { typeahead: true, remote: true, loading: true, + selectionOptional: true, + value: 'Ticino', placeholder: 'Try finding your canton', } TypeaheadRemote.parameters = {