Просмотр исходного кода

Merge branch 'zauberzeug:main' into plotly_orjson

Rino B 2 лет назад
Родитель
Сommit
d3830eb8e6

+ 11 - 2
CONTRIBUTING.md

@@ -31,6 +31,13 @@ python3 -m pip install -e .
 
 This will install the `nicegui` package and all its dependencies, and link it to your local development environment so that changes you make to the code will be immediately reflected.
 Thereby enabling you to use your local version of NiceGUI in other projects.
+To run the tests you need some additional setup which is described in [tests/README.md](https://github.com/zauberzeug/nicegui/blob/main/tests/README.md).
+
+There is no special Python version required for development.
+At Zauberzeug we mainly use 3.11.
+This means we sometimes miss some incompatibilities with 3.7.
+But these will hopefully be uncovered by the GitHub Actions (see below).
+Also we use the 3.7 Docker container described below to verify compatibility in cases of uncertainty.
 
 ### Alternative: Docker
 
@@ -43,7 +50,7 @@ Simply start the development container using the command:
 
 By default, the development server listens to http://localhost:80/.
 
-The configuration is written in the `docker-compose.yml` file and automatically loads the `main.py` which is contains the website https://nicegui.io.
+The configuration is written in the `docker-compose.yml` file and automatically loads the `main.py` which contains the website https://nicegui.io.
 Every code change will result in reloading the content.
 We use Python 3.7 as a base to ensure compatibility (see `development.dockerfile`).
 
@@ -68,7 +75,7 @@ Then the formatting rules are applied whenever you save a file.
 
 ## Running tests
 
-Our tests are build with pytest and require python-selenium with Chrome driver.
+Our tests are built with pytest and require python-selenium with ChromeDriver.
 See [tests/README.md](https://github.com/zauberzeug/nicegui/blob/main/tests/README.md) for detailed installation instructions and more infos about the test infrastructure and tricks for daily usage.
 
 Before submitting a pull request, please make sure that all tests are passing.
@@ -84,6 +91,8 @@ New features should be well documented in [website/reference.py](https://github.
 By calling the `example(...)` function with an element as a parameter the docstring is used as a description.
 The docstrings are written in restructured-text.
 
+Because it has [numerous benefits](https://nick.groenen.me/notes/one-sentence-per-line/) we write each sentence in a new line.
+
 ## Pull requests
 
 To get started, fork the repository on GitHub, make your changes, and open a pull request (PR) with a detailed description of the changes you've made.

+ 8 - 8
examples/local_file_picker/local_file_picker.py

@@ -27,23 +27,23 @@ class local_file_picker(ui.dialog):
         self.show_hidden_files = show_hidden_files
 
         with self, ui.card():
-            self.table = ui.table({
+            self.grid = ui.aggrid({
                 'columnDefs': [{'field': 'name', 'headerName': 'File'}],
                 'rowSelection': 'multiple' if multiple else 'single',
             }, html_columns=[0]).classes('w-96').on('cellDoubleClicked', self.handle_double_click)
             with ui.row().classes('w-full justify-end'):
                 ui.button('Cancel', on_click=self.close).props('outline')
                 ui.button('Ok', on_click=self._handle_ok)
-        self.update_table()
+        self.update_grid()
 
-    def update_table(self) -> None:
+    def update_grid(self) -> None:
         paths = list(self.path.glob('*'))
         if not self.show_hidden_files:
             paths = [p for p in paths if not p.name.startswith('.')]
         paths.sort(key=lambda p: p.name.lower())
         paths.sort(key=lambda p: not p.is_dir())
 
-        self.table.options['rowData'] = [
+        self.grid.options['rowData'] = [
             {
                 'name': f'📁 <strong>{p.name}</strong>' if p.is_dir() else p.name,
                 'path': str(p),
@@ -52,19 +52,19 @@ class local_file_picker(ui.dialog):
         ]
         if self.upper_limit is None and self.path != self.path.parent or \
                 self.upper_limit is not None and self.path != self.upper_limit:
-            self.table.options['rowData'].insert(0, {
+            self.grid.options['rowData'].insert(0, {
                 'name': '📁 <strong>..</strong>',
                 'path': str(self.path.parent),
             })
-        self.table.update()
+        self.grid.update()
 
     async def handle_double_click(self, msg: dict) -> None:
         self.path = Path(msg['args']['data']['path'])
         if self.path.is_dir():
-            self.update_table()
+            self.update_grid()
         else:
             self.submit([str(self.path)])
 
     async def _handle_ok(self):
-        rows = await ui.run_javascript(f'getElement({self.table.id}).gridOptions.api.getSelectedRows()')
+        rows = await ui.run_javascript(f'getElement({self.grid.id}).gridOptions.api.getSelectedRows()')
         self.submit([r['path'] for r in rows])

+ 56 - 0
examples/trello_cards/main.py

@@ -0,0 +1,56 @@
+#!/usr/bin/env python3
+from __future__ import annotations
+
+from typing import Optional
+
+from nicegui import ui
+
+
+class Column(ui.column):
+
+    def __init__(self, name: str) -> None:
+        super().__init__()
+        with self.classes('bg-gray-200 w-48 p-4 rounded shadow'):
+            ui.label(name).classes('text-bold')
+        self.on('dragover.prevent', self.highlight)
+        self.on('dragleave', self.unhighlight)
+        self.on('drop', self.move_card)
+
+    def highlight(self) -> None:
+        self.classes(add='bg-gray-400')
+
+    def unhighlight(self) -> None:
+        self.classes(remove='bg-gray-400')
+
+    def move_card(self) -> None:
+        self.unhighlight()
+        Card.dragged.parent_slot.parent.remove(Card.dragged)
+        with self:
+            Card(Card.dragged.text)
+
+
+class Card(ui.card):
+    dragged: Optional[Card] = None
+
+    def __init__(self, text: str) -> None:
+        super().__init__()
+        self.text = text
+        with self.props('draggable').classes('w-full cursor-pointer'):
+            ui.label(self.text)
+        self.on('dragstart', self.handle_dragstart)
+
+    def handle_dragstart(self) -> None:
+        Card.dragged = self
+
+
+with ui.row():
+    with Column('Next'):
+        Card('Clean up the kitchen')
+        Card('Do the laundry')
+        Card('Go to the gym')
+    with Column('Doing'):
+        Card('Make dinner')
+    with Column('Done'):
+        Card('Buy groceries')
+
+ui.run()

+ 1 - 0
main.py

@@ -238,6 +238,7 @@ The command searches for `main.py` in in your current directory and makes the ap
             example_link('Local File Picker', 'demonstrates a dialog for selecting files locally on the server')
             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')
 
     with ui.row().classes('bg-primary w-full min-h-screen mt-16'):
         link_target('why')

+ 13 - 0
nicegui/deprecation.py

@@ -0,0 +1,13 @@
+import warnings
+from functools import wraps
+
+warnings.simplefilter('always', DeprecationWarning)
+
+
+def deprecated(func: type, old_name: str, new_name: str, issue: int) -> type:
+    @wraps(func)
+    def wrapped(*args, **kwargs):
+        url = f'https://github.com/zauberzeug/nicegui/issues/{issue}'
+        warnings.warn(DeprecationWarning(f'{old_name} is deprecated, use {new_name} instead ({url})'))
+        return func(*args, **kwargs)
+    return wrapped

+ 0 - 0
nicegui/elements/table.js → nicegui/elements/aggrid.js


+ 7 - 7
nicegui/elements/table.py → nicegui/elements/aggrid.py

@@ -4,15 +4,15 @@ from ..dependencies import register_component
 from ..element import Element
 from ..functions.javascript import run_javascript
 
-register_component('table', __file__, 'table.js', ['lib/ag-grid-community.min.js'])
+register_component('aggrid', __file__, 'aggrid.js', ['lib/ag-grid-community.min.js'])
 
 
-class Table(Element):
+class AgGrid(Element):
 
     def __init__(self, options: Dict, *, html_columns: List[int] = [], theme: str = 'balham') -> None:
-        """Table
+        """AG Grid
 
-        An element to create a table using `AG Grid <https://www.ag-grid.com/>`_.
+        An element to create a grid using `AG Grid <https://www.ag-grid.com/>`_.
 
         The `call_api_method` method can be used to call an AG Grid API method.
 
@@ -20,7 +20,7 @@ class Table(Element):
         :param html_columns: list of columns that should be rendered as HTML (default: `[]`)
         :param theme: AG Grid theme (default: 'balham')
         """
-        super().__init__('table')
+        super().__init__('aggrid')
         self._props['options'] = options
         self._props['html_columns'] = html_columns
         self._classes = [f'ag-theme-{theme}', 'w-full', 'h-64']
@@ -46,7 +46,7 @@ class Table(Element):
     async def get_selected_rows(self) -> List[Dict]:
         """Get the currently selected rows.
 
-        This method is especially useful when the table is configured with ``rowSelection: 'multiple'``.
+        This method is especially useful when the grid is configured with ``rowSelection: 'multiple'``.
 
         See `AG Grid API <https://www.ag-grid.com/javascript-data-grid/row-selection/#reference-selection-getSelectedRows>`_ for more information.
 
@@ -57,7 +57,7 @@ class Table(Element):
     async def get_selected_row(self) -> Optional[Dict]:
         """Get the single currently selected row.
 
-        This method is especially useful when the table is configured with ``rowSelection: 'single'``.
+        This method is especially useful when the grid is configured with ``rowSelection: 'single'``.
 
         :return: row data of the first selection if any row is selected, otherwise `None`
         """

+ 0 - 1
nicegui/elements/colors.js

@@ -1,5 +1,4 @@
 export default {
-  template: '<span style="display:none"></span>',
   mounted() {
     for (let name in this.$props) {
       document.body.style.setProperty("--q-" + name, this.$props[name]);

+ 6 - 0
nicegui/elements/interactive_image.js

@@ -55,6 +55,12 @@ export default {
           mouse_event_type: type,
           image_x: (e.offsetX * e.target.naturalWidth) / e.target.clientWidth,
           image_y: (e.offsetY * e.target.naturalHeight) / e.target.clientHeight,
+          button: e.button,
+          buttons: e.buttons,
+          altKey: e.altKey,
+          ctrlKey: e.ctrlKey,
+          metaKey: e.metaKey,
+          shiftKey: e.shiftKey,
         });
       });
     }

+ 6 - 0
nicegui/elements/interactive_image.py

@@ -43,6 +43,12 @@ class InteractiveImage(SourceElement, ContentElement):
                 type=msg['args'].get('mouse_event_type'),
                 image_x=msg['args'].get('image_x'),
                 image_y=msg['args'].get('image_y'),
+                button=msg['args'].get('button', 0),
+                buttons=msg['args'].get('buttons', 0),
+                alt=msg['args'].get('alt', False),
+                ctrl=msg['args'].get('ctrl', False),
+                meta=msg['args'].get('meta', False),
+                shift=msg['args'].get('shift', False),
             )
             return handle_event(on_mouse, arguments)
         self.on('mouse', handle_mouse)

+ 0 - 1
nicegui/elements/keyboard.js

@@ -1,5 +1,4 @@
 export default {
-  template: "<span></span>",
   mounted() {
     for (const event of this.events) {
       document.addEventListener(event, (evt) => {

+ 0 - 1
nicegui/elements/keyboard.py

@@ -26,7 +26,6 @@ class Keyboard(Element):
         self.active = active
         self._props['events'] = ['keydown', 'keyup']
         self._props['repeating'] = repeating
-        self.style('display: none')
         self.on('key', self.handle_key)
 
     def handle_key(self, msg: Dict) -> None:

+ 3 - 2
nicegui/elements/log.py

@@ -1,5 +1,5 @@
 from collections import deque
-from typing import Optional
+from typing import Any, Optional
 
 from ..dependencies import register_component
 from ..element import Element
@@ -23,7 +23,8 @@ class Log(Element):
         self.style('opacity: 1 !important; cursor: text !important')
         self.lines: deque[str] = deque(maxlen=max_lines)
 
-    def push(self, line: str) -> None:
+    def push(self, line: Any) -> None:
+        line = str(line)
         self.lines.extend(line.splitlines())
         self._props['lines'] = '\n'.join(self.lines)
         self.run_method('push', line)

+ 6 - 0
nicegui/events.py

@@ -52,6 +52,12 @@ class MouseEventArguments(EventArguments):
     type: str
     image_x: float
     image_y: float
+    button: int
+    buttons: int
+    alt: bool
+    ctrl: bool
+    meta: bool
+    shift: bool
 
 
 @dataclass

+ 3 - 2
nicegui/functions/notify.py

@@ -1,9 +1,9 @@
-from typing import Optional, Union
+from typing import Any, Optional, Union
 
 from .. import globals, outbox
 
 
-def notify(message: str, *,
+def notify(message: Any, *,
            position: str = 'bottom',
            closeBtn: Union[bool, str] = False,
            type: Optional[str] = None,
@@ -23,4 +23,5 @@ def notify(message: str, *,
     Note: You can pass additional keyword arguments according to `Quasar's Notify API <https://quasar.dev/quasar-plugins/notify#notify-api>`_.
     """
     options = {key: value for key, value in locals().items() if not key.startswith('_') and value is not None}
+    options['message'] = str(message)
     outbox.enqueue_message('notify', options, globals.get_client().id)

+ 5 - 4
nicegui/page.py

@@ -79,9 +79,10 @@ class page:
         parameters = [p for p in inspect.signature(func).parameters.values() if p.name != 'client']
         # NOTE adding request as a parameter so we can pass it to the client in the decorated function
         if 'request' not in {p.name for p in parameters}:
-            parameters.append(inspect.Parameter('request', inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=Request))
+            request = inspect.Parameter('request', inspect.Parameter.POSITIONAL_OR_KEYWORD, annotation=Request)
+            parameters.insert(0, request)
         decorated.__signature__ = inspect.Signature(parameters)
 
-        globals.page_routes[decorated] = self.path
-
-        return globals.app.get(self.path)(decorated)
+        globals.app.get(self.path)(decorated)
+        globals.page_routes[func] = self.path
+        return func

+ 4 - 1
nicegui/run.py

@@ -28,6 +28,7 @@ def run(*,
         uvicorn_reload_excludes: str = '.*, .py[cod], .sw.*, ~*',
         exclude: str = '',
         tailwind: bool = True,
+        **kwargs,
         ) -> None:
     '''ui.run
 
@@ -47,8 +48,9 @@ def run(*,
     :param uvicorn_reload_includes: string with comma-separated list of glob-patterns which trigger reload on modification (default: `'.py'`)
     :param uvicorn_reload_excludes: string with comma-separated list of glob-patterns which should be ignored for reload (default: `'.*, .py[cod], .sw.*, ~*'`)
     :param exclude: comma-separated string to exclude elements (with corresponding JavaScript libraries) to save bandwidth
-      (possible entries: audio, chart, colors, interactive_image, joystick, keyboard, log, markdown, mermaid, plotly, scene, table, video)
+      (possible entries: aggrid, audio, chart, colors, interactive_image, joystick, keyboard, log, markdown, mermaid, plotly, scene, video)
     :param tailwind: whether to use Tailwind (experimental, default: `True`)
+    :param kwargs: additional keyword arguments are passed to `uvicorn.run`
     '''
     globals.ui_run_has_been_called = True
     globals.host = host
@@ -82,6 +84,7 @@ def run(*,
         reload_excludes=split_args(uvicorn_reload_excludes) if reload else None,
         reload_dirs=split_args(uvicorn_reload_dirs) if reload else None,
         log_level=uvicorn_logging_level,
+        **kwargs,
     )
     globals.server = uvicorn.Server(config=config)
 

+ 5 - 2
nicegui/ui.py

@@ -1,6 +1,8 @@
 import os
 
+from .deprecation import deprecated
 from .element import Element as element
+from .elements.aggrid import AgGrid as aggrid
 from .elements.audio import Audio as audio
 from .elements.avatar import Avatar as avatar
 from .elements.badge import Badge as badge
@@ -44,7 +46,6 @@ from .elements.separator import Separator as separator
 from .elements.slider import Slider as slider
 from .elements.spinner import Spinner as spinner
 from .elements.switch import Switch as switch
-from .elements.table import Table as table
 from .elements.tabs import Tab as tab
 from .elements.tabs import TabPanel as tab_panel
 from .elements.tabs import TabPanels as tab_panels
@@ -72,7 +73,9 @@ from .page_layout import RightDrawer as right_drawer
 from .run import run
 from .run_with import run_with
 
+table = deprecated(aggrid, 'ui.table', 'ui.aggrid', 370)
+
 if os.environ.get('MATPLOTLIB', 'true').lower() == 'true':
     from .elements.line_plot import LinePlot as line_plot
-    from .elements.pyplot import Pyplot as plot  # NOTE: deprecated
     from .elements.pyplot import Pyplot as pyplot
+    plot = deprecated(pyplot, 'ui.plot', 'ui.pyplot', 317)

+ 6 - 6
tests/README.md

@@ -8,7 +8,7 @@ Even if automated testing needs a lot of infrastructure and results in long exec
 
 ## Setup
 
-Please be aware that the below commands installs the latest version of the ChromeDriver binary, which is compatible with the version of Google Chrome installed on your system.
+Please be aware that the below commands install the latest version of the ChromeDriver binary, which is compatible with the version of Google Chrome installed on your system.
 If you have a different version of Chrome installed, you may need to install a different version of ChromeDriver or update your Chrome installation to be compatible with the installed ChromeDriver version.
 
 ### Mac
@@ -48,14 +48,14 @@ Please refer to the documentation for your distribution for more information.
 
 ## Usage
 
-Because selenium queries are quite cumbersome and lengthily, we introduced a `Screen` class.
+Because Selenium queries are quite cumbersome and lengthy, we introduced a `Screen` class.
 This provides a high-level interface to work with the currently displayed state of the web browser.
 The workflow is as follows:
 
-1. get the screen fixture by providing `screen: Screen` as an argument to the function
-2. write your NiceGUI code inside the function
-3. use `screen.open(...)` with the appropriate url path to start querying the website
-4. for example use `screen.should_contain(...)` with some text as parameter to ensure that the text is shown
+1. Get the `screen` fixture by providing `screen: Screen` as an argument to the function.
+2. Write your NiceGUI code inside the function.
+3. Use `screen.open(...)` with the appropriate URL path to start querying the website.
+4. For example, use `screen.should_contain(...)` with some text as parameter to ensure that the text is shown.
 
 Here is a very simple example:
 

+ 3 - 1
tests/requirements.txt

@@ -1,3 +1,5 @@
 pytest
 pytest-selenium
-selenium
+selenium
+autopep8
+icecream

+ 15 - 15
tests/test_table.py → tests/test_aggrid.py

@@ -7,7 +7,7 @@ from .screen import Screen
 
 
 def test_update_table(screen: Screen):
-    table = ui.table({
+    grid = ui.aggrid({
         'columnDefs': [{'field': 'name'}, {'field': 'age'}],
         'rowData': [{'name': 'Alice', 'age': 18}],
     })
@@ -18,25 +18,25 @@ def test_update_table(screen: Screen):
     screen.should_contain('Alice')
     screen.should_contain('18')
 
-    table.options['rowData'][0]['age'] = 42
-    table.update()
+    grid.options['rowData'][0]['age'] = 42
+    grid.update()
     screen.should_contain('42')
 
 
 def test_add_row(screen: Screen):
-    table = ui.table({
+    grid = ui.aggrid({
         'columnDefs': [{'field': 'name'}, {'field': 'age'}],
         'rowData': [],
     })
-    ui.button('Update', on_click=table.update)
+    ui.button('Update', on_click=grid.update)
 
     screen.open('/')
-    table.options['rowData'].append({'name': 'Alice', 'age': 18})
+    grid.options['rowData'].append({'name': 'Alice', 'age': 18})
     screen.click('Update')
     screen.wait(0.5)
     screen.should_contain('Alice')
     screen.should_contain('18')
-    table.options['rowData'].append({'name': 'Bob', 'age': 21})
+    grid.options['rowData'].append({'name': 'Bob', 'age': 21})
     screen.click('Update')
     screen.wait(0.5)
     screen.should_contain('Alice')
@@ -46,11 +46,11 @@ def test_add_row(screen: Screen):
 
 
 def test_click_cell(screen: Screen):
-    table = ui.table({
+    grid = ui.aggrid({
         'columnDefs': [{'field': 'name'}, {'field': 'age'}],
         'rowData': [{'name': 'Alice', 'age': 18}],
     })
-    table.on('cellClicked', lambda msg: ui.label(f'{msg["args"]["data"]["name"]} has been clicked!'))
+    grid.on('cellClicked', lambda msg: ui.label(f'{msg["args"]["data"]["name"]} has been clicked!'))
 
     screen.open('/')
     screen.click('Alice')
@@ -58,7 +58,7 @@ def test_click_cell(screen: Screen):
 
 
 def test_html_columns(screen: Screen):
-    ui.table({
+    ui.aggrid({
         'columnDefs': [{'field': 'name'}, {'field': 'age'}],
         'rowData': [{'name': '<span class="text-bold">Alice</span>', 'age': 18}],
     }, html_columns=[0])
@@ -70,12 +70,12 @@ def test_html_columns(screen: Screen):
 
 
 def test_call_api_method_with_argument(screen: Screen):
-    table = ui.table({
+    grid = ui.aggrid({
         'columnDefs': [{'field': 'name', 'filter': True}],
         'rowData': [{'name': 'Alice'}, {'name': 'Bob'}, {'name': 'Carol'}],
     })
     filter = {'name': {'filterType': 'text', 'type': 'equals', 'filter': 'Alice'}}
-    ui.button('Filter', on_click=lambda: table.call_api_method('setFilterModel', filter))
+    ui.button('Filter', on_click=lambda: grid.call_api_method('setFilterModel', filter))
 
     screen.open('/')
     screen.should_contain('Alice')
@@ -88,18 +88,18 @@ def test_call_api_method_with_argument(screen: Screen):
 
 
 def test_get_selected_rows(screen: Screen):
-    table = ui.table({
+    grid = ui.aggrid({
         'columnDefs': [{'field': 'name'}],
         'rowData': [{'name': 'Alice'}, {'name': 'Bob'}, {'name': 'Carol'}],
         'rowSelection': 'multiple',
     })
 
     async def get_selected_rows():
-        ui.label(str(await table.get_selected_rows()))
+        ui.label(str(await grid.get_selected_rows()))
     ui.button('Get selected rows', on_click=get_selected_rows)
 
     async def get_selected_row():
-        ui.label(str(await table.get_selected_row()))
+        ui.label(str(await grid.get_selected_row()))
     ui.button('Get selected row', on_click=get_selected_row)
 
     screen.open('/')

+ 3 - 3
tests/test_date.py

@@ -14,7 +14,7 @@ def test_date(screen: Screen):
 
 
 def test_date_with_range(screen: Screen):
-    ui.date().props('range')
+    ui.date().props('range default-year-month=2023/01')
 
     screen.open('/')
     screen.click('16')
@@ -27,7 +27,7 @@ def test_date_with_range(screen: Screen):
 
 
 def test_date_with_multi_selection(screen: Screen):
-    ui.date().props('multiple')
+    ui.date().props('multiple default-year-month=2023/01')
 
     screen.open('/')
     screen.click('16')
@@ -40,7 +40,7 @@ def test_date_with_multi_selection(screen: Screen):
 
 
 def test_date_with_range_and_multi_selection(screen: Screen):
-    ui.date().props('range multiple')
+    ui.date().props('range multiple default-year-month=2023/01')
 
     screen.open('/')
     screen.click('16')

+ 1 - 4
tests/test_keyboard.py

@@ -1,5 +1,3 @@
-from selenium.webdriver.common.by import By
-
 from nicegui import ui
 
 from .screen import Screen
@@ -7,10 +5,9 @@ from .screen import Screen
 
 def test_keyboard(screen: Screen):
     result = ui.label()
-    keyboard = ui.keyboard(on_key=lambda e: result.set_text(f'{e.key, e.action}'))
+    ui.keyboard(on_key=lambda e: result.set_text(f'{e.key, e.action}'))
 
     screen.open('/')
-    assert screen.selenium.find_element(By.ID, keyboard.id)
     screen.wait(1.0)
     screen.type('t')
     screen.should_contain('t, KeyboardAction(keydown=False, keyup=True, repeat=False)')

+ 5 - 5
website/reference.py

@@ -263,9 +263,9 @@ To overlay an SVG, make the `viewBox` exactly the size of the image and provide
 
     h3('Data Elements')
 
-    @example(ui.table, menu)
+    @example(ui.aggrid, menu)
     def table_example():
-        table = ui.table({
+        grid = ui.aggrid({
             'columnDefs': [
                 {'headerName': 'Name', 'field': 'name'},
                 {'headerName': 'Age', 'field': 'age'},
@@ -279,11 +279,11 @@ To overlay an SVG, make the `viewBox` exactly the size of the image and provide
         }).classes('max-h-40')
 
         def update():
-            table.options['rowData'][0]['age'] += 1
-            table.update()
+            grid.options['rowData'][0]['age'] += 1
+            grid.update()
 
         ui.button('Update', on_click=update)
-        ui.button('Select all', on_click=lambda: table.call_api_method('selectAll'))
+        ui.button('Select all', on_click=lambda: grid.call_api_method('selectAll'))
 
     @example(ui.chart, menu)
     def chart_example():