Browse Source

PR #500, code review, refactoring, re-implement row selection

Falko Schindler 2 years ago
parent
commit
fac08be794

+ 16 - 28
examples/table_and_slots/main.py

@@ -3,11 +3,10 @@ import time
 
 from nicegui import ui
 
-fields = [
+columns = [
     {'name': 'name', 'label': 'Name', 'field': 'name', 'required': True},
     {'name': 'age', 'label': 'Age', 'field': 'age', 'sortable': True},
 ]
-
 rows = [
     {'id': 0, 'name': 'Alice', 'age': 18},
     {'id': 1, 'name': 'Bob', 'age': 21},
@@ -18,36 +17,25 @@ rows = [
     {'id': 6, 'name': 'Carol'},
 ]
 
-def add_row(item):
-    rows.append(item)
-    table.update()
-
-def remove_row(keys):
-    for i in range(len(rows)):
-        if rows[i]['id'] in keys:
-            del rows[i]
-            break
-    table.update()
-
-with ui.qtable(title='QTable', columns=fields, rows=rows, key='id', selection='single', pagination=15) as table:
+with ui.qtable(title='QTable', columns=columns, rows=rows, selection='multiple', rows_per_page=10).classes('w-96') as table:
     with table.add_slot('top-right'):
-        with ui.input(placeholder='Search').props('type="search"').bind_value(table, 'filter') as search:
-            with search.add_slot('append'):
-                ui.icon('search')
-
-    with table.add_slot('top-row'):
-        with table.row():
-            with table.cell().props('colspan="100%"'):
-                ui.label('This is a top row').classes('text-center')
-
+        with ui.input(placeholder='Search').props('type=search').bind_value(table, 'filter').add_slot('append'):
+            ui.icon('search')
     with table.add_slot('bottom-row'):
         with table.row():
-            with table.cell().props('colspan="2"'):
-                new_name = ui.input()
             with table.cell():
-                ui.button('add row', on_click=lambda: add_row({'id': time.time(), 'name': new_name.value, 'age': 10}))
+                ui.button(on_click=lambda: (
+                    table.add_rows({'id': time.time(), 'name': new_name.value, 'age': new_age.value}),
+                    new_name.set_value(None),
+                    new_age.set_value(None),
+                )).props('flat fab-mini icon=add')
+            with table.cell():
+                new_name = ui.input('Name')
+            with table.cell():
+                new_age = ui.number('Age')
 
-ui.label('').bind_text_from(table, 'selected', lambda val: f'Current selection: {val.__repr__()}')
-ui.button('Remove selection', on_click=lambda: remove_row(table.selected['keys'])).bind_visibility(table, 'selected')
+ui.label().bind_text_from(table, 'selected', lambda val: f'Current selection: {val}')
+ui.button('Remove', on_click=lambda: table.remove_rows(*table.selected)) \
+    .bind_visibility_from(table, 'selected', backward=lambda val: bool(val))
 
 ui.run()

+ 1 - 1
main.py

@@ -244,7 +244,7 @@ The command searches for `main.py` in in your current directory and makes the ap
             example_link('Search as you type', 'using public API of thecocktaildb.com to search for cocktails')
             example_link('Menu and Tabs', 'uses Quasar to create foldable menu and tabs inside a header bar')
             example_link('Trello Cards', 'shows Trello-like cards that can be dragged and dropped into columns')
-            example_link('Table and slots', 'shows how to use a component slot with the example of the QTable component')
+            example_link('Table and slots', 'shows how to use a component slot in a table')
 
     with ui.row().classes('bg-primary w-full min-h-screen mt-16'):
         link_target('why')

+ 8 - 8
nicegui/elements/mixins/filter_element.py

@@ -6,12 +6,12 @@ from ...element import Element
 
 class FilterElement(Element):
     FILTER_PROP = 'filter'
-    filter = BindableProperty(on_change=lambda sender, filter_by: sender.on_filter_change(filter_by))
+    filter = BindableProperty(on_change=lambda sender, filter: sender.on_filter_change(filter))
 
-    def __init__(self, *, filter_by: Optional[str] = '', **kwargs) -> None:
+    def __init__(self, *, filter: Optional[str] = None, **kwargs) -> None:
         super().__init__(**kwargs)
-        self.filter = filter_by
-        self._props[self.FILTER_PROP] = filter_by
+        self.filter = filter
+        self._props[self.FILTER_PROP] = filter
 
     def bind_filter_to(self, target_object: Any, target_name: str = 'filter', forward: Callable = lambda x: x):
         bind_to(self, 'filter', target_object, target_name, forward)
@@ -26,9 +26,9 @@ class FilterElement(Element):
         bind(self, 'filter', target_object, target_name, forward=forward, backward=backward)
         return self
 
-    def set_filter(self, filter_by: str) -> None:
-        self.filter = filter_by
+    def set_filter(self, filter: str) -> None:
+        self.filter = filter
 
-    def on_filter_change(self, filter_by: str) -> None:
-        self._props[self.FILTER_PROP] = filter_by
+    def on_filter_change(self, filter: str) -> None:
+        self._props[self.FILTER_PROP] = filter
         self.update()

+ 1 - 2
nicegui/elements/mixins/value_element.py

@@ -8,7 +8,6 @@ from ...events import ValueChangeEventArguments, handle_event
 class ValueElement(Element):
     VALUE_PROP = 'model-value'
     EVENT_ARGS = ['value']
-    EVENT = f'update:{VALUE_PROP}'
     LOOPBACK = True
     value = BindableProperty(on_change=lambda sender, value: sender.on_value_change(value))
 
@@ -24,7 +23,7 @@ class ValueElement(Element):
             self._send_update_on_value_change = self.LOOPBACK
             self.set_value(self._msg_to_value(msg))
             self._send_update_on_value_change = True
-        self.on(self.EVENT, handle_change, self.EVENT_ARGS, throttle=throttle)
+        self.on(f'update:{self.VALUE_PROP}', handle_change, self.EVENT_ARGS, throttle=throttle)
 
     def bind_value_to(self, target_object: Any, target_name: str = 'value', forward: Callable = lambda x: x):
         bind_to(self, 'value', target_object, target_name, forward)

+ 66 - 64
nicegui/elements/table.py

@@ -1,81 +1,83 @@
-from typing import Any, Dict, Optional
+from typing import Callable, Dict, List, Optional
 
 from typing_extensions import Literal
 
-from .mixins.filter_element import FilterElement
-from .mixins.value_element import ValueElement
 from ..element import Element
+from ..events import TableSelectionEventArguments, handle_event
+from .mixins.filter_element import FilterElement
 
 
-class QTr(Element):
-    def __init__(self) -> None:
-        super().__init__('q-tr')
-
-
-class QTh(Element):
-    def __init__(self) -> None:
-        super().__init__('q-th')
-
-
-class QTd(Element):
-    def __init__(self, key: str = '') -> None:
-        super().__init__('q-td')
-        if key:
-            self._props['key'] = key
-
-
-class QTable(ValueElement, FilterElement):
-    row = QTr
-    header = QTh
-    cell = QTd
+class QTable(FilterElement):
 
-    VALUE_PROP = 'selected'
-    EVENT_ARGS = None
-    EVENT = 'selection'
-    def __init__(
-            self,
-            columns: list,
-            rows: list,
-            key: str,
-            title: Optional[str] = None,
-            selection: Optional[Literal['single', 'multiple', 'none']] = 'none',
-            pagination: Optional[int] = 0,
-    ) -> None:
+    def __init__(self,
+                 columns: List[Dict],
+                 rows: List[Dict],
+                 row_key: str = 'id',
+                 title: Optional[str] = None,
+                 selection: Optional[Literal['single', 'multiple']] = None,
+                 rows_per_page: Optional[int] = None,
+                 on_select: Optional[Callable] = None,
+                 ) -> None:
         """QTable
 
-        A component that allows you to display using `QTable <https://quasar.dev/vue-components/table>`_ component.
+        A table based on Quasar's `QTable <https://quasar.dev/vue-components/table>`_ component.
 
-        :param columns: A list of column objects (see `column API <https://quasar.dev/vue-components/table#qtable-api>`_)
-        :param rows: A list of row objects.
-        :param key: The name of the column containing unique data identifying the row.
-        :param title: The title of the table.
-        :param selection: defines the selection behavior (default= 'none').
-            'single': only one cell can be selected at a time.
-            'multiple': more than one cell can be selected at a time.
-            'none': no cells can be selected.
-        :param pagination: defines the number of rows per page. Explicitly set to None to hide pagination (default = 0).
+        :param columns: list of column objects
+        :param rows: list of row objects
+        :param row_key: name of the column containing unique data identifying the row (default: "id")
+        :param title: title of the table
+        :param selection: selection type ("single" or "multiple"; default: `None`)
+        :param rows_per_page: number of rows per page (`None` hides the pagination, 0 means "infinite"; default: 0)
+        :param on_select: callback which is invoked when the selection changes
 
-        If selection is passed to 'single' or 'multiple', then a `selected` property is accessible containing
-              the selected rows.
+        If selection is 'single' or 'multiple', then a `selection` property is accessible containing the selected rows.
         """
+        super().__init__(tag='q-table')
 
-        super().__init__(tag='q-table', value=[], on_value_change=None)
+        self.rows = rows
+        self.row_key = row_key
+        self.selected: List[Dict] = []
 
         self._props['columns'] = columns
         self._props['rows'] = rows
-        self._props['row-key'] = key
+        self._props['row-key'] = row_key
         self._props['title'] = title
-
-        if pagination is None:
-            self._props['hide-pagination'] = True
-            pagination = 0
-        self._props['pagination'] = {
-            'rowsPerPage': pagination
-        }
-
-        self.selected: list = []
-        self._props['selection'] = selection
-
-    def _msg_to_value(self, msg: Dict) -> Any:
-        self.selected = msg['args']
-        return msg['args']['rows']
+        self._props['hide-pagination'] = rows_per_page is None
+        self._props['pagination'] = {'rowsPerPage': rows_per_page or 0}
+        self._props['selection'] = selection or 'none'
+        self._props['selected'] = self.selected
+
+        def handle_selection(msg: Dict) -> None:
+            if msg['args']['added']:
+                self.selected.extend(msg['args']['rows'])
+            else:
+                self.selected[:] = [row for row in self.selected if row[row_key] not in msg['args']['keys']]
+            self.update()
+            arguments = TableSelectionEventArguments(sender=self, client=self.client, selection=self.selected)
+            handle_event(on_select, arguments)
+        self.on('selection', handle_selection)
+
+    def add_rows(self, *rows: Dict) -> None:
+        """Add rows to the table."""
+        self.rows.extend(rows)
+        self.update()
+
+    def remove_rows(self, *rows: Dict) -> None:
+        """Remove rows from the table."""
+        keys = [row[self.row_key] for row in rows]
+        self.rows[:] = [row for row in self.rows if row[self.row_key] not in keys]
+        self.update()
+
+    class row(Element):
+        def __init__(self) -> None:
+            super().__init__('q-tr')
+
+    class header(Element):
+        def __init__(self) -> None:
+            super().__init__('q-th')
+
+    class cell(Element):
+        def __init__(self, key: str = '') -> None:
+            super().__init__('q-td')
+            if key:
+                self._props['key'] = key

+ 2 - 2
nicegui/elements/tree.py

@@ -1,4 +1,4 @@
-from typing import Any, Callable, Dict, Optional
+from typing import Any, Callable, Dict, List, Optional
 
 from nicegui.events import ValueChangeEventArguments, handle_event
 
@@ -7,7 +7,7 @@ from ..element import Element
 
 class Tree(Element):
 
-    def __init__(self, nodes: list, *,
+    def __init__(self, nodes: List, *,
                  node_key: str = 'id',
                  label_key: str = 'label',
                  children_key: str = 'children',

+ 5 - 0
nicegui/events.py

@@ -79,6 +79,11 @@ class ValueChangeEventArguments(EventArguments):
     value: Any
 
 
+@dataclass
+class TableSelectionEventArguments(EventArguments):
+    selection: List[Any]
+
+
 @dataclass
 class KeyboardAction:
     keydown: bool

+ 4 - 7
website/reference.py

@@ -294,19 +294,16 @@ To overlay an SVG, make the `viewBox` exactly the size of the image and provide
 
     @example(ui.qtable, menu)
     def qtable_example():
-
-        fields = [
+        columns = [
             {'name': 'name', 'label': 'Name', 'field': 'name', 'required': True, 'align': 'left'},
-            {'name': 'age', 'label': 'Age', 'field': 'age', 'sortable': True, 'align': 'center'},
+            {'name': 'age', 'label': 'Age', 'field': 'age', 'sortable': True},
         ]
-
-        data = [
+        rows = [
             {'name': 'Alice', 'age': 18},
             {'name': 'Bob', 'age': 21},
             {'name': 'Carol'},
         ]
-
-        ui.qtable(columns=fields, rows=data, key='name', pagination=None).classes('w-full')
+        ui.qtable(columns=columns, rows=rows, row_key='name')
 
     @example(ui.chart, menu)
     def chart_example():