Replies: 2 comments 1 reply
-
I did something similar for myself, but I couldn't find a way to get the filter/search working without altering I basically created a Code changes madeHere's only the changes I made from the original Python: # [Changed]
# * ChoiceElement -> SearchableChoiceElement
# * component: select.js -> searchable_select.js
- class Select(LabelElement, ValidationElement, ChoiceElement, DisableableElement, component='select.js'):
+ class SearchableSelect(LabelElement, ValidationElement, SearchableChoiceElement, DisableableElement, component="searchable_select.js"):
...
- class ChoiceElement(ValueElement):
+ class SearchableChoiceElement(ValueElement):
...
- self._values: List[str] = []
- self._labels: List[str] = []
+ self._values: List[str | dict] = []
+ self._labels: List[str | dict] = []
...
- self._props['options'] = [{'value': index, 'label': option} for index, option in enumerate(self._labels)]
+ options = []
+ for index, option in enumerate(self._labels):
+ data_label: dict = {"label": option, "search": option} if isinstance(option, str) else option
+ options.append({"value": index, "label": data_label})
+ self._props["options"] = options
...
- self.value = before_value if before_value in self._values else None
+ if before_value is None:
+ self.value = None
+ else:
+ _values = [val["value"] if isinstance(val, dict) else val for val in self._values]
+ self.value = before_value if before_value["value"] in _values else None Javascript: findFilteredOptions() {
- const needle = this.$el.querySelector("input[type=search]")?.value.toLocaleLowerCase();
- return needle
- ? this.initialOptions.filter((v) => String(v.label).toLocaleLowerCase().indexOf(needle) > -1)
+ // [Changed] needle -> queryTerms that is split(" ")
+ const queryTerms = this.$el.querySelector("input[type=search]")?.value.toLocaleLowerCase().split(" ");
+ // [Changed] added whole queryMatch instead of just str.toLocaleLowerCase().indexOf() > -1
+ const queryMatches = function(queryTerms, candidate) {
+ const _candidate = candidate.toLocaleLowerCase();
+ const allFound = queryTerms.every(term =>
+ _candidate.includes(term.toLocaleLowerCase())
+ );
+ return allFound;
+ };
+ // [Changed] v.label -> v.label.search
+ return queryTerms
+ ? this.initialOptions.filter((v) => queryMatches(queryTerms, String(v.label.search)))
: this.initialOptions;
}, Full codeIt's a bit messy, but here's the full code (2 files).
# ruff: noqa
from collections.abc import Generator, Iterable
from copy import deepcopy
from typing import Any, Callable, Dict, Iterator, List, Literal, Optional, Union
from nicegui.events import GenericEventArguments, Handler, ValueChangeEventArguments
from nicegui.elements.mixins.disableable_element import DisableableElement
from nicegui.elements.mixins.label_element import LabelElement
from nicegui.elements.mixins.validation_element import ValidationDict, ValidationElement, ValidationFunction
from typing import Any, Dict, List, Optional, Union
from nicegui.events import Handler, ValueChangeEventArguments
from nicegui.elements.mixins.value_element import ValueElement
# Copied from nicegui.elements.choice_element
class SearchableChoiceElement(ValueElement):
def __init__(
self,
*,
tag: Optional[str] = None,
options: Union[List, Dict],
value: Any,
on_change: Optional[Handler[ValueChangeEventArguments]] = None,
) -> None:
self.options = options
self._values: List[str | dict] = []
self._labels: List[str | dict] = []
self._update_values_and_labels()
if not isinstance(value, list) and value is not None and value not in self._values:
raise ValueError(f"Invalid value: {value}")
super().__init__(tag=tag, value=value, on_value_change=on_change)
self._update_options()
# def _normalize_options(self, options):
# normalized = []
# if isinstance(dict):
# for index, (key, value) in enumerate(options.items()):
# data_label: dict = {"label": value, "search": value} if isinstance(value, str) else value
# normalized.append({"value": index, "label": data_label})
# return {}
# return normalized
def _update_values_and_labels(self) -> None:
self._values = self.options if isinstance(self.options, list) else list(self.options.keys())
# [Changed] Comment: Should be called _options or _data
self._labels = self.options if isinstance(self.options, list) else list(self.options.values())
def _update_options(self) -> None:
before_value = self.value
options = []
# [Changed]
for index, option in enumerate(self._labels):
data_label: dict = {"label": option, "search": option} if isinstance(option, str) else option
options.append({"value": index, "label": data_label})
self._props["options"] = options
self._props[self.VALUE_PROP] = self._value_to_model_value(before_value)
if not isinstance(before_value, list): # NOTE: no need to update value in case of multi-select
# [Changed]
if before_value is None:
self.value = None
else:
_values = [val["value"] if isinstance(val, dict) else val for val in self._values]
self.value = before_value if before_value["value"] in _values else None
def update(self) -> None:
self._update_values_and_labels()
self._update_options()
super().update()
def set_options(self, options: Union[List, Dict], *, value: Any = ...) -> None:
"""Set the options of this choice element.
:param options: The new options.
:param value: The new value. If not given, the current value is kept.
"""
self.options = options
if value is not ...:
self.value = value
self.update()
# Copied from nicegui.elements.select
# [Changed]
# * ChoiceElement -> SearchableChoiceElement
# * component: select.js -> aselect.js
class SearchableSelect(
LabelElement, ValidationElement, SearchableChoiceElement, DisableableElement, component="searchable_select.js"
):
def __init__(
self,
options: Union[List, Dict],
*,
label: Optional[str] = None,
value: Any = None,
on_change: Optional[Handler[ValueChangeEventArguments]] = None,
with_input: bool = False,
new_value_mode: Optional[Literal["add", "add-unique", "toggle"]] = None,
multiple: bool = False,
clearable: bool = False,
validation: Optional[Union[ValidationFunction, ValidationDict]] = None,
key_generator: Optional[Union[Callable[[Any], Any], Iterator[Any]]] = None,
) -> None:
"""Dropdown Selection
This element is based on Quasar's `QSelect <https://quasar.dev/vue-components/select>`_ component.
The options can be specified as a list of values, or as a dictionary mapping values to labels.
After manipulating the options, call `update()` to update the options in the UI.
If `with_input` is True, an input field is shown to filter the options.
If `new_value_mode` is not None, it implies `with_input=True` and the user can enter new values in the input field.
See `Quasar's documentation <https://quasar.dev/vue-components/select#the-new-value-mode-prop>`_ for details.
Note that this mode is ineffective when setting the `value` property programmatically.
You can use the `validation` parameter to define a dictionary of validation rules,
e.g. ``{'Too long!': lambda value: len(value) < 3}``.
The key of the first rule that fails will be displayed as an error message.
Alternatively, you can pass a callable that returns an optional error message.
To disable the automatic validation on every value change, you can use the `without_auto_validation` method.
:param options: a list ['value1', ...] or dictionary `{'value1':'label1', ...}` specifying the options
:param label: the label to display above the selection
:param value: the initial value
:param on_change: callback to execute when selection changes
:param with_input: whether to show an input field to filter the options
:param new_value_mode: handle new values from user input (default: None, i.e. no new values)
:param multiple: whether to allow multiple selections
:param clearable: whether to add a button to clear the selection
:param validation: dictionary of validation rules or a callable that returns an optional error message (default: None for no validation)
:param key_generator: a callback or iterator to generate a dictionary key for new values
"""
self.multiple = multiple
if multiple:
if value is None:
value = []
elif not isinstance(value, list):
value = [value]
else:
value = value[:] # NOTE: avoid modifying the original list which could be the list of options (#3014)
super().__init__(label=label, options=options, value=value, on_change=on_change, validation=validation)
if isinstance(key_generator, Generator):
next(key_generator) # prime the key generator, prepare it to receive the first value
self.key_generator = key_generator
if new_value_mode is not None:
if isinstance(options, dict) and new_value_mode == "add" and key_generator is None:
raise ValueError('new_value_mode "add" is not supported for dict options without key_generator')
self._props["new-value-mode"] = new_value_mode
with_input = True
if with_input:
self.original_options = deepcopy(options)
self._props["use-input"] = True
self._props["hide-selected"] = not multiple
self._props["fill-input"] = True
self._props["input-debounce"] = 0
self._props["multiple"] = multiple
self._props["clearable"] = clearable
self._is_showing_popup = False
self.on("popup-show", lambda e: setattr(e.sender, "_is_showing_popup", True))
self.on("popup-hide", lambda e: setattr(e.sender, "_is_showing_popup", False))
@property
def is_showing_popup(self) -> bool:
"""Whether the options popup is currently shown."""
return self._is_showing_popup
def _event_args_to_value(self, e: GenericEventArguments) -> Any:
if self.multiple:
if e.args is None:
return []
else:
args = [self._values[arg["value"]] if isinstance(arg, dict) else arg for arg in e.args]
for arg in e.args:
if isinstance(arg, str):
self._handle_new_value(arg)
return [arg for arg in args if arg in self._values]
else: # noqa: PLR5501
if e.args is None:
return None
else: # noqa: PLR5501
if isinstance(e.args, str):
new_value = self._handle_new_value(e.args)
return new_value if new_value in self._values else None
else:
return self._values[e.args["value"]]
def _value_to_model_value(self, value: Any) -> Any:
# pylint: disable=no-else-return
if self.multiple:
result = []
for item in value or []:
try:
index = self._values.index(item)
result.append({"value": index, "label": self._labels[index]})
except ValueError:
pass
return result
else:
try:
index = self._values.index(value)
return {"value": index, "label": self._labels[index]}
except ValueError:
return None
def _generate_key(self, value: str) -> Any:
if isinstance(self.key_generator, Generator):
return self.key_generator.send(value)
if isinstance(self.key_generator, Iterable):
return next(self.key_generator)
if callable(self.key_generator):
return self.key_generator(value)
return value
def _handle_new_value(self, value: str) -> Any:
mode = self._props["new-value-mode"]
if isinstance(self.options, list):
if mode == "add":
self.options.append(value)
elif mode == "add-unique":
if value not in self.options:
self.options.append(value)
elif mode == "toggle":
if value in self.options:
self.options.remove(value)
else:
self.options.append(value)
# NOTE: self._labels and self._values are updated via self.options since they share the same references
return value
else:
key = value
if mode == "add":
key = self._generate_key(value)
self.options[key] = value
elif mode == "add-unique":
if value not in self.options.values():
key = self._generate_key(value)
self.options[key] = value
elif mode == "toggle":
if value in self.options:
self.options.pop(value)
else:
key = self._generate_key(value)
self.options.update({key: value})
self._update_values_and_labels()
return key
export default {
props: ["options"],
template: `
<q-select
ref="qRef"
v-bind="$attrs"
:options="filteredOptions"
@filter="filterFn"
>
<template v-for="(_, slot) in $slots" v-slot:[slot]="slotProps">
<slot :name="slot" v-bind="slotProps || {}" />
</template>
</q-select>
`,
data() {
return {
initialOptions: this.options,
filteredOptions: this.options,
};
},
methods: {
filterFn(val, update, abort) {
update(() => (this.filteredOptions = val ? this.findFilteredOptions() : this.initialOptions));
},
findFilteredOptions() {
// [Changed] needle -> queryTerms that is split(" ")
const queryTerms = this.$el.querySelector("input[type=search]")?.value.toLocaleLowerCase().split(" ");
// [Changed] added whole queryMatch instead of just str.toLocaleLowerCase().indexOf() > -1
const queryMatches = function(queryTerms, candidate) {
const _candidate = candidate.toLocaleLowerCase();
const allFound = queryTerms.every(term =>
_candidate.includes(term.toLocaleLowerCase())
);
return allFound;
};
// [Changed] v.label -> v.label.search
return queryTerms
? this.initialOptions.filter((v) => queryMatches(queryTerms, String(v.label.search)))
: this.initialOptions;
},
},
updated() {
if (!this.$attrs.multiple) return;
const newFilteredOptions = this.findFilteredOptions();
if (newFilteredOptions.length !== this.filteredOptions.length) {
this.filteredOptions = newFilteredOptions;
}
},
watch: {
options: {
handler(newOptions) {
this.initialOptions = newOptions;
this.filteredOptions = newOptions;
},
immediate: true,
},
},
}; |
Beta Was this translation helpful? Give feedback.
-
This is tricky. The filter function of Unfortunately I don't have an easy solution for it. You could create a feature request to make the filter function more flexible. But with the many features
I managed to do it with the "selected-item" slot. But you need to remove the "hide-selected" prop, which is automatically set by select = ui.select({
'google': {'label': 'Google', 'value': 'google', 'icon': 'mail'},
'facebook': {'label': 'Facebook', 'value': 'facebook', 'icon': 'bluetooth'},
'instagram': {'label': 'Instagram', 'value': 'instagram', 'icon': 'photo_camera'},
}, value='google', with_input=True)
select.add_slot('option', '''
<q-item :props="props" class="flex items-center gap-2" clickable @click="props.toggleOption(props.opt)">
<q-item-section avatar>
<q-icon :name="props.opt.label.icon"></q-icon>
</q-item-section>
<q-item-section>
<span>{{props.opt.label.label}}</span>
</q-item-section>
</q-item>
''')
select.add_slot('selected-item', '''
<q-icon :props="props" :name="props.opt.label.icon" size="sm" class="mr-2"></q-icon>
''')
select.props(':option-label="(opt) => opt.label.label"', remove='hide-selected') |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
First Check
Example Code
Description
I am trying to implement a subclass of
ui.select
that has icons for each option. I based my code from the answer to #3977.Creating the select using
with_input=True
and attempting to filter the options does not seem to work. If anything is typed in the input, no options become available to select. How to make the options filtering work?Also how could I get the icon to show in the main field after making the selection? I was a bit confused trying to determine the right slot from Quasar's Select documentation.
Thank you for your help!
NiceGUI Version
2.24.1
Python Version
3.11
Browser
Chrome
Operating System
Windows
Additional Context
No response
Beta Was this translation helpful? Give feedback.
All reactions