Преглед на файлове

Merge remote-tracking branch 'origin/carousel_component' into carousel_component

angelisM преди 1 година
родител
ревизия
43e8d8939d

+ 1 - 1
nicegui/app.py

@@ -126,7 +126,7 @@ class App(FastAPI):
         :param url_path: string that starts with a slash "/" and identifies the path at which the files should be served
         :param local_directory: local folder with files to serve as media content
         """
-        @self.get(url_path + '/{filename}')
+        @self.get(url_path + '/{filename:path}')
         async def read_item(request: Request, filename: str) -> StreamingResponse:
             filepath = Path(local_directory) / filename
             if not filepath.is_file():

+ 3 - 0
nicegui/elements/input.js

@@ -47,6 +47,9 @@ export default {
     },
   },
   methods: {
+    updateValue() {
+      this.inputValue = this.value;
+    },
     perform_autocomplete(e) {
       if (this.shadowText) {
         this.inputValue += this.shadowText;

+ 5 - 0
nicegui/elements/input.py

@@ -62,3 +62,8 @@ class Input(ValidationElement, DisableableElement):
         """Set the autocomplete list."""
         self._props['autocomplete'] = autocomplete
         self.update()
+
+    def on_value_change(self, value: Any) -> None:
+        super().on_value_change(value)
+        if self._send_update_on_value_change:
+            self.run_method('updateValue')

+ 26 - 17
nicegui/elements/joystick.py

@@ -1,4 +1,4 @@
-from typing import Any, Callable, Optional
+from typing import Any, Callable, Dict, Optional
 
 from ..dependencies import register_component
 from ..element import Element
@@ -26,20 +26,29 @@ class Joystick(Element):
         :param options: arguments like `color` which should be passed to the `underlying nipple.js library <https://github.com/yoannmoinet/nipplejs#options>`_
         """
         super().__init__('joystick')
-        self.on('start',
-                lambda _: handle_event(on_start, JoystickEventArguments(sender=self,
-                                                                        client=self.client,
-                                                                        action='start')))
-        self.on('move',
-                lambda msg: handle_event(on_move, JoystickEventArguments(sender=self,
-                                                                         client=self.client,
-                                                                         action='move',
-                                                                         x=msg['args']['data']['vector']['x'],
-                                                                         y=msg['args']['data']['vector']['y'])),
-                args=['data'],
-                throttle=throttle)
-        self.on('end',
-                lambda _: handle_event(on_end, JoystickEventArguments(sender=self,
-                                                                      client=self.client,
-                                                                      action='end')))
         self._props['options'] = options
+        self.active = False
+
+        def handle_start() -> None:
+            self.active = True
+            handle_event(on_start, JoystickEventArguments(sender=self,
+                                                          client=self.client,
+                                                          action='start'))
+
+        def handle_move(msg: Dict) -> None:
+            if self.active:
+                handle_event(on_move, JoystickEventArguments(sender=self,
+                                                             client=self.client,
+                                                             action='move',
+                                                             x=msg['args']['data']['vector']['x'],
+                                                             y=msg['args']['data']['vector']['y']))
+
+        def handle_end() -> None:
+            self.active = False
+            handle_event(on_end, JoystickEventArguments(sender=self,
+                                                        client=self.client,
+                                                        action='end'))
+
+        self.on('start', handle_start)
+        self.on('move', handle_move, args=['data'], throttle=throttle),
+        self.on('end', handle_end)

+ 1 - 0
nicegui/elements/plotly.py

@@ -37,6 +37,7 @@ class Plotly(Element):
         self.update()
 
     def update(self) -> None:
+        super().update()
         self._props['options'] = self._get_figure_json()
         self.run_method('update', self._props['options'])
 

+ 11 - 2
nicegui/elements/select.py

@@ -19,6 +19,7 @@ class Select(ChoiceElement, DisableableElement):
                  on_change: Optional[Callable[..., Any]] = None,
                  with_input: bool = False,
                  multiple: bool = False,
+                 clearable: bool = False,
                  ) -> None:
         """Dropdown Selection
 
@@ -30,6 +31,7 @@ class Select(ChoiceElement, DisableableElement):
         :param on_change: callback to execute when selection changes
         :param with_input: whether to show an input field to filter the options
         :param multiple: whether to allow multiple selections
+        :param clearable: whether to add a button to clear the selection
         """
         self.multiple = multiple
         if multiple:
@@ -48,6 +50,7 @@ class Select(ChoiceElement, DisableableElement):
             self._props['fill-input'] = True
             self._props['input-debounce'] = 0
         self._props['multiple'] = multiple
+        self._props['clearable'] = clearable
 
     def on_filter(self, event: Dict) -> None:
         self.options = [
@@ -59,9 +62,15 @@ class Select(ChoiceElement, DisableableElement):
 
     def _msg_to_value(self, msg: Dict) -> Any:
         if self.multiple:
-            return [self._values[arg['value']] for arg in msg['args']]
+            if msg['args'] is None:
+                return []
+            else:
+                return [self._values[arg['value']] for arg in msg['args']]
         else:
-            return self._values[msg['args']['value']]
+            if msg['args'] is None:
+                return None
+            else:
+                return self._values[msg['args']['value']]
 
     def _value_to_model_value(self, value: Any) -> Any:
         if self.multiple:

+ 1 - 0
nicegui/elements/separator.py

@@ -7,6 +7,7 @@ class Separator(Element):
         """Separator
 
         A separator for cards, menus and other component containers.
+        Similar to HTML's <hr> tag.
         """
         super().__init__('q-separator')
         self._classes = ['nicegui-separator']

+ 1 - 1
nicegui/nicegui.py

@@ -100,7 +100,7 @@ def print_welcome_message():
     addresses = [(f'http://{ip}:{port}' if port != '80' else f'http://{ip}') for ip in ['localhost'] + sorted(ips)]
     if len(addresses) >= 2:
         addresses[-1] = 'and ' + addresses[-1]
-    print(f'NiceGUI ready to go on {", ".join(addresses)}')
+    print(f'NiceGUI ready to go on {", ".join(addresses)}', flush=True)
 
 
 @app.on_event('shutdown')

+ 16 - 0
tests/test_input.py

@@ -125,3 +125,19 @@ def test_clearable_input(screen: Screen):
     screen.should_contain('value: foo')
     screen.click('cancel')
     screen.should_contain('value: None')
+
+
+def test_update_input(screen: Screen):
+    input = ui.input('Name', value='Pete')
+
+    screen.open('/')
+    element = screen.selenium.find_element(By.XPATH, '//*[@aria-label="Name"]')
+    assert element.get_attribute('value') == 'Pete'
+
+    element.send_keys('r')
+    screen.wait(0.5)
+    assert element.get_attribute('value') == 'Peter'
+
+    input.value = 'Pete'
+    screen.wait(0.5)
+    assert element.get_attribute('value') == 'Pete'

+ 1 - 0
tests/test_storage.py

@@ -89,6 +89,7 @@ async def test_access_user_storage_from_fastapi(screen: Screen):
         response = await http_client.get(f'http://localhost:{PORT}/api')
         assert response.status_code == 200
         assert response.text == '"OK"'
+        await asyncio.sleep(0.5)  # wait for storage to be written
         assert next(Path('.nicegui').glob('storage_user_*.json')).read_text() == '{"msg": "yes"}'
 
 

+ 1 - 0
website/documentation.py

@@ -165,6 +165,7 @@ def create_full() -> None:
         ui.button('Clear', on_click=container.clear)
 
     load_demo(ui.expansion)
+    load_demo(ui.separator)
     load_demo(ui.splitter)
     load_demo('tabs')
     load_demo(ui.stepper)

+ 1 - 1
website/more_documentation/bindings_documentation.py

@@ -63,7 +63,7 @@ def more() -> None:
 
         # @ui.page('/')
         # def index():
-        #     ui.textarea('This note is kept between visits') \
+        #     ui.textarea('This note is kept between visits') 
         #         .classes('w-full').bind_value(app.storage.user, 'note')
         # END OF DEMO
         ui.textarea('This note is kept between visits').classes('w-full').bind_value(app.storage.user, 'note')

+ 32 - 0
website/more_documentation/log_documentation.py

@@ -1,4 +1,5 @@
 from nicegui import ui
+from website.documentation_tools import text_demo
 
 
 def main_demo() -> None:
@@ -6,3 +7,34 @@ def main_demo() -> None:
 
     log = ui.log(max_lines=10).classes('w-full h-20')
     ui.button('Log time', on_click=lambda: log.push(datetime.now().strftime('%X.%f')[:-5]))
+
+
+def more() -> None:
+    @text_demo('Attach to a logger', '''
+        You can attach a `ui.log` element to a Python logger object so that log messages are pushed to the log element.
+    ''')
+    def logger_handler():
+        import logging
+        from datetime import datetime
+
+        logger = logging.getLogger()
+
+        class LogElementHandler(logging.Handler):
+            """A logging handler that emits messages to a log element."""
+
+            def __init__(self, element: ui.log, level: int = logging.NOTSET) -> None:
+                self.element = element
+                super().__init__(level)
+
+            def emit(self, record: logging.LogRecord) -> None:
+                try:
+                    msg = self.format(record)
+                    self.element.push(msg)
+                except (KeyboardInterrupt, SystemExit):
+                    raise
+                except:
+                    self.handleError(record)
+
+        log = ui.log(max_lines=10).classes('w-full')
+        logger.addHandler(LogElementHandler(log))
+        ui.button('Log time', on_click=lambda: logger.warning(datetime.now().strftime('%X.%f')[:-5]))

+ 7 - 0
website/more_documentation/separator_documentation.py

@@ -0,0 +1,7 @@
+from nicegui import ui
+
+
+def main_demo() -> None:
+    ui.label('text above')
+    ui.separator()
+    ui.label('text below')

+ 2 - 6
website/more_documentation/table_documentation.py

@@ -74,15 +74,11 @@ def more() -> None:
             {'name': 'Bob', 'age': 21},
             {'name': 'Carol'},
         ]
-        visible_columns = {column['name'] for column in columns}
         table = ui.table(columns=columns, rows=rows, row_key='name')
 
         def toggle(column: Dict, visible: bool) -> None:
-            if visible:
-                visible_columns.add(column['name'])
-            else:
-                visible_columns.remove(column['name'])
-            table._props['columns'] = [column for column in columns if column['name'] in visible_columns]
+            column['classes'] = '' if visible else 'hidden'
+            column['headerClasses'] = '' if visible else 'hidden'
             table.update()
 
         with ui.button(icon='menu'):