Skip to content
8 changes: 8 additions & 0 deletions examples/chips_on_blur/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Input Chips with Blur Support

Demonstrates how `ui.input_chips` automatically adds typed values as chips when the input field loses focus (blur event), in addition to the Enter key behavior.

The example shows all three `new_value_mode` options:
- **toggle**: Adds value if absent, removes if present
- **add**: Always adds values (allows duplicates)
- **add-unique**: Only adds if not already present
46 changes: 46 additions & 0 deletions examples/chips_on_blur/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#!/usr/bin/env python3
"""Demonstrates input_chips blur functionality.

This example shows how input_chips automatically adds typed values as chips
when the field loses focus (blur event), in addition to the default Enter key behavior.
"""
from nicegui import ui

ui.markdown('## Input Chips with Blur Support')
ui.markdown('''
Type a value and press **Tab** or **click elsewhere** to add it as a chip.
The **Enter** key also works (original behavior).
''')

ui.separator()

# Toggle mode (default)
with ui.card().classes('w-full'):
ui.label('Toggle Mode (default)').classes('text-h6')
ui.markdown('Adding the same value twice **removes** it.')
chips_toggle = ui.input_chips(label='Try typing "test" twice', new_value_mode='toggle')
ui.label().bind_text_from(chips_toggle, 'value', lambda v: f'Chips: {v}')

ui.separator()

# Add mode
with ui.card().classes('w-full'):
ui.label('Add Mode').classes('text-h6')
ui.markdown('**Allows duplicate** values.')
chips_add = ui.input_chips(label='Try typing "test" twice', new_value_mode='add')
ui.label().bind_text_from(chips_add, 'value', lambda v: f'Chips: {v}')

ui.separator()

# Add-unique mode
with ui.card().classes('w-full'):
ui.label('Add-Unique Mode').classes('text-h6')
ui.markdown('**Prevents duplicate** values.')
chips_unique = ui.input_chips(label='Try typing "test" twice', new_value_mode='add-unique')
ui.label().bind_text_from(chips_unique, 'value', lambda v: f'Chips: {v}')

ui.separator()

ui.button('Click to trigger blur', icon='touch_app')

ui.run(port=8087)
55 changes: 54 additions & 1 deletion nicegui/elements/input_chips.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,14 @@ def __init__(self,
"""Input Chips

An input field that manages a collection of values as visual "chips" or tags.
Users can type to add new chips and remove existing ones by clicking or using keyboard shortcuts.
Users can add new chips by pressing Enter or when the input field loses focus (Tab, click away).
Chips can be removed by clicking the "x" icon on each chip.

The ``new_value_mode`` parameter controls how duplicate values are handled:

- ``'add'``: Always adds the value (allows duplicates)
- ``'add-unique'``: Only adds if not already present
- ``'toggle'``: Adds if absent, removes if present

This element is based on Quasar's `QSelect <https://quasar.dev/vue-components/select>`_ component.
Unlike a traditional dropdown selection, this variant focuses on free-form text input with chips,
Expand Down Expand Up @@ -55,5 +62,51 @@ def __init__(self,
self._props['hide-dropdown-icon'] = True
self._props['clearable'] = clearable

# Track the current input value to add on blur
self._current_input_value = ''
self._new_value_mode = new_value_mode

# Listen to input-value changes to track what user is typing
self.on('input-value', self._handle_input_value_change)

# Listen to blur event to add the current input value as a chip
self.on('blur', self._handle_blur)

def _handle_input_value_change(self, e: GenericEventArguments) -> None:
"""Track the current input value as user types."""
self._current_input_value = e.args if e.args else ''

def _handle_blur(self) -> None:
"""Add the current input value as a chip when field loses focus."""
val = self._current_input_value.strip() if isinstance(self._current_input_value, str) else ''

if not val:
return

# Get current chips
current_value = self.value if self.value else []

# Apply new-value-mode logic
if self._new_value_mode == 'add':
# Always add the value
new_value = [*current_value, val]
elif self._new_value_mode == 'add-unique':
# Only add if not already present
if val not in current_value:
new_value = [*current_value, val]
else:
return
elif self._new_value_mode == 'toggle':
# Toggle: add if not present, remove if present
if val in current_value:
new_value = [v for v in current_value if v != val]
else:
new_value = [*current_value, val]
else:
return

# Update the value
self.value = new_value

def _event_args_to_value(self, e: GenericEventArguments) -> Any:
return e.args or []
35 changes: 35 additions & 0 deletions tests/test_input_chips.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,38 @@ def page():
screen.should_contain('Too many')
else:
screen.should_not_contain('Too many')


@pytest.mark.parametrize('new_value_mode', ['add', 'add-unique', 'toggle'])
def test_input_chips_blur_adds_value(screen: Screen, new_value_mode: str):
@ui.page('/')
def page():
chips = ui.input_chips(new_value_mode=new_value_mode)
ui.label().bind_text_from(chips, 'value', lambda v: f'value = {v}')

screen.open('/')
screen.should_contain('value = []')

# Type 'x' and trigger blur
screen.find_by_tag('input').send_keys('x')
screen.wait(0.5)
screen.click('value = []')
screen.wait(0.5)

# Type 'y' twice with blur each time to test new_value_mode behavior:
# - 'add' keeps both: ['x', 'y', 'y']
# - 'add-unique' deduplicates: ['x', 'y']
# - 'toggle' removes second 'y': ['x']
for _ in range(2):
screen.find_by_tag('input').send_keys('y')
screen.wait(0.5)
screen.click('value = ') # Trigger blur
screen.wait(0.5)

# Verify behavior matches new_value_mode setting
if new_value_mode == 'add':
screen.should_contain("value = ['x', 'y', 'y']")
elif new_value_mode == 'add-unique':
screen.should_contain("value = ['x', 'y']")
elif new_value_mode == 'toggle':
screen.should_contain("value = ['x']")