Skip to content

Commit

Permalink
fix(select): typeahead + remote accept initial value. Closes #603, #819
Browse files Browse the repository at this point in the history
  • Loading branch information
hirsch88 committed Nov 2, 2022
1 parent 5af0d64 commit 5f11bb9
Show file tree
Hide file tree
Showing 3 changed files with 158 additions and 93 deletions.
Original file line number Diff line number Diff line change
@@ -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
*/
Expand Down
237 changes: 146 additions & 91 deletions packages/components/src/components/form/bal-select/bal-select.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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.
*/
Expand Down Expand Up @@ -231,6 +257,55 @@ export class Select {
*/
@Event() balKeyPress!: EventEmitter<KeyboardEvent>

/**
* LIFECYCLE
* ------------------------------------------------------
*/

connectedCallback() {
const debounceUpdateOptions = debounce(() => this.updateOptions(), 0)

this.initialValue = this.value

debounceUpdateOptions()

this.mutationO = watchForOptions<HTMLBalSelectOptionElement>(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) {
Expand Down Expand Up @@ -301,88 +376,10 @@ export class Select {
}
}

connectedCallback() {
const debounceUpdateOptions = debounce(() => this.updateOptions(), 0)

this.initialValue = this.value

debounceUpdateOptions()

this.mutationO = watchForOptions<HTMLBalSelectOptionElement>(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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -733,9 +782,10 @@ export class Select {
}
}

/********************************************************
* EVENT HANDLERS
********************************************************/
/**
* EVENT BINDING
* ------------------------------------------------------
*/

private handleClick = (event: MouseEvent) => {
if (this.disabled || this.readonly) {
Expand Down Expand Up @@ -842,6 +892,11 @@ export class Select {
this.focusIndex = index
}

/**
* RENDER
* ------------------------------------------------------
*/

render() {
const labelId = this.inputId + '-lbl'
const label = findItemLabel(this.el)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ Typeahead.parameters = {
}

export const TypeaheadRemote = args => ({
components: { ...component.components },
components: { ...component.components, BalField, BalFieldLabel, BalFieldControl },
setup: () => {
const cantons = [
'Zürich',
Expand Down Expand Up @@ -197,6 +197,8 @@ TypeaheadRemote.args = {
typeahead: true,
remote: true,
loading: true,
selectionOptional: true,
value: 'Ticino',
placeholder: 'Try finding your canton',
}
TypeaheadRemote.parameters = {
Expand Down

0 comments on commit 5f11bb9

Please sign in to comment.