Skip to content

Commit

Permalink
ui.tree select/deselect, tick/untick programmatically (#3476)
Browse files Browse the repository at this point in the history
* Methods: select & deselect implemented with test.

* Methods: tick (all) & untick (all) implemented with test.

* Demo added to tree details documentation page.

* Update tree.py to correct param description

* code review

* tiny cleanup

---------

Co-authored-by: Falko Schindler <falko@zauberzeug.com>
  • Loading branch information
arunaseva and falkoschindler authored Aug 12, 2024
1 parent 4b6d16b commit 1a044b2
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 9 deletions.
34 changes: 33 additions & 1 deletion nicegui/elements/tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def __init__(self,
self._props['node-key'] = node_key
self._props['label-key'] = label_key
self._props['children-key'] = children_key
self._props['selected'] = []
self._props['selected'] = None
self._props['expanded'] = []
self._props['ticked'] = []
if tick_strategy is not None:
Expand Down Expand Up @@ -78,6 +78,20 @@ def on_select(self, callback: Callable[..., Any]) -> Self:
self._select_handlers.append(callback)
return self

def select(self, node_key: Optional[str]) -> Self:
"""Select the given node.
:param node_key: node key to select
"""
if self._props['selected'] != node_key:
self._props['selected'] = node_key
self.update()
return self

def deselect(self) -> Self:
"""Remove node selection."""
return self.select(None)

def on_expand(self, callback: Callable[..., Any]) -> Self:
"""Add a callback to be invoked when the expansion changes."""
self._expand_handlers.append(callback)
Expand All @@ -88,6 +102,24 @@ def on_tick(self, callback: Callable[..., Any]) -> Self:
self._tick_handlers.append(callback)
return self

def tick(self, node_keys: Optional[List[str]] = None) -> Self:
"""Tick the given nodes.
:param node_keys: list of node keys to tick or ``None`` to tick all nodes (default: ``None``)
"""
self._props['ticked'][:] = self._find_node_keys(node_keys).union(self._props['ticked'])
self.update()
return self

def untick(self, node_keys: Optional[List[str]] = None) -> Self:
"""Remove tick from the given nodes.
:param node_keys: list of node keys to untick or ``None`` to untick all nodes (default: ``None``)
"""
self._props['ticked'][:] = set(self._props['ticked']).difference(self._find_node_keys(node_keys))
self.update()
return self

def expand(self, node_keys: Optional[List[str]] = None) -> Self:
"""Expand the given nodes.
Expand Down
46 changes: 46 additions & 0 deletions tests/test_tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,49 @@ def test_expand_and_collapse_nodes(screen: Screen):
screen.should_not_contain('2')
screen.should_contain('A')
screen.should_contain('B')


def test_select_deselect_node(screen: Screen):
tree = ui.tree([
{'id': 'numbers', 'children': [{'id': '1'}, {'id': '2'}]},
{'id': 'letters', 'children': [{'id': 'A'}, {'id': 'B'}]},
], label_key='id')

ui.button('Select', on_click=lambda: tree.select('2'))
ui.button('Deselect', on_click=tree.deselect)
ui.label().bind_text_from(tree._props, 'selected', lambda x: f'Selected: {x}')

screen.open('/')
screen.click('Select')
screen.should_contain('Selected: 2')

screen.click('Deselect')
screen.should_contain('Selected: None')


def test_tick_untick_node_or_nodes(screen: Screen):
tree = ui.tree([
{'id': 'numbers', 'children': [{'id': '1'}, {'id': '2'}]},
{'id': 'letters', 'children': [{'id': 'A'}, {'id': 'B'}]},
], label_key='id', tick_strategy='leaf')

ui.button('Tick some', on_click=lambda: tree.tick(['1', '2', 'B']))
ui.button('Untick some', on_click=lambda: tree.untick(['1', 'B']))
ui.button('Tick all', on_click=tree.tick)
ui.button('Untick all', on_click=tree.untick)
ui.label().bind_text_from(tree._props, 'ticked', lambda x: f'Ticked: {sorted(x)}')

screen.open('/')
screen.should_contain('Ticked: []')

screen.click('Tick some')
screen.should_contain("Ticked: ['1', '2', 'B']")

screen.click('Untick some')
screen.should_contain("Ticked: ['2']")

screen.click('Tick all')
screen.should_contain("Ticked: ['1', '2', 'A', 'B', 'letters', 'numbers']")

screen.click('Untick all')
screen.should_contain('Ticked: []')
48 changes: 40 additions & 8 deletions website/documentation/content/tree_documentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,17 @@ def tree_with_custom_header_and_body():
''')


@doc.demo('Expand and collapse programmatically', '''
@doc.demo('Tree with checkboxes', '''
The tree can be used with checkboxes by setting the "tick-strategy" prop.
''')
def tree_with_checkboxes():
ui.tree([
{'id': 'A', 'children': [{'id': 'A1'}, {'id': 'A2'}]},
{'id': 'B', 'children': [{'id': 'B1'}, {'id': 'B2'}]},
], label_key='id', tick_strategy='leaf', on_tick=lambda e: ui.notify(e.value))


@doc.demo('Expand/collapse programmatically', '''
The whole tree or individual nodes can be toggled programmatically using the `expand()` and `collapse()` methods.
This even works if a node is disabled (e.g. not clickable by the user).
''')
Expand All @@ -52,14 +62,36 @@ def expand_programmatically():
ui.button('- A', on_click=lambda: t.collapse(['A']))


@doc.demo('Tree with checkboxes', '''
The tree can be used with checkboxes by setting the "tick-strategy" prop.
@doc.demo('Select/deselect programmatically', '''
You can select or deselect nodes with the `select()` and `deselect()` methods.
''')
def tree_with_checkboxes():
ui.tree([
{'id': 'A', 'children': [{'id': 'A1'}, {'id': 'A2'}]},
{'id': 'B', 'children': [{'id': 'B1'}, {'id': 'B2'}]},
], label_key='id', tick_strategy='leaf', on_tick=lambda e: ui.notify(e.value))
def select_programmatically():
t = ui.tree([
{'id': 'numbers', 'children': [{'id': '1'}, {'id': '2'}]},
{'id': 'letters', 'children': [{'id': 'A'}, {'id': 'B'}]},
], label_key='id').expand()

with ui.row():
ui.button('Select A', on_click=lambda: t.select('A'))
ui.button('Deselect A', on_click=t.deselect)


@doc.demo('Tick/untick programmatically', '''
After setting a `tick_strategy`, you can tick or untick nodes with the `tick()` and `untick()` methods.
You can either specify a list of node keys or `None` to tick or untick all nodes.
''')
def tick_programmatically():
t = ui.tree([
{'id': 'numbers', 'children': [{'id': '1'}, {'id': '2'}]},
{'id': 'letters', 'children': [{'id': 'A'}, {'id': 'B'}]},
], label_key='id', tick_strategy='leaf').expand()

with ui.row():
ui.button('Tick 1, 2 and B', on_click=lambda: t.tick(['1', '2', 'B']))
ui.button('Untick 2 and B', on_click=lambda: t.untick(['2', 'B']))
with ui.row():
ui.button('Tick all', on_click=t.tick)
ui.button('Untick all', on_click=t.untick)


doc.reference(ui.tree)

0 comments on commit 1a044b2

Please sign in to comment.