Replies: 1 comment 1 reply
-
Kinda duck taped this solution could be improved but wanted to provide an improvement to the dmc.Multiselect based on these examples: MultiSelectCheckbox- MultiSelectImages- My solution was based on this change to the src/ts/core/components/combobox/MultiSelect.tsx import {
Checkbox,
Combobox,
Group,
Input,
Pill,
PillsInput,
CloseButton,
useCombobox
} from "@mantine/core";
import { useDidUpdate } from "@mantine/hooks";
import { BoxProps } from "props/box";
import { __CloseButtonProps } from "props/button";
import { ComboboxLikeProps } from "props/combobox";
import { DashBaseProps, PersistenceProps } from "props/dash";
import { __BaseInputProps } from "props/input";
import { ScrollAreaProps } from "props/scrollarea";
import { StylesApiProps } from "props/styles";
import React, { useState } from "react";
import { filterSelected } from "../../../utils/combobox";
// Define types for the image/flag data
interface DataItem {
value: string;
label: string;
image?: string;
}
interface Props
extends BoxProps,
__BaseInputProps,
ComboboxLikeProps,
StylesApiProps,
DashBaseProps,
PersistenceProps {
/** Controlled component value */
value?: string[];
/** Controlled search value */
searchValue?: string;
/** Data array with optional image components */
data: DataItem[];
/** Maximum number of values, `Infinity` by default */
maxValues?: number;
/** Determines whether the select should be searchable, `false` by default */
searchable?: boolean;
/** Message displayed when no option matched current search query, only applicable when `searchable` prop is set */
nothingFoundMessage?: React.ReactNode;
/** Use checkbox style for options */
withCheckbox?: boolean;
/** Hide picked options from the dropdown */
hidePickedOptions?: boolean;
/** Determines whether the clear button should be displayed */
clearable?: boolean;
/** Props passed down to the clear button */
clearButtonProps?: __CloseButtonProps;
/** Props passed down to the hidden input */
hiddenInputProps?: object;
/** Props passed down to the underlying `ScrollArea` component in the dropdown */
scrollAreaProps?: ScrollAreaProps;
}
// Custom Pill component for items with images
const CustomPill: React.FC<{
item: DataItem;
onRemove: () => void;
}> = ({ item, onRemove }) => {
return (
<div className="flex items-center rounded-full border border-gray-300 bg-white px-2 py-1">
{item.image && (
<div className="mr-2" dangerouslySetInnerHTML={{ __html: item.image }} />
)}
<span className="text-sm">{item.label}</span>
<CloseButton
onMouseDown={onRemove}
variant="transparent"
color="gray"
size={22}
iconSize={14}
tabIndex={-1}
/>
</div>
);
};
/** Enhanced MultiSelect */
const MultiSelect = (props: Props) => {
const {
setProps,
data,
searchValue,
value,
withCheckbox,
hidePickedOptions,
...others
} = props;
const [selected, setSelected] = useState(value);
const [searchVal, setSearchVal] = useState(searchValue);
const combobox = useCombobox({
onDropdownClose: () => combobox.resetSelectedOption(),
onDropdownOpen: () => combobox.updateSelectedOptionIndex("active"),
});
const handleValueSelect = (val: string) => {
const newSelected = selected?.includes(val)
? selected.filter((v) => v !== val)
: [...(selected ?? []), val];
setSelected(newSelected);
setProps({ value: newSelected });
};
const handleValueRemove = (val: string) => {
const newSelected = selected?.filter((v) => v !== val) ?? [];
setSelected(newSelected);
setProps({ value: newSelected });
};
useDidUpdate(() => {
const filteredSelected = filterSelected(data, selected);
setSelected(filteredSelected ?? []);
}, [data]);
useDidUpdate(() => {
setSelected(value ?? []);
}, [value]);
useDidUpdate(() => {
setProps({ searchValue: searchVal });
}, [searchVal]);
const values = selected?.map((item) => {
const dataItem = data.find((d) => d.value === item);
if (!dataItem) return null;
return (
<CustomPill
key={item}
item={dataItem}
onRemove={() => handleValueRemove(item)}
/>
);
});
const filteredData = hidePickedOptions
? data.filter((item) => !selected?.includes(item.value))
: data;
const options = filteredData.map((item) => (
<Combobox.Option
value={item.value}
key={item.value}
active={selected?.includes(item.value)}
>
<Group gap="sm">
{withCheckbox && (
<Checkbox
checked={selected?.includes(item.value)}
onChange={() => {}}
aria-hidden
tabIndex={-1}
style={{ pointerEvents: "none" }}
/>
)}
{item.image && <div dangerouslySetInnerHTML={{ __html: item.image }} />}
<span>{item.label}</span>
</Group>
</Combobox.Option>
));
return (
<Combobox
store={combobox}
onOptionSubmit={handleValueSelect}
withinPortal={false}
>
<Combobox.DropdownTarget>
<PillsInput
pointer
onClick={() => combobox.toggleDropdown()}
{...others}
>
<Pill.Group>
{values?.length ? (
values
) : (
<Input.Placeholder>
Pick one or more values
</Input.Placeholder>
)}
<Combobox.EventsTarget>
<PillsInput.Field
type="hidden"
onBlur={() => combobox.closeDropdown()}
onKeyDown={(event) => {
if (
event.key === "Backspace" &&
selected?.length
) {
event.preventDefault();
handleValueRemove(
selected[selected.length - 1]
);
}
}}
/>
</Combobox.EventsTarget>
</Pill.Group>
</PillsInput>
</Combobox.DropdownTarget>
<Combobox.Dropdown>
<Combobox.Options>{options}</Combobox.Options>
</Combobox.Dropdown>
</Combobox>
);
};
MultiSelect.defaultProps = {
persisted_props: ["value"],
persistence_type: "local",
data: [],
value: [],
withCheckbox: false,
hidePickedOptions: false,
};
export default MultiSelect; example.py: from dash import Dash, _dash_renderer, html, callback, Input, Output
import dash_mantine_components as dmc
from dash_iconify import DashIconify
_dash_renderer._set_react_version("18.2.0")
app = Dash(external_stylesheets=dmc.styles.ALL)
def create_avatar_html(country_code):
# Using country flag API for demonstration
return f'<img src="https://flagcdn.com/w40/{country_code.lower()}.png" alt="{country_code}" style="width: 24px; height: 16px; border-radius: 4px;" />'
# Sample data for countries with flag icons
countries_data = [
{
"value": "us",
"label": "United States",
"image": create_avatar_html("us")
},
{
"value": "gb", # Changed from uk to gb for flag API
"label": "United Kingdom",
"image": create_avatar_html("gb")
},
{
"value": "fr",
"label": "France",
"image": create_avatar_html("fr")
},
{
"value": "de",
"label": "Germany",
"image": create_avatar_html("de")
},
{
"value": "jp",
"label": "Japan",
"image": create_avatar_html("jp")
}
]
# Sample data for grocery items with checkboxes
grocery_data = [
{"value": "apples", "label": "🍎 Apples"},
{"value": "bananas", "label": "🍌 Bananas"},
{"value": "oranges", "label": "🍊 Oranges"},
{"value": "broccoli", "label": "🥦 Broccoli"},
{"value": "carrots", "label": "🥕 Carrots"}
]
app.layout = dmc.MantineProvider(
html.Div([
# Title and description
dmc.Title("MultiSelect Component Demo", order=1, mb=16),
dmc.Text(
"Demonstration of MultiSelect with images and checkboxes",
# color="dimmed",
mb=32
),
# Countries MultiSelect with flags
dmc.Stack([
dmc.Text("Select Countries (with flags)", size="lg"),
dmc.MultiSelect(
id="countries-select",
data=countries_data,
placeholder="Select countries...",
searchable=True,
clearable=True,
style={"width": 400}
),
dmc.Text(id="countries-output", size="sm")
], mb=32),
# Grocery MultiSelect with checkboxes
dmc.Stack([
dmc.Text("Select Groceries (with checkboxes)", size="lg"),
dmc.MultiSelect(
id="grocery-select",
data=grocery_data,
placeholder="Select groceries...",
withCheckbox=True,
searchable=True,
clearable=True,
# nothingFound="No items found",
style={"width": 400}
),
dmc.Text(id="grocery-output", size="sm")
])
], style={"padding": 20})
)
# Callback for countries selection
@callback(
Output("countries-output", "children"),
Input("countries-select", "value")
)
def update_countries_output(value):
if not value:
return "No countries selected"
selected = [item["label"] for item in countries_data if item["value"] in value]
return f"Selected: {', '.join(selected)}"
# Callback for grocery selection
@callback(
Output("grocery-output", "children"),
Input("grocery-select", "value")
)
def update_grocery_output(value):
if not value:
return "No items selected"
selected = [item["label"] for item in grocery_data if item["value"] in value]
return f"Selected: {', '.join(selected)}"
if __name__ == "__main__":
app.run(debug=True) |
Beta Was this translation helpful? Give feedback.
1 reply
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
-
In V7 of the upstream Mantine library, they added a
Combobox
prop that allows for creating custom Select, MultiSelect, TagsInput components. They dropped popular props likecreateable
.They included 50+ examples, including a Createable Select component , however most of these examples would not work for Dash users because they require the ability to pass JavaScript functions as props, or direct access to the Mantine API from Dash, which is not yet supported in dmc.
In the meantime, we could add back certain features. Please feel free to add to this list and upvote your favorite:
creatable
(boolean)- When True, allows user to add new options to the Select and MultiSelect dropdowns.selectOnBlur
See Docs on selectOnBlur #118debounce
Debounce not working for dmc.Select and dmc.MultiSelect #194, [Feature Request] Input/Select components add debounce=True, n_submit and n_blur prop to trigger callback #87Here is more information from the release notes and worth repeating here:
Note the 50+ examples are excellent, and most are not included in the docs. It include an example of how to recreate the
selectOnBlur
functionality and other props that were removed in V7, likecreateable
Beta Was this translation helpful? Give feedback.
All reactions