add autocomplete component and make it optional for optionsField and relationshipField

This commit is contained in:
Maurits Lourens 2021-08-16 23:31:51 +02:00
parent fe5d3fe1d4
commit 62f7676cdf
7 changed files with 142 additions and 5 deletions

View File

@ -0,0 +1,80 @@
<script>
import { createEventDispatcher } from "svelte"
import Picker from "./Picker.svelte"
export let value = null
export let id = null
export let selectPlaceholder = "Choose an option"
export let autocompletePlaceholder = "Search an option"
export let disabled = false
export let error = null
export let options = []
export let getOptionLabel = option => option
export let getOptionValue = option => option
export let getOptionIcon = () => null
export let readonly = false
export let quiet = false
export let autoWidth = false
const dispatch = createEventDispatcher()
let open = false
$: fieldText = getFieldText(value, options, selectPlaceholder)
$: fieldIcon = getFieldIcon(value, options, selectPlaceholder)
const getFieldText = (value, options, placeholder) => {
// Always use placeholder if no value
if (value == null || value === "") {
return placeholder || "Choose an option"
}
// Wait for options to load if there is a value but no options
if (!options?.length) {
return ""
}
// Render the label if the selected option is found, otherwise raw value
const index = options.findIndex(
(option, idx) => getOptionValue(option, idx) === value
)
return index !== -1 ? getOptionLabel(options[index], index) : value
}
const getFieldIcon = (value, options) => {
// Wait for options to load if there is a value but no options
if (!options?.length) {
return ""
}
const index = options.findIndex(
(option, idx) => getOptionValue(option, idx) === value
)
return index !== -1 ? getOptionIcon(options[index], index) : null
}
const selectOption = value => {
dispatch("change", value)
open = false
}
</script>
<Picker
on:click
bind:open
{quiet}
{id}
{error}
{disabled}
{readonly}
{fieldText}
{options}
{autoWidth}
{getOptionLabel}
{getOptionValue}
{getOptionIcon}
{fieldIcon}
autocomplete={true}
isPlaceholder={value == null || value === ""}
{autocompletePlaceholder}
placeholderOption={selectPlaceholder}
isOptionSelected={option => option === value}
onSelectOption={selectOption}
/>

View File

@ -5,6 +5,7 @@
import { fly } from "svelte/transition" import { fly } from "svelte/transition"
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import clickOutside from "../../Actions/click_outside" import clickOutside from "../../Actions/click_outside"
import Search from "./Search.svelte"
export let id = null export let id = null
export let disabled = false export let disabled = false
@ -13,6 +14,7 @@
export let fieldIcon = "" export let fieldIcon = ""
export let isPlaceholder = false export let isPlaceholder = false
export let placeholderOption = null export let placeholderOption = null
export let autocompletePlaceholder = ""
export let options = [] export let options = []
export let isOptionSelected = () => false export let isOptionSelected = () => false
export let onSelectOption = () => {} export let onSelectOption = () => {}
@ -23,10 +25,17 @@
export let readonly = false export let readonly = false
export let quiet = false export let quiet = false
export let autoWidth = false export let autoWidth = false
export let autocomplete = false
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
let searchTerm = null
$: sortedOptions = getSortedOptions(options, getOptionLabel) $: sortedOptions = getSortedOptions(options, getOptionLabel)
$: filteredOptions = getFilteredOptions(
sortedOptions,
searchTerm,
getOptionLabel
)
const onClick = () => { const onClick = () => {
dispatch("click") dispatch("click")
@ -46,6 +55,16 @@
return labelA > labelB ? 1 : -1 return labelA > labelB ? 1 : -1
}) })
} }
const getFilteredOptions = (options, term, getLabel) => {
if (autocomplete && term) {
const lowerCaseTerm = term.toLowerCase()
return options.filter(option =>
getLabel(option).toLowerCase().includes(lowerCaseTerm)
)
}
return options
}
</script> </script>
<button <button
@ -96,6 +115,14 @@
class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover is-open" class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover is-open"
class:auto-width={autoWidth} class:auto-width={autoWidth}
> >
{#if autocomplete}
<Search
on:change={event => (searchTerm = event.detail)}
updateOnChange="true"
{disabled}
placeholder={autocompletePlaceholder}
/>
{/if}
<ul class="spectrum-Menu" role="listbox"> <ul class="spectrum-Menu" role="listbox">
{#if placeholderOption} {#if placeholderOption}
<li <li
@ -116,8 +143,8 @@
</svg> </svg>
</li> </li>
{/if} {/if}
{#if sortedOptions.length} {#if filteredOptions.length}
{#each sortedOptions as option, idx} {#each filteredOptions as option, idx}
<li <li
class="spectrum-Menu-item" class="spectrum-Menu-item"
class:is-selected={isOptionSelected(getOptionValue(option, idx))} class:is-selected={isOptionSelected(getOptionValue(option, idx))}

View File

@ -95,4 +95,7 @@
.is-disabled { .is-disabled {
pointer-events: none; pointer-events: none;
} }
.spectrum-Search-clearButton {
position: absolute;
}
</style> </style>

View File

@ -9,3 +9,4 @@ export { default as CoreSwitch } from "./Switch.svelte"
export { default as CoreSearch } from "./Search.svelte" export { default as CoreSearch } from "./Search.svelte"
export { default as CoreDatePicker } from "./DatePicker.svelte" export { default as CoreDatePicker } from "./DatePicker.svelte"
export { default as CoreDropzone } from "./Dropzone.svelte" export { default as CoreDropzone } from "./Dropzone.svelte"
export { default as CoreAutocomplete } from "./Autocomplete.svelte"

View File

@ -1914,6 +1914,10 @@
"label": "Select", "label": "Select",
"value": "select" "value": "select"
}, },
{
"label": "Autocomplete",
"value": "autocomplete"
},
{ {
"label": "Radio buttons", "label": "Radio buttons",
"value": "radio" "value": "radio"
@ -2135,6 +2139,12 @@
"label": "Placeholder", "label": "Placeholder",
"key": "placeholder" "key": "placeholder"
}, },
{
"type": "boolean",
"label": "Autocomplete",
"key": "autocomplete",
"defaultValue": false
},
{ {
"type": "boolean", "type": "boolean",
"label": "Disabled", "label": "Disabled",

View File

@ -1,5 +1,5 @@
<script> <script>
import { CoreSelect, RadioGroup } from "@budibase/bbui" import { CoreAutocomplete, CoreSelect, RadioGroup } from "@budibase/bbui"
import Field from "./Field.svelte" import Field from "./Field.svelte"
export let field export let field
@ -37,6 +37,16 @@
{placeholder} {placeholder}
on:change={e => fieldApi.setValue(e.detail)} on:change={e => fieldApi.setValue(e.detail)}
/> />
{:else if optionsType === "autocomplete"}
<CoreAutocomplete
value={$fieldState.value}
id={$fieldState.fieldId}
disabled={$fieldState.disabled}
error={$fieldState.error}
options={fieldSchema?.constraints?.inclusion ?? []}
{placeholder}
on:change={e => fieldApi.setValue(e.detail)}
/>
{:else if optionsType === "radio"} {:else if optionsType === "radio"}
<RadioGroup <RadioGroup
value={$fieldState.value} value={$fieldState.value}

View File

@ -1,5 +1,5 @@
<script> <script>
import { CoreSelect, CoreMultiselect } from "@budibase/bbui" import { CoreAutocomplete, CoreSelect, CoreMultiselect } from "@budibase/bbui"
import { getContext } from "svelte" import { getContext } from "svelte"
import Field from "./Field.svelte" import Field from "./Field.svelte"
@ -10,6 +10,7 @@
export let placeholder export let placeholder
export let disabled = false export let disabled = false
export let validation export let validation
export let autocomplete = false
let fieldState let fieldState
let fieldApi let fieldApi
@ -24,6 +25,11 @@
$: fetchTable(linkedTableId) $: fetchTable(linkedTableId)
$: singleValue = flatten($fieldState?.value)?.[0] $: singleValue = flatten($fieldState?.value)?.[0]
$: multiValue = flatten($fieldState?.value) ?? [] $: multiValue = flatten($fieldState?.value) ?? []
$: component = multiselect
? CoreMultiselect
: autocomplete
? CoreAutocomplete
: CoreSelect
const fetchTable = async id => { const fetchTable = async id => {
if (id) { if (id) {
@ -74,7 +80,7 @@
> >
{#if fieldState} {#if fieldState}
<svelte:component <svelte:component
this={multiselect ? CoreMultiselect : CoreSelect} this={component}
{options} {options}
value={multiselect ? multiValue : singleValue} value={multiselect ? multiValue : singleValue}
on:change={multiselect ? multiHandler : singleHandler} on:change={multiselect ? multiHandler : singleHandler}