Browse Source

Merge branch 'background'

# Conflicts:
#	website/documentation.py
Falko Schindler 2 years ago
parent
commit
5455de5c8d

+ 11 - 9
main.py

@@ -314,27 +314,29 @@ def documentation_page():
 
 @ui.page('/documentation/{name}')
 def documentation_page_more(name: str):
+    if not hasattr(ui, name):
+        name = name.replace('_', '')  # NOTE: "AG Grid" leads to anchor name "ag_grid", but class is `ui.aggrid`
+    module = importlib.import_module(f'website.more_documentation.{name}_documentation')
+    api = getattr(ui, name)
+    more = getattr(module, 'more', None)
+    back_link_target = str(api.__doc__ or api.__init__.__doc__).splitlines()[0].strip()
+
     add_head_html()
     add_header()
     with side_menu() as menu:
-        ui.markdown(f'[← back](/documentation#{create_anchor_name(name)})').classes('bold-links')
+        ui.markdown(f'[← back](/documentation#{create_anchor_name(back_link_target)})').classes('bold-links')
     with ui.column().classes('w-full p-8 lg:p-16 max-w-[1250px] mx-auto'):
-        if not hasattr(ui, name):
-            name = name.replace('_', '')  # NOTE: "AG Grid" leads to anchor name "ag_grid", but class is `ui.aggrid`
         section_heading('Documentation', f'ui.*{name}*')
-        module = importlib.import_module(f'website.more_documentation.{name}_documentation')
-        element_class = getattr(ui, name)
-        more = getattr(module, 'more', None)
         with menu:
             ui.markdown('**Demos**' if more else '**Demo**').classes('mt-4')
-        element_demo(element_class)(getattr(module, 'main_demo'))
+        element_demo(api)(getattr(module, 'main_demo'))
         if more:
             more()
-        if inspect.isclass(element_class):
+        if inspect.isclass(api):
             with menu:
                 ui.markdown('**Reference**').classes('mt-4')
             ui.markdown('## Reference').classes('mt-16')
-            generate_class_doc(element_class)
+            generate_class_doc(api)
 
 
 ui.run(uvicorn_reload_includes='*.py, *.css, *.html')

+ 13 - 8
nicegui/element.py

@@ -93,6 +93,16 @@ class Element(Visibility):
             'events': [listener.to_dict() for listener in self._event_listeners.values()],
         }
 
+    @staticmethod
+    def _update_classes_list(
+            classes: List[str],
+            add: Optional[str] = None, remove: Optional[str] = None, replace: Optional[str] = None) -> List[str]:
+        class_list = classes 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()
+        return list(dict.fromkeys(class_list))  # NOTE: remove duplicates while preserving order
+
     def classes(self, add: Optional[str] = None, *, remove: Optional[str] = None, replace: Optional[str] = None) \
             -> Self:
         """Apply, remove, or replace HTML classes.
@@ -105,11 +115,7 @@ class Element(Visibility):
         :param remove: whitespace-delimited string of classes to remove from the element
         :param replace: whitespace-delimited string of classes to use instead of existing ones
         """
-        class_list = self._classes 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()
-        new_classes = list(dict.fromkeys(class_list))  # NOTE: remove duplicates while preserving order
+        new_classes = self._update_classes_list(self._classes, add, remove, replace)
         if self._classes != new_classes:
             self._classes = new_classes
             self.update()
@@ -133,11 +139,10 @@ class Element(Visibility):
         :param add: semicolon-separated list of styles to add to the element
         :param remove: semicolon-separated list of styles to remove from the element
         :param replace: semicolon-separated list of styles to use instead of existing ones
-         """
+        """
         style_dict = deepcopy(self._style) if replace is None else {}
         for key in self._parse_style(remove):
-            if key in style_dict:
-                del style_dict[key]
+            style_dict.pop(key, None)
         style_dict.update(self._parse_style(add))
         style_dict.update(self._parse_style(replace))
         if self._style != style_dict:

+ 37 - 0
nicegui/elements/query.js

@@ -0,0 +1,37 @@
+export default {
+  mounted() {
+    this.add_classes(this.classes);
+    this.add_style(this.style);
+    this.add_props(this.props);
+  },
+  methods: {
+    add_classes(classes) {
+      document.querySelectorAll(this.selector).forEach((e) => e.classList.add(...classes));
+    },
+    remove_classes(classes) {
+      document.querySelectorAll(this.selector).forEach((e) => e.classList.remove(...classes));
+    },
+    add_style(style) {
+      Object.entries(style).forEach(([key, val]) =>
+        document.querySelectorAll(this.selector).forEach((e) => (e.style[key] = val))
+      );
+    },
+    remove_style(keys) {
+      keys.forEach((key) => document.querySelectorAll(this.selector).forEach((e) => e.style.removeProperty(key)));
+    },
+    add_props(props) {
+      Object.entries(props).forEach(([key, val]) =>
+        document.querySelectorAll(this.selector).forEach((e) => e.setAttribute(key, val))
+      );
+    },
+    remove_props(keys) {
+      keys.forEach((key) => document.querySelectorAll(this.selector).forEach((e) => e.removeAttribute(key)));
+    },
+  },
+  props: {
+    selector: String,
+    classes: Array,
+    style: Object,
+    props: Object,
+  },
+};

+ 69 - 0
nicegui/elements/query.py

@@ -0,0 +1,69 @@
+from typing import Optional
+
+from typing_extensions import Self
+
+from ..dependencies import register_component
+from ..element import Element
+from ..globals import get_client
+
+register_component('query', __file__, 'query.js')
+
+
+class Query(Element):
+
+    def __init__(self, selector: str) -> None:
+        super().__init__('query')
+        self._props['selector'] = selector
+        self._props['classes'] = []
+        self._props['style'] = {}
+        self._props['props'] = {}
+
+    def classes(self, add: Optional[str] = None, *, remove: Optional[str] = None, replace: Optional[str] = None) \
+            -> Self:
+        classes = self._update_classes_list(self._props['classes'], add, remove, replace)
+        new_classes = [c for c in classes if c not in self._props['classes']]
+        old_classes = [c for c in self._props['classes'] if c not in classes]
+        if new_classes:
+            self.run_method('add_classes', new_classes)
+        if old_classes:
+            self.run_method('remove_classes', old_classes)
+        self._props['classes'] = classes
+        return self
+
+    def style(self, add: Optional[str] = None, *, remove: Optional[str] = None, replace: Optional[str] = None) \
+            -> Self:
+        old_style = Element._parse_style(remove)
+        for key in old_style:
+            self._props['style'].pop(key, None)
+        if old_style:
+            self.run_method('remove_style', list(old_style))
+        self._props['style'].update(Element._parse_style(add))
+        self._props['style'].update(Element._parse_style(replace))
+        if self._props['style']:
+            self.run_method('add_style', self._props['style'])
+        return self
+
+    def props(self, add: Optional[str] = None, *, remove: Optional[str] = None) -> Self:
+        old_props = self._parse_props(remove)
+        for key in old_props:
+            self._props['props'].pop(key, None)
+        if old_props:
+            self.run_method('remove_props', list(old_props))
+        new_props = self._parse_props(add)
+        self._props['props'].update(new_props)
+        if self._props['props']:
+            self.run_method('add_props', self._props['props'])
+        return self
+
+
+def query(selector: str) -> Query:
+    """Query Selector
+
+    To manipulate elements like the document body, you can use the `ui.query` function.
+    With the query result you can add classes, styles, and attributes like with every other UI element.
+    This can be useful for example to change the background color of the page (e.g. `ui.query('body').classes('bg-green')`).
+    """
+    for element in get_client().elements.values():
+        if isinstance(element, Query) and element._props['selector'] == selector:
+            return element
+    return Query(selector)

+ 1 - 0
nicegui/ui.py

@@ -39,6 +39,7 @@ from .elements.number import Number as number
 from .elements.plotly import Plotly as plotly
 from .elements.progress import CircularProgress as circular_progress
 from .elements.progress import LinearProgress as linear_progress
+from .elements.query import query
 from .elements.radio import Radio as radio
 from .elements.row import Row as row
 from .elements.scene import Scene as scene

+ 54 - 0
tests/test_query.py

@@ -0,0 +1,54 @@
+from nicegui import ui
+
+from .screen import Screen
+
+
+def test_query_body(screen: Screen):
+    ui.label('Hello')
+    ui.query('body').classes('bg-orange-100')
+    ui.button('Red background', on_click=lambda: ui.query('body').classes(replace='bg-red-100'))
+    ui.button('Blue background', on_click=lambda: ui.query('body').classes(replace='bg-blue-100'))
+    ui.button('Small padding', on_click=lambda: ui.query('body').style('padding: 1px'))
+    ui.button('Large padding', on_click=lambda: ui.query('body').style('padding: 10px'))
+    ui.button('Data X = 1', on_click=lambda: ui.query('body').props('data-x=1'))
+    ui.button('Data X = 2', on_click=lambda: ui.query('body').props('data-x=2'))
+
+    screen.open('/')
+    screen.should_contain('Hello')
+    assert screen.find_by_tag('body').get_attribute('class') == 'desktop no-touch body--light bg-orange-100'
+
+    screen.click('Red background')
+    screen.wait(0.5)
+    assert screen.find_by_tag('body').get_attribute('class') == 'desktop no-touch body--light bg-red-100'
+
+    screen.click('Blue background')
+    screen.wait(0.5)
+    assert screen.find_by_tag('body').get_attribute('class') == 'desktop no-touch body--light bg-blue-100'
+
+    screen.click('Small padding')
+    screen.wait(0.5)
+    assert screen.find_by_tag('body').value_of_css_property('padding') == '1px'
+
+    screen.click('Large padding')
+    screen.wait(0.5)
+    assert screen.find_by_tag('body').value_of_css_property('padding') == '10px'
+
+    screen.click('Data X = 1')
+    screen.wait(0.5)
+    assert screen.find_by_tag('body').get_attribute('data-x') == '1'
+
+    screen.click('Data X = 2')
+    screen.wait(0.5)
+    assert screen.find_by_tag('body').get_attribute('data-x') == '2'
+
+
+def test_query_multiple_divs(screen: Screen):
+    ui.label('A')
+    ui.label('B')
+    ui.button('Add border', on_click=lambda: ui.query('div').style('border: 1px solid black'))
+
+    screen.open('/')
+    screen.click('Add border')
+    screen.wait(0.5)
+    assert screen.find('A').value_of_css_property('border') == '1px solid rgb(0, 0, 0)'
+    assert screen.find('B').value_of_css_property('border') == '1px solid rgb(0, 0, 0)'

+ 1 - 0
website/documentation.py

@@ -256,6 +256,7 @@ def create_full() -> None:
         red_style.apply(label_c)
         ui.label('Label D').tailwind(red_style)
 
+    load_demo(ui.query)
     load_demo(ui.colors)
 
     heading('Action')

+ 3 - 3
website/documentation_tools.py

@@ -98,13 +98,13 @@ class element_demo:
             return demo(browser_title=self.browser_title)(f)
 
 
-def load_demo(element_class: type) -> None:
-    name = pascal_to_snake(element_class.__name__)
+def load_demo(api: Union[type, Callable]) -> None:
+    name = pascal_to_snake(api.__name__)
     try:
         module = importlib.import_module(f'website.more_documentation.{name}_documentation')
     except ModuleNotFoundError:
         module = importlib.import_module(f'website.more_documentation.{name.replace("_", "")}_documentation')
-    element_demo(element_class)(getattr(module, 'main_demo'), more_link=name)
+    element_demo(api)(getattr(module, 'main_demo'), more_link=name)
 
 
 def is_method_or_property(cls: type, attribute_name: str) -> bool:

+ 12 - 0
website/more_documentation/query_documentation.py

@@ -0,0 +1,12 @@
+from nicegui import ui
+
+
+def main_demo() -> None:
+    def set_background(color: str) -> None:
+        ui.query('body').style(f'background-color: {color}')
+
+    # ui.button('Blue', on_click=lambda: set_background(#ddeeff'))
+    # ui.button('Orange', on_click=lambda: set_background(#ffeedd'))
+    # END OF DEMO
+    ui.button('Blue', on_click=lambda e: e.sender.parent_slot.parent.style('background-color: #ddeeff'))
+    ui.button('Orange', on_click=lambda e: e.sender.parent_slot.parent.style('background-color: #ffeedd'))