it's been hours that I try to get a customized dropdown in Jupyter Notebook using widgets.
My goal is to have a dropdown that lists color names and has a little square/round/whatever showing the color.
I understand that HTML alone doesn't allow for a select option to be something else than text. I tried a bit ipywidgets
to warm up and test the form logic, then I moved to ipyvuetify
(1.10.0
then 3.0.0.a2
) to hopefully be able to customize my dropdown. I tried all that came through my mind using the v.Select
options with v_slots
and children
, and finally and created a v.VuetifyTemplate
to be able to be closer to Vuetify's documentation.
Here is what I have now:
import ipyvuetify as v
import traitlets
from typing import Annotated
import enum
class ColorSelector(v.VuetifyTemplate):
items = traitlets.List(traitlets.Dict(traitlets.Unicode()), default_value=[{"name": "test", "value": "test"}]).tag(sync=True)
selected = traitlets.Unicode(default_value=None, allow_none=True).tag(sync=True)
label = traitlets.Unicode(default_value="Color", allow_none=True).tag(sync=True)
@traitlets.default("template")
def _template(self):
return '''
<template>
<v-select
:items="items"
item-title="name"
item-value="value"
v-model="selected"
label="'''+self.label+'''"
class="mx-3">
<template v-slot:item="{ props, item }">
<v-list-item v-bind="props" :key="item.value" @click="item_click(item.value)">
<div>
<v-icon left small :color="item.value"></v-icon>
<span>{{ item.value }}</span>
</div>
</v-list-item>
</template>
</v-select>
</template>
'''
def vue_item_click(self, value):
print(repr(value))
self.selected = value
# Just a piece of the data for demo (Note: colors are HTML colors, so the #-value is optional for the demo)
class TestColor(str, enum.Enum):
PALEVIOLETRED: Annotated[str, "#DB7093"] = "PaleVioletRed"
YELLOWGREEN: Annotated[str, "#9ACD32"] = "YellowGreen"
LIGHTCORAL: Annotated[str, "#F08080"] = "LightCoral"
THISTLE: Annotated[str, "#D8BFD8"] = "Thistle"
wid_favorite_color = ColorSelector(
items = [
{
"name": c.name,
"value": c.value
}
for c in TestColor
],
label = "Favorite Color"
)
display(wid_favorite_color)
Sadly, the customization doesn't show up, but also the selection doesn't work properly.
Result showing incorrect selection
The documentation at Vuetify's Select item slot doesn't say anything about having to restore the @click
behavior. Following the Example at ipyvuetify for menus, I added the @click
callback and a :key
attribute, but if I use item.value
, the result shown in the dropdown input is [object Object]
, if I use item.name
nothing is displayed. Also, the menu doesn't close when a selection is made.
There is definitely something that I don't get. Help! I just want a dropdown with colors! ðŸ˜
I finally made it. First, reloading the notebook (actually restarting VSCode) solved almost half of the issues I had. Probably the change of version for ipyvuetify
did not happen properly on the rendering side (notebook client).
With that, debugging was much more consistent and I could relate back to the documentation and some examples. Well, enough talk, here is the solution:
import ipyvuetify as v
import traitlets
from typing import Annotated
import enum
class ColorSelector(v.VuetifyTemplate):
items = traitlets.List(traitlets.Dict(traitlets.Unicode()), default_value=[{"name": "test", "value": "test", "hex": "#FF0000"}]).tag(sync=True)
selected = traitlets.Unicode(default_value=None, allow_none=True).tag(sync=True)
label = traitlets.Unicode(default_value="Color", allow_none=True).tag(sync=True)
@traitlets.default("template")
def _template(self):
return f'''
<template>
<v-select
:items="items"
item-title="value"
item-value="name"
v-model="selected"
label="{self.label}"
class="mx-3">
<template v-slot:selection="{{ item, index }}">
<v-icon icon="mdi-square-rounded" :color="item.raw.hex" size="x-large" style="margin-right: 0.5em"></v-icon>
<span>{{{{ item.title }}}}<span>
</template>
<template v-slot:item="{{ props, item }}">
<v-list-item v-bind="props">
<template v-slot:prepend>
<v-icon icon="mdi-square-rounded" :color="item.raw.hex" size="x-large"></v-icon>
</template>
</v-list-item>
</template>
</v-select>
</template>
'''
# Just a piece of the data for demo
class TestColor(str, enum.Enum):
INDIANRED: Annotated[str, "#CD5C5C"] = "IndianRed"
LIGHTCORAL: Annotated[str, "#F08080"] = "LightCoral"
SALMON: Annotated[str, "#FA8072"] = "Salmon"
DARKSALMON: Annotated[str, "#E9967A"] = "DarkSalmon"
LIGHTSALMON: Annotated[str, "#FFA07A"] = "LightSalmon"
CRIMSON: Annotated[str, "#DC143C"] = "Crimson"
RED: Annotated[str, "#FF0000"] = "Red"
wid_favorite_color = ColorSelector(
items = [
{
"name": c.name,
"value": c.value,
"hex": TestColor.__annotations__[c.name].__metadata__[0]
}
for c in TestColor
],
label = "Favorite Color"
)
display(wid_favorite_color)
@click
, it was indeed not necessaryitem.raw.hex
when needed. I finally learned that the item wasn't the JS counter part of the Python object. item.raw
is, but the name is mapped to item.title
and value to item.value
, but this depends on the properies you set for item-title
and item-value
of the v-select
component.v-list-item
using a simple v-icon
inside the prepend
slot.selection
slot to display also the color in the inputNote:
Of course, if you copy this code, you don't need all the enum
and Annotated
stuff, with the complicated TestColor.__annotations__[c.name].__metadata__[0]
. I need this in my project because the form is linked to data that is validated using Pydantic.