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

test and improve element classes, style and props; make UI updates lazy; remove unnecessary replace argument for props

Falko Schindler 2 лет назад
Родитель
Сommit
bdd6256aa3
2 измененных файлов с 111 добавлено и 30 удалено
  1. 35 30
      nicegui/elements/element.py
  2. 76 0
      tests/test_element.py

+ 35 - 30
nicegui/elements/element.py

@@ -1,6 +1,7 @@
 from __future__ import annotations
 
 import asyncio
+from typing import Dict, Optional
 
 import justpy as jp
 
@@ -11,10 +12,6 @@ from ..task_logger import create_task
 
 def _handle_visibility_change(sender: Element, visible: bool) -> None:
     (sender.view.remove_class if visible else sender.view.set_class)('hidden')
-    try:
-        asyncio.get_running_loop()  # NOTE: make sure we already have an event loop
-    except RuntimeError:
-        return
     sender.update()
 
 
@@ -50,53 +47,61 @@ class Element:
         bind_to(self, 'visible', target_object, target_name, forward=forward)
         return self
 
-    def classes(self, add: str = None, *, remove: str = None, replace: str = None):
+    def classes(self, add: Optional[str] = None, *, remove: Optional[str] = None, replace: Optional[str] = None):
         '''HTML classes to modify the look of the element.
         Every class in the `remove` parameter will be removed from the element.
         Classes are separated with a blank space.
         This can be helpful if the predefined classes by NiceGUI are not wanted in a particular styling.
         '''
-        class_list = [] if replace is not None else self.view.classes.split()
-        class_list = [c for c in class_list if c not in (remove or '')]
+        class_list = self.view.classes.split() if replace is None else []
+        class_list = [c for c in class_list if c not in (remove or '').split()]
         class_list += (add or '').split()
         class_list += (replace or '').split()
-        self.view.classes = ' '.join(class_list)
-
-        self.update()
+        new_classes = ' '.join(dict.fromkeys(class_list))  # NOTE: remove duplicates while preserving order
+        if self.view.classes != new_classes:
+            self.view.classes = new_classes
+            self.update()
         return self
 
-    def style(self, add: str = None, *, remove: str = None, replace: str = None):
+    def style(self, add: str = Optional[str], *, remove: str = Optional[str], replace: str = Optional[str]):
         '''CSS style sheet definitions to modify the look of the element.
         Every style in the `remove` parameter will be removed from the element.
         Styles are separated with a semicolon.
         This can be helpful if the predefined style sheet definitions by NiceGUI are not wanted in a particular styling.
         '''
-        style_list = [] if replace is not None else self.view.style.split(';')
-        style_list = [c for c in style_list if c not in (remove or '').split(';')]
-        style_list += (add or '').split(';')
-        style_list += (replace or '').split(';')
-        self.view.style = ';'.join(style_list)
-
-        self.update()
+        def str_to_dict(s: Optional[str]) -> Dict[str, str]:
+            return dict((word.strip() for word in part.split(':')) for part in s.split(';')) if s else {}
+        style_dict = str_to_dict(self.view.style) if replace is None else {}
+        for key in str_to_dict(remove):
+            del style_dict[key]
+        style_dict.update(str_to_dict(add))
+        style_dict.update(str_to_dict(replace))
+        new_style = ';'.join(f'{key}:{value}' for key, value in style_dict.items())
+        if self.view.style != new_style:
+            self.view.style = new_style
+            self.update()
         return self
 
-    def props(self, add: str = None, *, remove: str = None, replace: str = None):
+    def props(self, add: Optional[str] = None, *, remove: Optional[str] = None):
         '''Quasar props https://quasar.dev/vue-components/button#design to modify the look of the element.
         Boolean props will automatically activated if they appear in the list of the `add` property.
         Props are separated with a blank space.
         Every prop passed to the `remove` parameter will be removed from the element.
         This can be helpful if the predefined props by NiceGUI are not wanted in a particular styling.
         '''
-        for prop in (remove or '').split() + (replace or '').split():
-            setattr(self.view, prop.split('=')[0], None)
-
-        for prop in (add or '').split() + (replace or '').split():
-            if '=' in prop:
-                setattr(self.view, *prop.split('='))
-            else:
-                setattr(self.view, prop, True)
-
-        self.update()
+        def str_to_dict(s: Optional[str]) -> Dict[str, str]:
+            return {prop.split('=')[0]: prop.split('=')[1] if '=' in prop else True for prop in s.split()} if s else {}
+        needs_update = False
+        for key in str_to_dict(remove):
+            if getattr(self.view, key, None) is not None:
+                needs_update = True
+            setattr(self.view, key, None)
+        for key, value in str_to_dict(add).items():
+            if getattr(self.view, key, None) != value:
+                needs_update = True
+            setattr(self.view, key, value)
+        if needs_update:
+            self.update()
         return self
 
     def tooltip(self, text: str, *, props: str = ''):
@@ -116,4 +121,4 @@ class Element:
             asyncio.get_running_loop()
         except RuntimeError:
             return
-            create_task(self.view.update())
+        create_task(self.view.update())

+ 76 - 0
tests/test_element.py

@@ -0,0 +1,76 @@
+from nicegui import ui
+
+from .user import User
+
+
+def test_classes(user: User):
+    label = ui.label('label')
+
+    def classes() -> str:
+        user.open('/')
+        return user.find('label').get_attribute('class')
+
+    assert classes() == ''
+
+    label.classes('one')
+    assert classes() == 'one'
+
+    label.classes('one')
+    assert classes() == 'one'
+
+    label.classes('two three')
+    assert classes() == 'one two three'
+
+    label.classes(remove='two')
+    assert classes() == 'one three'
+
+    label.classes(replace='four')
+    assert classes() == 'four'
+
+
+def test_style(user: User):
+    label = ui.label('label')
+
+    def style() -> str:
+        user.open('/')
+        return user.find('label').get_attribute('style')
+
+    assert style() == ''
+
+    label.style('color: red')
+    assert style() == 'color: red;'
+
+    label.style('color: red')
+    assert style() == 'color: red;'
+
+    label.style('color: blue')
+    assert style() == 'color: blue;'
+
+    label.style('font-weight: bold')
+    assert style() == 'color: blue; font-weight: bold;'
+
+    label.style(remove='color: blue')
+    assert style() == 'font-weight: bold;'
+
+    label.style(replace='text-decoration: underline')
+    assert style() == 'text-decoration: underline;'
+
+
+def test_props(user: User):
+    input = ui.input('input')
+
+    def props() -> str:
+        user.open('/')
+        element = user.selenium.find_element_by_tag_name('label')
+        return [c.replace('q-field--', '') for c in element.get_attribute('class').split() if c.startswith('q-field--')]
+
+    assert props() == ['standard', 'labeled']
+
+    input.props('dark')
+    assert props() == ['standard', 'labeled', 'dark']
+
+    input.props('dark')
+    assert props() == ['standard', 'labeled', 'dark']
+
+    input.props(remove='dark')
+    assert props() == ['standard', 'labeled']