浏览代码

Merge pull request #1134 from zauberzeug/path_improvements

using hierarchical path names to identify JS dependencies
Rodja Trappe 1 年之前
父节点
当前提交
7a695193b2
共有 42 个文件被更改,包括 426 次插入378 次删除
  1. 2 7
      examples/custom_vue_component/counter.py
  2. 1 1
      examples/custom_vue_component/main.py
  3. 2 8
      examples/map/leaflet.py
  4. 4 6
      examples/single_page_app/router.py
  5. 5 5
      nicegui/client.py
  6. 130 64
      nicegui/dependencies.py
  7. 55 16
      nicegui/element.py
  8. 2 9
      nicegui/elements/aggrid.py
  9. 2 6
      nicegui/elements/audio.py
  10. 6 18
      nicegui/elements/chart.py
  11. 2 7
      nicegui/elements/chat_message.py
  12. 1 1
      nicegui/elements/choice_element.py
  13. 2 8
      nicegui/elements/colors.py
  14. 2 7
      nicegui/elements/dark_mode.py
  15. 2 7
      nicegui/elements/image.py
  16. 2 8
      nicegui/elements/input.py
  17. 2 6
      nicegui/elements/interactive_image.py
  18. 2 8
      nicegui/elements/joystick.py
  19. 2 7
      nicegui/elements/keyboard.py
  20. 2 7
      nicegui/elements/link.py
  21. 2 7
      nicegui/elements/log.py
  22. 0 6
      nicegui/elements/markdown.js
  23. 4 8
      nicegui/elements/markdown.py
  24. 16 1
      nicegui/elements/mermaid.js
  25. 5 10
      nicegui/elements/mermaid.py
  26. 2 8
      nicegui/elements/plotly.py
  27. 2 7
      nicegui/elements/query.py
  28. 11 19
      nicegui/elements/scene.py
  29. 2 7
      nicegui/elements/select.py
  30. 2 8
      nicegui/elements/table.py
  31. 1 1
      nicegui/elements/textarea.py
  32. 2 7
      nicegui/elements/upload.py
  33. 2 6
      nicegui/elements/video.py
  34. 5 7
      nicegui/functions/refreshable.py
  35. 14 15
      nicegui/nicegui.py
  36. 68 42
      nicegui/templates/index.html
  37. 18 0
      tests/test_aggrid.py
  38. 15 6
      tests/test_chart.py
  39. 9 0
      tests/test_mermaid.py
  40. 8 0
      tests/test_plotly.py
  41. 8 0
      tests/test_scene.py
  42. 2 7
      website/intersection_observer.py

+ 2 - 7
examples/custom_vue_component/counter.py

@@ -1,19 +1,14 @@
-from pathlib import Path
 from typing import Callable, Optional
 from typing import Callable, Optional
 
 
-from nicegui.dependencies import register_vue_component
 from nicegui.element import Element
 from nicegui.element import Element
 
 
-register_vue_component('counter', Path(__file__).parent / 'counter.js')
 
 
-
-class Counter(Element):
+class Counter(Element, component='counter.js'):
 
 
     def __init__(self, title: str, *, on_change: Optional[Callable] = None) -> None:
     def __init__(self, title: str, *, on_change: Optional[Callable] = None) -> None:
-        super().__init__('counter')
+        super().__init__()
         self._props['title'] = title
         self._props['title'] = title
         self.on('change', on_change)
         self.on('change', on_change)
-        self.use_component('counter')
 
 
     def reset(self) -> None:
     def reset(self) -> None:
         self.run_method('reset')
         self.run_method('reset')

+ 1 - 1
examples/custom_vue_component/main.py

@@ -14,4 +14,4 @@ with ui.card():
 
 
 ui.button('Reset', on_click=counter.reset).props('small outline')
 ui.button('Reset', on_click=counter.reset).props('small outline')
 
 
-ui.run(port=1234)
+ui.run()

+ 2 - 8
examples/map/leaflet.py

@@ -1,18 +1,12 @@
-from pathlib import Path
 from typing import Tuple
 from typing import Tuple
 
 
 from nicegui import ui
 from nicegui import ui
-from nicegui.dependencies import register_vue_component
-from nicegui.element import Element
 
 
-register_vue_component('leaflet', Path(__file__).parent / 'leaflet.js')
 
 
-
-class leaflet(Element):
+class leaflet(ui.element, component='leaflet.js'):
 
 
     def __init__(self) -> None:
     def __init__(self) -> None:
-        super().__init__('leaflet')
-        self.use_component('leaflet')
+        super().__init__()
         ui.add_head_html('<link href="https://unpkg.com/leaflet@1.6.0/dist/leaflet.css" rel="stylesheet"/>')
         ui.add_head_html('<link href="https://unpkg.com/leaflet@1.6.0/dist/leaflet.css" rel="stylesheet"/>')
         ui.add_head_html('<script src="https://unpkg.com/leaflet@1.6.0/dist/leaflet.js"></script>')
         ui.add_head_html('<script src="https://unpkg.com/leaflet@1.6.0/dist/leaflet.js"></script>')
 
 

+ 4 - 6
examples/single_page_app/router.py

@@ -1,10 +1,10 @@
-from pathlib import Path
 from typing import Awaitable, Callable, Dict, Union
 from typing import Awaitable, Callable, Dict, Union
 
 
 from nicegui import background_tasks, ui
 from nicegui import background_tasks, ui
-from nicegui.dependencies import register_vue_component
 
 
-register_vue_component('router_frame', Path(__file__).parent / 'router_frame.js')
+
+class RouterFrame(ui.element, component='router_frame.js'):
+    pass
 
 
 
 
 class Router():
 class Router():
@@ -41,7 +41,5 @@ class Router():
         background_tasks.create(build())
         background_tasks.create(build())
 
 
     def frame(self) -> ui.element:
     def frame(self) -> ui.element:
-        self.content = ui.element('router_frame') \
-            .on('open', lambda e: self.open(e.args)) \
-            .use_component('router_frame')
+        self.content = RouterFrame().on('open', lambda e: self.open(e.args))
         return self.content
         return self.content

+ 5 - 5
nicegui/client.py

@@ -70,17 +70,17 @@ class Client:
     def build_response(self, request: Request, status_code: int = 200) -> Response:
     def build_response(self, request: Request, status_code: int = 200) -> Response:
         prefix = request.headers.get('X-Forwarded-Prefix', request.scope.get('root_path', ''))
         prefix = request.headers.get('X-Forwarded-Prefix', request.scope.get('root_path', ''))
         elements = json.dumps({id: element._to_dict() for id, element in self.elements.items()})
         elements = json.dumps({id: element._to_dict() for id, element in self.elements.items()})
-        vue_html, vue_styles, vue_scripts, import_maps, js_imports = generate_resources(prefix, self.elements.values())
+        vue_html, vue_styles, vue_scripts, imports, js_imports = generate_resources(prefix, self.elements.values())
         return templates.TemplateResponse('index.html', {
         return templates.TemplateResponse('index.html', {
             'request': request,
             'request': request,
             'version': __version__,
             'version': __version__,
             'client_id': str(self.id),
             'client_id': str(self.id),
             'elements': elements,
             'elements': elements,
             'head_html': self.head_html,
             'head_html': self.head_html,
-            'body_html': f'{vue_styles}\n{self.body_html}\n{vue_html}',
-            'vue_scripts': vue_scripts,
-            'import_maps': import_maps,
-            'js_imports': js_imports,
+            'body_html': '<style>' + '\n'.join(vue_styles) + '</style>\n' + self.body_html + '\n' + '\n'.join(vue_html),
+            'vue_scripts': '\n'.join(vue_scripts),
+            'imports': json.dumps(imports),
+            'js_imports': '\n'.join(js_imports),
             'title': self.page.resolve_title(),
             'title': self.page.resolve_title(),
             'viewport': self.page.resolve_viewport(),
             'viewport': self.page.resolve_viewport(),
             'favicon_url': get_favicon_url(self.page, prefix),
             'favicon_url': get_favicon_url(self.page, prefix),

+ 130 - 64
nicegui/dependencies.py

@@ -1,85 +1,151 @@
-import json
+from __future__ import annotations
+
+import hashlib
+from dataclasses import dataclass
 from pathlib import Path
 from pathlib import Path
-from typing import Any, Dict, List, Set, Tuple
+from typing import TYPE_CHECKING, Dict, List, Set, Tuple
 
 
 import vbuild
 import vbuild
 
 
 from . import __version__
 from . import __version__
-from .element import Element
+from .helpers import KWONLY_SLOTS
+
+if TYPE_CHECKING:
+    from .element import Element
+
+
+@dataclass(**KWONLY_SLOTS)
+class Component:
+    key: str
+    name: str
+    path: Path
+
+    @property
+    def tag(self) -> str:
+        return f'nicegui-{self.name}'
+
+
+@dataclass(**KWONLY_SLOTS)
+class VueComponent(Component):
+    html: str
+    script: str
+    style: str
+
+
+@dataclass(**KWONLY_SLOTS)
+class JsComponent(Component):
+    pass
+
+
+@dataclass(**KWONLY_SLOTS)
+class Library:
+    key: str
+    name: str
+    path: Path
+    expose: bool
 
 
-vue_components: Dict[str, Any] = {}
-js_components: Dict[str, Any] = {}
-libraries: Dict[str, Any] = {}
 
 
+vue_components: Dict[str, VueComponent] = {}
+js_components: Dict[str, JsComponent] = {}
+libraries: Dict[str, Library] = {}
 
 
-def register_vue_component(name: str, path: Path) -> None:
+
+def register_vue_component(path: Path) -> Component:
     """Register a .vue or .js Vue component.
     """Register a .vue or .js Vue component.
 
 
-    :param name: unique machine-name (used in element's `use_library`): no space, no special characters
-    :param path: local path
+    Single-file components (.vue) are built right away
+    to delegate this "long" process to the bootstrap phase
+    and to avoid building the component on every single request.
     """
     """
-    suffix = path.suffix.lower()
-    assert suffix in {'.vue', '.js', '.mjs'}, 'Only VUE and JS components are supported.'
-    if suffix == '.vue':
-        assert name not in vue_components, f'Duplicate VUE component name {name}'
-        # The component (in case of .vue) is built right away to:
-        # 1. delegate this "long" process to the bootstrap phase
-        # 2. avoid building the component on every single request
-        vue_components[name] = vbuild.VBuild(name, path.read_text())
-    elif suffix == '.js':
-        assert name not in js_components, f'Duplicate JS component name {name}'
-        js_components[name] = {'name': name, 'path': path}
-
-
-def register_library(name: str, path: Path, *, expose: bool = False) -> None:
-    """Register a new external library.
-
-    :param name: unique machine-name (used in element's `use_library`): no space, no special characters
-    :param path: local path
-    :param expose: if True, this will be exposed as an ESM module but NOT imported
+    key = compute_key(path)
+    name = get_name(path)
+    if path.suffix == '.vue':
+        if key in vue_components and vue_components[key].path == path:
+            return vue_components[key]
+        assert key not in vue_components, f'Duplicate VUE component {key}'
+        v = vbuild.VBuild(name, path.read_text())
+        vue_components[key] = VueComponent(key=key, name=name, path=path, html=v.html, script=v.script, style=v.style)
+        return vue_components[key]
+    if path.suffix == '.js':
+        if key in js_components and js_components[key].path == path:
+            return js_components[key]
+        assert key not in js_components, f'Duplicate JS component {key}'
+        js_components[key] = JsComponent(key=key, name=name, path=path)
+        return js_components[key]
+    raise ValueError(f'Unsupported component type "{path.suffix}"')
+
+
+def register_library(path: Path, *, expose: bool = False) -> Library:
+    """Register a *.js library."""
+    key = compute_key(path)
+    name = get_name(path)
+    if path.suffix in {'.js', '.mjs'}:
+        if key in libraries and libraries[key].path == path:
+            return libraries[key]
+        assert key not in libraries, f'Duplicate js library {key}'
+        libraries[key] = Library(key=key, name=name, path=path, expose=expose)
+        return libraries[key]
+    raise ValueError(f'Unsupported library type "{path.suffix}"')
+
+
+def compute_key(path: Path) -> str:
+    """Compute a key for a given path using a hash function.
+
+    If the path is relative to the NiceGUI base directory, the key is computed from the relative path.
     """
     """
-    assert path.suffix == '.js' or path.suffix == '.mjs', 'Only JS dependencies are supported.'
-    libraries[name] = {'name': name, 'path': path, 'expose': expose}
+    nicegui_base = Path(__file__).parent
+    try:
+        path = path.relative_to(nicegui_base)
+    except ValueError:
+        pass
+    return f'{hashlib.sha256(str(path.parent).encode()).hexdigest()}/{path.name}'
+
+
+def get_name(path: Path) -> str:
+    return path.name.split('.', 1)[0]
 
 
 
 
-def generate_resources(prefix: str, elements: List[Element]) -> Tuple[str, str, str, str, str]:
+def generate_resources(prefix: str, elements: List[Element]) -> Tuple[List[str],
+                                                                      List[str],
+                                                                      List[str],
+                                                                      Dict[str, str],
+                                                                      List[str]]:
     done_libraries: Set[str] = set()
     done_libraries: Set[str] = set()
     done_components: Set[str] = set()
     done_components: Set[str] = set()
-    vue_scripts = ''
-    vue_html = ''
-    vue_styles = ''
-    js_imports = ''
-    import_maps = {'imports': {}}
-
-    # Build the importmap structure for exposed libraries.
-    for key in libraries:
-        if key not in done_libraries and libraries[key]['expose']:
-            name = libraries[key]['name']
-            import_maps['imports'][name] = f'{prefix}/_nicegui/{__version__}/library/{key}/include'
+    vue_scripts: List[str] = []
+    vue_html: List[str] = []
+    vue_styles: List[str] = []
+    js_imports: List[str] = []
+    imports: Dict[str, str] = {}
+
+    # build the importmap structure for exposed libraries
+    for key, library in libraries.items():
+        if key not in done_libraries and library.expose:
+            imports[library.name] = f'{prefix}/_nicegui/{__version__}/libraries/{key}'
             done_libraries.add(key)
             done_libraries.add(key)
-    # Build the none optimized component (ie, the vue component).
-    for key in vue_components:
+
+    # build the none-optimized component (i.e. the Vue component)
+    for key, component in vue_components.items():
         if key not in done_components:
         if key not in done_components:
-            vue_html += f'{vue_components[key].html}\n'
-            vue_scripts += f'{vue_components[key].script.replace("Vue.component", "app.component", 1)}\n'
-            vue_styles += f'{vue_components[key].style}\n'
+            vue_html.append(component.html)
+            vue_scripts.append(component.script.replace(f"Vue.component('{component.name}',",
+                                                        f"app.component('{component.tag}',", 1))
+            vue_styles.append(component.style)
             done_components.add(key)
             done_components.add(key)
 
 
-    # Build the resources associated with the elements.
+    # build the resources associated with the elements
     for element in elements:
     for element in elements:
-        for key in element.libraries:
-            if key in libraries and key not in done_libraries:
-                if not libraries[key]['expose']:
-                    js_imports += f'import "{prefix}/_nicegui/{__version__}/library/{key}/include";\n'
-                done_libraries.add(key)
-        for key in element.components:
-            if key in js_components and key not in done_components:
-                name = js_components[key]['name']
-                var = key.replace('-', '_')
-                js_imports += f'import {{ default as {var} }} from "{prefix}/_nicegui/{__version__}/components/{key}";\n'
-                js_imports += f'app.component("{name}", {var});\n'
-                done_components.add(key)
-
-    vue_styles = f'<style>{vue_styles}</style>'
-    import_maps = f'<script type="importmap">{json.dumps(import_maps)}</script>'
-    return vue_html, vue_styles, vue_scripts, import_maps, js_imports
+        for library in element.libraries:
+            if library.key not in done_libraries:
+                if not library.expose:
+                    url = f'{prefix}/_nicegui/{__version__}/libraries/{library.key}'
+                    js_imports.append(f'import "{url}";')
+                done_libraries.add(library.key)
+        if element.component:
+            component = element.component
+            if component.key not in done_components and component.path.suffix.lower() == '.js':
+                url = f'{prefix}/_nicegui/{__version__}/components/{component.key}'
+                js_imports.append(f'import {{ default as {component.name} }} from "{url}";')
+                js_imports.append(f'app.component("{component.tag}", {component.name});')
+                done_components.add(component.key)
+    return vue_html, vue_styles, vue_scripts, imports, js_imports

+ 55 - 16
nicegui/element.py

@@ -1,7 +1,9 @@
 from __future__ import annotations
 from __future__ import annotations
 
 
+import inspect
 import re
 import re
 from copy import deepcopy
 from copy import deepcopy
+from pathlib import Path
 from typing import TYPE_CHECKING, Any, Callable, Dict, Iterator, List, Optional, Union
 from typing import TYPE_CHECKING, Any, Callable, Dict, Iterator, List, Optional, Union
 
 
 from typing_extensions import Self
 from typing_extensions import Self
@@ -9,6 +11,7 @@ from typing_extensions import Self
 from nicegui import json
 from nicegui import json
 
 
 from . import binding, events, globals, outbox, storage
 from . import binding, events, globals, outbox, storage
+from .dependencies import JsComponent, Library, register_library, register_vue_component
 from .elements.mixins.visibility import Visibility
 from .elements.mixins.visibility import Visibility
 from .event_listener import EventListener
 from .event_listener import EventListener
 from .slot import Slot
 from .slot import Slot
@@ -21,8 +24,12 @@ PROPS_PATTERN = re.compile(r'([:\w\-]+)(?:=(?:("[^"\\]*(?:\\.[^"\\]*)*")|([\w\-.
 
 
 
 
 class Element(Visibility):
 class Element(Visibility):
+    component: Optional[JsComponent] = None
+    libraries: List[Library] = []
+    extra_libraries: List[Library] = []
+    exposed_libraries: List[Library] = []
 
 
-    def __init__(self, tag: str, *, _client: Optional[Client] = None) -> None:
+    def __init__(self, tag: Optional[str] = None, *, _client: Optional[Client] = None) -> None:
         """Generic Element
         """Generic Element
 
 
         This class is the base class for all other UI elements.
         This class is the base class for all other UI elements.
@@ -35,14 +42,12 @@ class Element(Visibility):
         self.client = _client or globals.get_client()
         self.client = _client or globals.get_client()
         self.id = self.client.next_element_id
         self.id = self.client.next_element_id
         self.client.next_element_id += 1
         self.client.next_element_id += 1
-        self.tag = tag
+        self.tag = tag if tag else self.component.tag if self.component else 'div'
         self._classes: List[str] = []
         self._classes: List[str] = []
         self._style: Dict[str, str] = {}
         self._style: Dict[str, str] = {}
         self._props: Dict[str, Any] = {'key': self.id}  # HACK: workaround for #600 and #898
         self._props: Dict[str, Any] = {'key': self.id}  # HACK: workaround for #600 and #898
         self._event_listeners: Dict[str, EventListener] = {}
         self._event_listeners: Dict[str, EventListener] = {}
         self._text: Optional[str] = None
         self._text: Optional[str] = None
-        self.components: List[str] = []
-        self.libraries: List[str] = []
         self.slots: Dict[str, Slot] = {}
         self.slots: Dict[str, Slot] = {}
         self.default_slot = self.add_slot('default')
         self.default_slot = self.add_slot('default')
 
 
@@ -59,6 +64,41 @@ class Element(Visibility):
         if self.parent_slot:
         if self.parent_slot:
             outbox.enqueue_update(self.parent_slot.parent)
             outbox.enqueue_update(self.parent_slot.parent)
 
 
+    def __init_subclass__(cls, *,
+                          component: Union[str, Path, None] = None,
+                          libraries: List[Union[str, Path]] = [],
+                          exposed_libraries: List[Union[str, Path]] = [],
+                          extra_libraries: List[Union[str, Path]] = [],
+                          ) -> None:
+        super().__init_subclass__()
+        base = Path(inspect.getfile(cls)).parent
+
+        def glob_absolute_paths(file: Union[str, Path]) -> List[Path]:
+            path = Path(file)
+            if not path.is_absolute():
+                path = base / path
+            return sorted(path.parent.glob(path.name), key=lambda p: p.stem)
+
+        cls.component = None
+        if component:
+            for path in glob_absolute_paths(component):
+                cls.component = register_vue_component(path)
+
+        cls.libraries = []
+        for library in libraries:
+            for path in glob_absolute_paths(library):
+                cls.libraries.append(register_library(path))
+
+        cls.extra_libraries = []
+        for library in extra_libraries:
+            for path in glob_absolute_paths(library):
+                cls.extra_libraries.append(register_library(path))
+
+        cls.exposed_libraries = []
+        for library in exposed_libraries:
+            for path in glob_absolute_paths(library):
+                cls.exposed_libraries.append(register_library(path, expose=True))
+
     def add_slot(self, name: str, template: Optional[str] = None) -> Slot:
     def add_slot(self, name: str, template: Optional[str] = None) -> Slot:
         """Add a slot to the element.
         """Add a slot to the element.
 
 
@@ -97,8 +137,17 @@ class Element(Visibility):
             'text': self._text,
             'text': self._text,
             'slots': self._collect_slot_dict(),
             'slots': self._collect_slot_dict(),
             'events': [listener.to_dict() for listener in self._event_listeners.values()],
             'events': [listener.to_dict() for listener in self._event_listeners.values()],
-            'libraries': self.libraries,
-            'components': self.components,
+            'component': {
+                'key': self.component.key,
+                'name': self.component.name,
+                'tag': self.component.tag
+            } if self.component else None,
+            'libraries': [
+                {
+                    'key': library.key,
+                    'name': library.name,
+                } for library in self.libraries
+            ],
         }
         }
 
 
     @staticmethod
     @staticmethod
@@ -306,13 +355,3 @@ class Element(Visibility):
 
 
         Can be overridden to perform cleanup.
         Can be overridden to perform cleanup.
         """
         """
-
-    def use_component(self, name: str) -> Self:
-        """Register a ``*.js`` Vue component to be used by this element."""
-        self.components.append(name)
-        return self
-
-    def use_library(self, name: str) -> Self:
-        """Register a JavaScript library to be used by this element."""
-        self.libraries.append(name)
-        return self

+ 2 - 9
nicegui/elements/aggrid.py

@@ -1,17 +1,12 @@
 from __future__ import annotations
 from __future__ import annotations
 
 
-from pathlib import Path
 from typing import Dict, List, Optional, cast
 from typing import Dict, List, Optional, cast
 
 
-from ..dependencies import register_library, register_vue_component
 from ..element import Element
 from ..element import Element
 from ..functions.javascript import run_javascript
 from ..functions.javascript import run_javascript
 
 
-register_vue_component('aggrid', Path(__file__).parent / 'aggrid.js')
-register_library('aggrid', Path(__file__).parent / 'lib' / 'aggrid' / 'ag-grid-community.min.js')
 
 
-
-class AgGrid(Element):
+class AgGrid(Element, component='aggrid.js', libraries=['lib/aggrid/ag-grid-community.min.js']):
 
 
     def __init__(self, options: Dict, *, html_columns: List[int] = [], theme: str = 'balham') -> None:
     def __init__(self, options: Dict, *, html_columns: List[int] = [], theme: str = 'balham') -> None:
         """AG Grid
         """AG Grid
@@ -24,12 +19,10 @@ class AgGrid(Element):
         :param html_columns: list of columns that should be rendered as HTML (default: `[]`)
         :param html_columns: list of columns that should be rendered as HTML (default: `[]`)
         :param theme: AG Grid theme (default: 'balham')
         :param theme: AG Grid theme (default: 'balham')
         """
         """
-        super().__init__('aggrid')
+        super().__init__()
         self._props['options'] = options
         self._props['options'] = options
         self._props['html_columns'] = html_columns
         self._props['html_columns'] = html_columns
         self._classes = ['nicegui-aggrid', f'ag-theme-{theme}']
         self._classes = ['nicegui-aggrid', f'ag-theme-{theme}']
-        self.use_component('aggrid')
-        self.use_library('aggrid')
 
 
     @staticmethod
     @staticmethod
     def from_pandas(df: 'pandas.DataFrame', *, theme: str = 'balham') -> AgGrid:
     def from_pandas(df: 'pandas.DataFrame', *, theme: str = 'balham') -> AgGrid:

+ 2 - 6
nicegui/elements/audio.py

@@ -3,13 +3,10 @@ from pathlib import Path
 from typing import Union
 from typing import Union
 
 
 from .. import globals
 from .. import globals
-from ..dependencies import register_vue_component
 from ..element import Element
 from ..element import Element
 
 
-register_vue_component('audio', Path(__file__).parent / 'audio.js')
 
 
-
-class Audio(Element):
+class Audio(Element, component='audio.js'):
 
 
     def __init__(self, src: Union[str, Path], *,
     def __init__(self, src: Union[str, Path], *,
                  controls: bool = True,
                  controls: bool = True,
@@ -29,7 +26,7 @@ class Audio(Element):
         See `here <https://developer.mozilla.org/en-US/docs/Web/HTML/Element/audio#events>`_
         See `here <https://developer.mozilla.org/en-US/docs/Web/HTML/Element/audio#events>`_
         for a list of events you can subscribe to using the generic event subscription `on()`.
         for a list of events you can subscribe to using the generic event subscription `on()`.
         """
         """
-        super().__init__('audio')
+        super().__init__()
         if Path(src).is_file():
         if Path(src).is_file():
             src = globals.app.add_media_file(local_file=src)
             src = globals.app.add_media_file(local_file=src)
         self._props['src'] = src
         self._props['src'] = src
@@ -37,7 +34,6 @@ class Audio(Element):
         self._props['autoplay'] = autoplay
         self._props['autoplay'] = autoplay
         self._props['muted'] = muted
         self._props['muted'] = muted
         self._props['loop'] = loop
         self._props['loop'] = loop
-        self.use_component('audio')
 
 
         if type:
         if type:
             url = f'https://github.com/zauberzeug/nicegui/pull/624'
             url = f'https://github.com/zauberzeug/nicegui/pull/624'

+ 6 - 18
nicegui/elements/chart.py

@@ -1,20 +1,12 @@
-from pathlib import Path
 from typing import Dict, List
 from typing import Dict, List
 
 
-from ..dependencies import register_library, register_vue_component
 from ..element import Element
 from ..element import Element
 
 
-register_vue_component('chart', Path(__file__).parent / 'chart.js')
 
 
-core_dependencies: List[Path] = []
-for path in sorted((Path(__file__).parent / 'lib' / 'highcharts').glob('*.js'), key=lambda p: p.stem):
-    register_library(path.stem, path)
-    core_dependencies.append(path)
-for path in sorted((Path(__file__).parent / 'lib' / 'highcharts' / 'modules').glob('*.js'), key=lambda p: p.stem):
-    register_library(path.stem, path)
-
-
-class Chart(Element):
+class Chart(Element,
+            component='chart.js',
+            libraries=['lib/highcharts/*.js'],
+            extra_libraries=['lib/highcharts/modules/*.js']):
 
 
     def __init__(self, options: Dict, *, type: str = 'chart', extras: List[str] = []) -> None:
     def __init__(self, options: Dict, *, type: str = 'chart', extras: List[str] = []) -> None:
         """Chart
         """Chart
@@ -30,15 +22,11 @@ class Chart(Element):
         :param type: chart type (e.g. "chart", "stockChart", "mapChart", ...; default: "chart")
         :param type: chart type (e.g. "chart", "stockChart", "mapChart", ...; default: "chart")
         :param extras: list of extra dependencies to include (e.g. "annotations", "arc-diagram", "solid-gauge", ...)
         :param extras: list of extra dependencies to include (e.g. "annotations", "arc-diagram", "solid-gauge", ...)
         """
         """
-        super().__init__('chart')
+        super().__init__()
         self._props['type'] = type
         self._props['type'] = type
         self._props['options'] = options
         self._props['options'] = options
         self._props['extras'] = extras
         self._props['extras'] = extras
-        self.use_component('chart')
-        for dependency in core_dependencies:
-            self.use_library(dependency.stem)
-        for extra in extras:
-            self.use_library(extra)
+        self.libraries.extend(library for library in self.extra_libraries if library.path.stem in extras)
 
 
     @property
     @property
     def options(self) -> Dict:
     def options(self) -> Dict:

+ 2 - 7
nicegui/elements/chat_message.py

@@ -1,14 +1,10 @@
 import html
 import html
-from pathlib import Path
 from typing import List, Optional, Union
 from typing import List, Optional, Union
 
 
-from ..dependencies import register_vue_component
 from ..element import Element
 from ..element import Element
 
 
-register_vue_component('chat_message', Path(__file__).parent / 'chat_message.js')
 
 
-
-class ChatMessage(Element):
+class ChatMessage(Element, component='chat_message.js'):
 
 
     def __init__(self,
     def __init__(self,
                  text: Union[str, List[str]], *,
                  text: Union[str, List[str]], *,
@@ -31,8 +27,7 @@ class ChatMessage(Element):
         :param sent: render as a sent message (so from current user) (default: False)
         :param sent: render as a sent message (so from current user) (default: False)
         :param text_html: render text as HTML (default: False)
         :param text_html: render text as HTML (default: False)
         """
         """
-        super().__init__('chat_message')
-        self.use_component('chat_message')
+        super().__init__()
 
 
         if isinstance(text, str):
         if isinstance(text, str):
             text = [text]
             text = [text]

+ 1 - 1
nicegui/elements/choice_element.py

@@ -6,7 +6,7 @@ from .mixins.value_element import ValueElement
 class ChoiceElement(ValueElement):
 class ChoiceElement(ValueElement):
 
 
     def __init__(self, *,
     def __init__(self, *,
-                 tag: str,
+                 tag: Optional[str] = None,
                  options: Union[List, Dict],
                  options: Union[List, Dict],
                  value: Any,
                  value: Any,
                  on_change: Optional[Callable[..., Any]] = None,
                  on_change: Optional[Callable[..., Any]] = None,

+ 2 - 8
nicegui/elements/colors.py

@@ -1,12 +1,7 @@
-from pathlib import Path
-
-from ..dependencies import register_vue_component
 from ..element import Element
 from ..element import Element
 
 
-register_vue_component('colors', Path(__file__).parent / 'colors.js')
-
 
 
-class Colors(Element):
+class Colors(Element, component='colors.js'):
 
 
     def __init__(self, *,
     def __init__(self, *,
                  primary='#5898d4',
                  primary='#5898d4',
@@ -21,8 +16,7 @@ class Colors(Element):
 
 
         Sets the main colors (primary, secondary, accent, ...) used by `Quasar <https://quasar.dev/>`_.
         Sets the main colors (primary, secondary, accent, ...) used by `Quasar <https://quasar.dev/>`_.
         """
         """
-        super().__init__('colors')
-        self.use_component('colors')
+        super().__init__()
         self._props['primary'] = primary
         self._props['primary'] = primary
         self._props['secondary'] = secondary
         self._props['secondary'] = secondary
         self._props['accent'] = accent
         self._props['accent'] = accent

+ 2 - 7
nicegui/elements/dark_mode.py

@@ -1,13 +1,9 @@
-from pathlib import Path
 from typing import Optional
 from typing import Optional
 
 
-from ..dependencies import register_vue_component
 from .mixins.value_element import ValueElement
 from .mixins.value_element import ValueElement
 
 
-register_vue_component('dark_mode', Path(__file__).parent / 'dark_mode.js')
 
 
-
-class DarkMode(ValueElement):
+class DarkMode(ValueElement, component='dark_mode.js'):
     VALUE_PROP = 'value'
     VALUE_PROP = 'value'
 
 
     def __init__(self, value: Optional[bool] = False) -> None:
     def __init__(self, value: Optional[bool] = False) -> None:
@@ -20,8 +16,7 @@ class DarkMode(ValueElement):
 
 
         :param value: Whether dark mode is enabled. If None, dark mode is set to auto.
         :param value: Whether dark mode is enabled. If None, dark mode is set to auto.
         """
         """
-        super().__init__(tag='dark_mode', value=value, on_value_change=None)
-        self.use_component('dark_mode')
+        super().__init__(value=value, on_value_change=None)
 
 
     def enable(self) -> None:
     def enable(self) -> None:
         """Enable dark mode."""
         """Enable dark mode."""

+ 2 - 7
nicegui/elements/image.py

@@ -1,14 +1,10 @@
 from pathlib import Path
 from pathlib import Path
 from typing import Union
 from typing import Union
 
 
-from nicegui.dependencies import register_vue_component
-
 from .mixins.source_element import SourceElement
 from .mixins.source_element import SourceElement
 
 
-register_vue_component('image', Path(__file__).parent / 'image.js')
-
 
 
-class Image(SourceElement):
+class Image(SourceElement, component='image.js'):
 
 
     def __init__(self, source: Union[str, Path] = '') -> None:
     def __init__(self, source: Union[str, Path] = '') -> None:
         """Image
         """Image
@@ -17,5 +13,4 @@ class Image(SourceElement):
 
 
         :param source: the source of the image; can be a URL, local file path or a base64 string
         :param source: the source of the image; can be a URL, local file path or a base64 string
         """
         """
-        super().__init__(tag='image', source=source)
-        self.use_component('image')
+        super().__init__(source=source)

+ 2 - 8
nicegui/elements/input.py

@@ -1,15 +1,11 @@
-from pathlib import Path
 from typing import Any, Callable, Dict, List, Optional
 from typing import Any, Callable, Dict, List, Optional
 
 
-from ..dependencies import register_vue_component
 from .icon import Icon
 from .icon import Icon
 from .mixins.disableable_element import DisableableElement
 from .mixins.disableable_element import DisableableElement
 from .mixins.validation_element import ValidationElement
 from .mixins.validation_element import ValidationElement
 
 
-register_vue_component('nicegui-input', Path(__file__).parent / 'input.js')
 
 
-
-class Input(ValidationElement, DisableableElement):
+class Input(ValidationElement, DisableableElement, component='input.js'):
     VALUE_PROP: str = 'value'
     VALUE_PROP: str = 'value'
     LOOPBACK = False
     LOOPBACK = False
 
 
@@ -42,7 +38,7 @@ class Input(ValidationElement, DisableableElement):
         :param autocomplete: optional list of strings for autocompletion
         :param autocomplete: optional list of strings for autocompletion
         :param validation: dictionary of validation rules, e.g. ``{'Too long!': lambda value: len(value) < 3}``
         :param validation: dictionary of validation rules, e.g. ``{'Too long!': lambda value: len(value) < 3}``
         """
         """
-        super().__init__(tag='nicegui-input', value=value, on_value_change=on_change, validation=validation)
+        super().__init__(value=value, on_value_change=on_change, validation=validation)
         if label is not None:
         if label is not None:
             self._props['label'] = label
             self._props['label'] = label
         if placeholder is not None:
         if placeholder is not None:
@@ -59,8 +55,6 @@ class Input(ValidationElement, DisableableElement):
 
 
         self._props['autocomplete'] = autocomplete or []
         self._props['autocomplete'] = autocomplete or []
 
 
-        self.use_component('nicegui-input')
-
     def set_autocomplete(self, autocomplete: Optional[List[str]]) -> None:
     def set_autocomplete(self, autocomplete: Optional[List[str]]) -> None:
         """Set the autocomplete list."""
         """Set the autocomplete list."""
         self._props['autocomplete'] = autocomplete
         self._props['autocomplete'] = autocomplete

+ 2 - 6
nicegui/elements/interactive_image.py

@@ -3,15 +3,12 @@ from __future__ import annotations
 from pathlib import Path
 from pathlib import Path
 from typing import Any, Callable, List, Optional, Union
 from typing import Any, Callable, List, Optional, Union
 
 
-from ..dependencies import register_vue_component
 from ..events import GenericEventArguments, MouseEventArguments, handle_event
 from ..events import GenericEventArguments, MouseEventArguments, handle_event
 from .mixins.content_element import ContentElement
 from .mixins.content_element import ContentElement
 from .mixins.source_element import SourceElement
 from .mixins.source_element import SourceElement
 
 
-register_vue_component('interactive_image', Path(__file__).parent / 'interactive_image.js')
 
 
-
-class InteractiveImage(SourceElement, ContentElement):
+class InteractiveImage(SourceElement, ContentElement, component='interactive_image.js'):
     CONTENT_PROP = 'content'
     CONTENT_PROP = 'content'
 
 
     def __init__(self,
     def __init__(self,
@@ -35,10 +32,9 @@ class InteractiveImage(SourceElement, ContentElement):
         :param events: list of JavaScript events to subscribe to (default: `['click']`)
         :param events: list of JavaScript events to subscribe to (default: `['click']`)
         :param cross: whether to show crosshairs (default: `False`)
         :param cross: whether to show crosshairs (default: `False`)
         """
         """
-        super().__init__(tag='interactive_image', source=source, content=content)
+        super().__init__(source=source, content=content)
         self._props['events'] = events
         self._props['events'] = events
         self._props['cross'] = cross
         self._props['cross'] = cross
-        self.use_component('interactive_image')
 
 
         def handle_mouse(e: GenericEventArguments) -> None:
         def handle_mouse(e: GenericEventArguments) -> None:
             if on_mouse is None:
             if on_mouse is None:

+ 2 - 8
nicegui/elements/joystick.py

@@ -1,15 +1,10 @@
-from pathlib import Path
 from typing import Any, Callable, Optional
 from typing import Any, Callable, Optional
 
 
-from ..dependencies import register_library, register_vue_component
 from ..element import Element
 from ..element import Element
 from ..events import GenericEventArguments, JoystickEventArguments, handle_event
 from ..events import GenericEventArguments, JoystickEventArguments, handle_event
 
 
-register_vue_component('joystick', Path(__file__).parent / 'joystick.vue')
-register_library('nipplejs', Path(__file__).parent / 'lib' / 'nipplejs' / 'nipplejs.js')
 
 
-
-class Joystick(Element):
+class Joystick(Element, component='joystick.vue', libraries=['lib/nipplejs/nipplejs.js']):
 
 
     def __init__(self, *,
     def __init__(self, *,
                  on_start: Optional[Callable[..., Any]] = None,
                  on_start: Optional[Callable[..., Any]] = None,
@@ -27,8 +22,7 @@ class Joystick(Element):
         :param throttle: throttle interval in seconds for the move event (default: 0.05)
         :param throttle: throttle interval in seconds for the move event (default: 0.05)
         :param options: arguments like `color` which should be passed to the `underlying nipple.js library <https://github.com/yoannmoinet/nipplejs#options>`_
         :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.use_library('nipplejs')
+        super().__init__()
         self._props['options'] = options
         self._props['options'] = options
         self.active = False
         self.active = False
 
 

+ 2 - 7
nicegui/elements/keyboard.py

@@ -1,18 +1,14 @@
-from pathlib import Path
 from typing import Any, Callable, List
 from typing import Any, Callable, List
 
 
 from typing_extensions import Literal
 from typing_extensions import Literal
 
 
 from ..binding import BindableProperty
 from ..binding import BindableProperty
-from ..dependencies import register_vue_component
 from ..element import Element
 from ..element import Element
 from ..events import (GenericEventArguments, KeyboardAction, KeyboardKey, KeyboardModifiers, KeyEventArguments,
 from ..events import (GenericEventArguments, KeyboardAction, KeyboardKey, KeyboardModifiers, KeyEventArguments,
                       handle_event)
                       handle_event)
 
 
-register_vue_component('keyboard', Path(__file__).parent / 'keyboard.js')
 
 
-
-class Keyboard(Element):
+class Keyboard(Element, component='keyboard.js'):
     active = BindableProperty()
     active = BindableProperty()
 
 
     def __init__(self,
     def __init__(self,
@@ -30,14 +26,13 @@ class Keyboard(Element):
         :param repeating: boolean flag indicating whether held keys should be sent repeatedly (default: `True`)
         :param repeating: boolean flag indicating whether held keys should be sent repeatedly (default: `True`)
         :param ignore: ignore keys when one of these element types is focussed (default: `['input', 'select', 'button', 'textarea']`)
         :param ignore: ignore keys when one of these element types is focussed (default: `['input', 'select', 'button', 'textarea']`)
         """
         """
-        super().__init__('keyboard')
+        super().__init__()
         self.key_handler = on_key
         self.key_handler = on_key
         self.active = active
         self.active = active
         self._props['events'] = ['keydown', 'keyup']
         self._props['events'] = ['keydown', 'keyup']
         self._props['repeating'] = repeating
         self._props['repeating'] = repeating
         self._props['ignore'] = ignore
         self._props['ignore'] = ignore
         self.on('key', self.handle_key)
         self.on('key', self.handle_key)
-        self.use_component('keyboard')
 
 
     def handle_key(self, e: GenericEventArguments) -> None:
     def handle_key(self, e: GenericEventArguments) -> None:
         if not self.active:
         if not self.active:

+ 2 - 7
nicegui/elements/link.py

@@ -1,15 +1,11 @@
-from pathlib import Path
 from typing import Any, Callable, Union
 from typing import Any, Callable, Union
 
 
 from .. import globals
 from .. import globals
-from ..dependencies import register_vue_component
 from ..element import Element
 from ..element import Element
 from .mixins.text_element import TextElement
 from .mixins.text_element import TextElement
 
 
-register_vue_component('link', Path(__file__).parent / 'link.js')
 
 
-
-class Link(TextElement):
+class Link(TextElement, component='link.js'):
 
 
     def __init__(self,
     def __init__(self,
                  text: str = '',
                  text: str = '',
@@ -27,7 +23,7 @@ class Link(TextElement):
         :param target: page function, NiceGUI element on the same page or string that is a an absolute URL or relative path from base URL
         :param target: page function, NiceGUI element on the same page or string that is a an absolute URL or relative path from base URL
         :param new_tab: open link in new tab (default: False)
         :param new_tab: open link in new tab (default: False)
         """
         """
-        super().__init__(tag='link', text=text)
+        super().__init__(text=text)
         if isinstance(target, str):
         if isinstance(target, str):
             self._props['href'] = target
             self._props['href'] = target
         elif isinstance(target, Element):
         elif isinstance(target, Element):
@@ -36,7 +32,6 @@ class Link(TextElement):
             self._props['href'] = globals.page_routes[target]
             self._props['href'] = globals.page_routes[target]
         self._props['target'] = '_blank' if new_tab else '_self'
         self._props['target'] = '_blank' if new_tab else '_self'
         self._classes = ['nicegui-link']
         self._classes = ['nicegui-link']
-        self.use_component('link')
 
 
 
 
 class LinkTarget(Element):
 class LinkTarget(Element):

+ 2 - 7
nicegui/elements/log.py

@@ -1,15 +1,11 @@
 import urllib.parse
 import urllib.parse
 from collections import deque
 from collections import deque
-from pathlib import Path
 from typing import Any, Optional
 from typing import Any, Optional
 
 
-from ..dependencies import register_vue_component
 from ..element import Element
 from ..element import Element
 
 
-register_vue_component('log', Path(__file__).parent / 'log.js')
 
 
-
-class Log(Element):
+class Log(Element, component='log.js'):
 
 
     def __init__(self, max_lines: Optional[int] = None) -> None:
     def __init__(self, max_lines: Optional[int] = None) -> None:
         """Log view
         """Log view
@@ -18,12 +14,11 @@ class Log(Element):
 
 
         :param max_lines: maximum number of lines before dropping oldest ones (default: `None`)
         :param max_lines: maximum number of lines before dropping oldest ones (default: `None`)
         """
         """
-        super().__init__('log')
+        super().__init__()
         self._props['max_lines'] = max_lines
         self._props['max_lines'] = max_lines
         self._props['lines'] = ''
         self._props['lines'] = ''
         self._classes = ['nicegui-log']
         self._classes = ['nicegui-log']
         self.lines: deque[str] = deque(maxlen=max_lines)
         self.lines: deque[str] = deque(maxlen=max_lines)
-        self.use_component('log')
         self.total_count: int = 0
         self.total_count: int = 0
 
 
     def push(self, line: Any) -> None:
     def push(self, line: Any) -> None:

+ 0 - 6
nicegui/elements/markdown.js

@@ -37,9 +37,3 @@ export default {
     },
     },
   },
   },
 };
 };
-
-function decodeHtml(html) {
-  const txt = document.createElement("textarea");
-  txt.innerHTML = html;
-  return txt.value;
-}

+ 4 - 8
nicegui/elements/markdown.py

@@ -1,19 +1,16 @@
 import os
 import os
 import re
 import re
 from functools import lru_cache
 from functools import lru_cache
-from pathlib import Path
 from typing import List
 from typing import List
 
 
 import markdown2
 import markdown2
 from pygments.formatters import HtmlFormatter
 from pygments.formatters import HtmlFormatter
 
 
-from ..dependencies import register_vue_component
+from .mermaid import Mermaid
 from .mixins.content_element import ContentElement
 from .mixins.content_element import ContentElement
 
 
-register_vue_component('markdown', Path(__file__).parent / 'markdown.js')
 
 
-
-class Markdown(ContentElement):
+class Markdown(ContentElement, component='markdown.js'):
 
 
     def __init__(self, content: str = '', *, extras: List[str] = ['fenced-code-blocks', 'tables']) -> None:
     def __init__(self, content: str = '', *, extras: List[str] = ['fenced-code-blocks', 'tables']) -> None:
         """Markdown Element
         """Markdown Element
@@ -24,13 +21,12 @@ class Markdown(ContentElement):
         :param extras: list of `markdown2 extensions <https://github.com/trentm/python-markdown2/wiki/Extras#implemented-extras>`_ (default: `['fenced-code-blocks', 'tables']`)
         :param extras: list of `markdown2 extensions <https://github.com/trentm/python-markdown2/wiki/Extras#implemented-extras>`_ (default: `['fenced-code-blocks', 'tables']`)
         """
         """
         self.extras = extras
         self.extras = extras
-        super().__init__(tag='markdown', content=content)
+        super().__init__(content=content)
         self._classes = ['nicegui-markdown']
         self._classes = ['nicegui-markdown']
         self._props['codehilite_css'] = HtmlFormatter(nobackground=True).get_style_defs('.codehilite')
         self._props['codehilite_css'] = HtmlFormatter(nobackground=True).get_style_defs('.codehilite')
-        self.use_component('markdown')
         if 'mermaid' in extras:
         if 'mermaid' in extras:
             self._props['use_mermaid'] = True
             self._props['use_mermaid'] = True
-            self.use_library('mermaid')
+            self.libraries.append(Mermaid.exposed_libraries[0])
 
 
     def on_content_change(self, content: str) -> None:
     def on_content_change(self, content: str) -> None:
         html = prepare_content(content, extras=' '.join(self.extras))
         html = prepare_content(content, extras=' '.join(self.extras))

+ 16 - 1
nicegui/elements/mermaid.js

@@ -1,14 +1,29 @@
 import mermaid from "mermaid";
 import mermaid from "mermaid";
+
+let is_running = false;
+const queue = [];
+
 export default {
 export default {
   template: `<div></div>`,
   template: `<div></div>`,
+  data: () => ({
+    last_content: "",
+  }),
   mounted() {
   mounted() {
     this.update(this.content);
     this.update(this.content);
   },
   },
   methods: {
   methods: {
     async update(content) {
     async update(content) {
+      if (this.last_content === content) return;
+      this.last_content = content;
       this.$el.innerHTML = content;
       this.$el.innerHTML = content;
       this.$el.removeAttribute("data-processed");
       this.$el.removeAttribute("data-processed");
-      await mermaid.run({ nodes: [this.$el] });
+      queue.push(this.$el);
+      if (is_running) return;
+      is_running = true;
+      while (queue.length) {
+        await mermaid.run({ nodes: [queue.shift()] });
+      }
+      is_running = false;
     },
     },
   },
   },
   props: {
   props: {

+ 5 - 10
nicegui/elements/mermaid.py

@@ -1,13 +1,10 @@
-from pathlib import Path
-
-from ..dependencies import register_library, register_vue_component
 from .mixins.content_element import ContentElement
 from .mixins.content_element import ContentElement
 
 
-register_vue_component('mermaid', Path(__file__).parent / 'mermaid.js')
-register_library('mermaid', Path(__file__).parent / 'lib' / 'mermaid' / 'mermaid.esm.min.mjs', expose=True)
-
 
 
-class Mermaid(ContentElement):
+class Mermaid(ContentElement,
+              component='mermaid.js',
+              exposed_libraries=['lib/mermaid/mermaid.esm.min.mjs'],
+              extra_libraries=['lib/mermaid/*.js']):
     CONTENT_PROP = 'content'
     CONTENT_PROP = 'content'
 
 
     def __init__(self, content: str) -> None:
     def __init__(self, content: str) -> None:
@@ -18,9 +15,7 @@ class Mermaid(ContentElement):
 
 
         :param content: the Mermaid content to be displayed
         :param content: the Mermaid content to be displayed
         '''
         '''
-        super().__init__(tag='mermaid', content=content)
-        self.use_component('mermaid')
-        self.use_library('mermaid')
+        super().__init__(content=content)
 
 
     def on_content_change(self, content: str) -> None:
     def on_content_change(self, content: str) -> None:
         self._props[self.CONTENT_PROP] = content.strip()
         self._props[self.CONTENT_PROP] = content.strip()

+ 2 - 8
nicegui/elements/plotly.py

@@ -1,16 +1,11 @@
-from pathlib import Path
 from typing import Dict, Union
 from typing import Dict, Union
 
 
 import plotly.graph_objects as go
 import plotly.graph_objects as go
 
 
-from ..dependencies import register_library, register_vue_component
 from ..element import Element
 from ..element import Element
 
 
-register_vue_component('plotly', Path(__file__).parent / 'plotly.vue')
-register_library('plotly', Path(__file__).parent / 'lib' / 'plotly' / 'plotly.min.js')
 
 
-
-class Plotly(Element):
+class Plotly(Element, component='plotly.vue', libraries=['lib/plotly/plotly.min.js']):
 
 
     def __init__(self, figure: Union[Dict, go.Figure]) -> None:
     def __init__(self, figure: Union[Dict, go.Figure]) -> None:
         """Plotly Element
         """Plotly Element
@@ -27,8 +22,7 @@ class Plotly(Element):
         :param figure: Plotly figure to be rendered. Can be either a `go.Figure` instance, or
         :param figure: Plotly figure to be rendered. Can be either a `go.Figure` instance, or
                        a `dict` object with keys `data`, `layout`, `config` (optional).
                        a `dict` object with keys `data`, `layout`, `config` (optional).
         """
         """
-        super().__init__('plotly')
-        self.use_library('plotly')
+        super().__init__()
 
 
         self.figure = figure
         self.figure = figure
         self.update()
         self.update()

+ 2 - 7
nicegui/elements/query.py

@@ -1,24 +1,19 @@
-from pathlib import Path
 from typing import Optional
 from typing import Optional
 
 
 from typing_extensions import Self
 from typing_extensions import Self
 
 
-from ..dependencies import register_vue_component
 from ..element import Element
 from ..element import Element
 from ..globals import get_client
 from ..globals import get_client
 
 
-register_vue_component('query', Path(__file__).parent / 'query.js')
 
 
-
-class Query(Element):
+class Query(Element, component='query.js'):
 
 
     def __init__(self, selector: str) -> None:
     def __init__(self, selector: str) -> None:
-        super().__init__('query')
+        super().__init__()
         self._props['selector'] = selector
         self._props['selector'] = selector
         self._props['classes'] = []
         self._props['classes'] = []
         self._props['style'] = {}
         self._props['style'] = {}
         self._props['props'] = {}
         self._props['props'] = {}
-        self.use_component('query')
 
 
     def classes(self, add: Optional[str] = None, *, remove: Optional[str] = None, replace: Optional[str] = None) \
     def classes(self, add: Optional[str] = None, *, remove: Optional[str] = None, replace: Optional[str] = None) \
             -> Self:
             -> Self:

+ 11 - 19
nicegui/elements/scene.py

@@ -1,22 +1,12 @@
 from dataclasses import dataclass
 from dataclasses import dataclass
-from pathlib import Path
 from typing import Any, Callable, Dict, List, Optional, Union
 from typing import Any, Callable, Dict, List, Optional, Union
 
 
 from .. import binding, globals
 from .. import binding, globals
-from ..dependencies import register_library, register_vue_component
 from ..element import Element
 from ..element import Element
 from ..events import GenericEventArguments, SceneClickEventArguments, SceneClickHit, handle_event
 from ..events import GenericEventArguments, SceneClickEventArguments, SceneClickHit, handle_event
 from ..helpers import KWONLY_SLOTS
 from ..helpers import KWONLY_SLOTS
 from .scene_object3d import Object3D
 from .scene_object3d import Object3D
 
 
-register_vue_component('scene', Path(__file__).parent / 'scene.js')
-register_library('three', Path(__file__).parent / 'lib' / 'three' / 'three.module.js', expose=True)
-register_library('CSS2DRenderer', Path(__file__).parent / 'lib' / 'three' / 'modules' / 'CSS2DRenderer.js', expose=True)
-register_library('CSS3DRenderer', Path(__file__).parent / 'lib' / 'three' / 'modules' / 'CSS3DRenderer.js', expose=True)
-register_library('OrbitControls', Path(__file__).parent / 'lib' / 'three' / 'modules' / 'OrbitControls.js', expose=True)
-register_library('STLLoader', Path(__file__).parent / 'lib' / 'three' / 'modules' / 'STLLoader.js', expose=True)
-register_library('tween', Path(__file__).parent / 'lib' / 'tween' / 'tween.umd.js')
-
 
 
 @dataclass(**KWONLY_SLOTS)
 @dataclass(**KWONLY_SLOTS)
 class SceneCamera:
 class SceneCamera:
@@ -36,7 +26,16 @@ class SceneObject:
     id: str = 'scene'
     id: str = 'scene'
 
 
 
 
-class Scene(Element):
+class Scene(Element,
+            component='scene.js',
+            libraries=['lib/tween/tween.umd.js'],
+            exposed_libraries=[
+                'lib/three/three.module.js',
+                'lib/three/modules/CSS2DRenderer.js',
+                'lib/three/modules/CSS3DRenderer.js',
+                'lib/three/modules/OrbitControls.js',
+                'lib/three/modules/STLLoader.js',
+            ]):
     from .scene_objects import Box as box
     from .scene_objects import Box as box
     from .scene_objects import Curve as curve
     from .scene_objects import Curve as curve
     from .scene_objects import Cylinder as cylinder
     from .scene_objects import Cylinder as cylinder
@@ -71,7 +70,7 @@ class Scene(Element):
         :param grid: whether to display a grid
         :param grid: whether to display a grid
         :param on_click: callback to execute when a 3d object is clicked
         :param on_click: callback to execute when a 3d object is clicked
         """
         """
-        super().__init__('scene')
+        super().__init__()
         self._props['width'] = width
         self._props['width'] = width
         self._props['height'] = height
         self._props['height'] = height
         self._props['grid'] = grid
         self._props['grid'] = grid
@@ -82,13 +81,6 @@ class Scene(Element):
         self.is_initialized = False
         self.is_initialized = False
         self.on('init', self.handle_init)
         self.on('init', self.handle_init)
         self.on('click3d', self.handle_click)
         self.on('click3d', self.handle_click)
-        self.use_component('scene')
-        self.use_library('three')
-        self.use_library('CSS2DRenderer')
-        self.use_library('CSS3DRenderer')
-        self.use_library('OrbitControls')
-        self.use_library('STLLoader')
-        self.use_library('tween')
 
 
     def handle_init(self, e: GenericEventArguments) -> None:
     def handle_init(self, e: GenericEventArguments) -> None:
         self.is_initialized = True
         self.is_initialized = True

+ 2 - 7
nicegui/elements/select.py

@@ -1,17 +1,13 @@
 import re
 import re
 from copy import deepcopy
 from copy import deepcopy
-from pathlib import Path
 from typing import Any, Callable, Dict, List, Optional, Union
 from typing import Any, Callable, Dict, List, Optional, Union
 
 
-from ..dependencies import register_vue_component
 from ..events import GenericEventArguments
 from ..events import GenericEventArguments
 from .choice_element import ChoiceElement
 from .choice_element import ChoiceElement
 from .mixins.disableable_element import DisableableElement
 from .mixins.disableable_element import DisableableElement
 
 
-register_vue_component('select', Path(__file__).parent / 'select.js')
 
 
-
-class Select(ChoiceElement, DisableableElement):
+class Select(ChoiceElement, DisableableElement, component='select.js'):
 
 
     def __init__(self,
     def __init__(self,
                  options: Union[List, Dict], *,
                  options: Union[List, Dict], *,
@@ -40,8 +36,7 @@ class Select(ChoiceElement, DisableableElement):
                 value = []
                 value = []
             elif not isinstance(value, list):
             elif not isinstance(value, list):
                 value = [value]
                 value = [value]
-        super().__init__(tag='select', options=options, value=value, on_change=on_change)
-        self.use_component('select')
+        super().__init__(options=options, value=value, on_change=on_change)
         if label is not None:
         if label is not None:
             self._props['label'] = label
             self._props['label'] = label
         if with_input:
         if with_input:

+ 2 - 8
nicegui/elements/table.py

@@ -1,17 +1,13 @@
-from pathlib import Path
 from typing import Any, Callable, Dict, List, Optional
 from typing import Any, Callable, Dict, List, Optional
 
 
 from typing_extensions import Literal
 from typing_extensions import Literal
 
 
-from ..dependencies import register_vue_component
 from ..element import Element
 from ..element import Element
 from ..events import GenericEventArguments, TableSelectionEventArguments, handle_event
 from ..events import GenericEventArguments, TableSelectionEventArguments, handle_event
 from .mixins.filter_element import FilterElement
 from .mixins.filter_element import FilterElement
 
 
-register_vue_component('nicegui-table', Path(__file__).parent / 'table.js')
 
 
-
-class Table(FilterElement):
+class Table(FilterElement, component='table.js'):
 
 
     def __init__(self,
     def __init__(self,
                  columns: List[Dict],
                  columns: List[Dict],
@@ -36,7 +32,7 @@ class Table(FilterElement):
 
 
         If selection is 'single' or 'multiple', then a `selected` property is accessible containing the selected rows.
         If selection is 'single' or 'multiple', then a `selected` property is accessible containing the selected rows.
         """
         """
-        super().__init__(tag='nicegui-table')
+        super().__init__()
 
 
         self.rows = rows
         self.rows = rows
         self.row_key = row_key
         self.row_key = row_key
@@ -63,8 +59,6 @@ class Table(FilterElement):
             handle_event(on_select, arguments)
             handle_event(on_select, arguments)
         self.on('selection', handle_selection, ['added', 'rows', 'keys'])
         self.on('selection', handle_selection, ['added', 'rows', 'keys'])
 
 
-        self.use_component('nicegui-table')
-
     def add_rows(self, *rows: Dict) -> None:
     def add_rows(self, *rows: Dict) -> None:
         """Add rows to the table."""
         """Add rows to the table."""
         self.rows.extend(rows)
         self.rows.extend(rows)

+ 1 - 1
nicegui/elements/textarea.py

@@ -3,7 +3,7 @@ from typing import Any, Callable, Dict, Optional
 from .input import Input
 from .input import Input
 
 
 
 
-class Textarea(Input):
+class Textarea(Input, component='input.js'):
 
 
     def __init__(self,
     def __init__(self,
                  label: Optional[str] = None, *,
                  label: Optional[str] = None, *,

+ 2 - 7
nicegui/elements/upload.py

@@ -1,18 +1,14 @@
-from pathlib import Path
 from typing import Any, Callable, Dict, Optional
 from typing import Any, Callable, Dict, Optional
 
 
 from fastapi import Request
 from fastapi import Request
 from starlette.datastructures import UploadFile
 from starlette.datastructures import UploadFile
 
 
-from ..dependencies import register_vue_component
 from ..events import EventArguments, UploadEventArguments, handle_event
 from ..events import EventArguments, UploadEventArguments, handle_event
 from ..nicegui import app
 from ..nicegui import app
 from .mixins.disableable_element import DisableableElement
 from .mixins.disableable_element import DisableableElement
 
 
-register_vue_component('upload', Path(__file__).parent / 'upload.js')
 
 
-
-class Upload(DisableableElement):
+class Upload(DisableableElement, component='upload.js'):
 
 
     def __init__(self, *,
     def __init__(self, *,
                  multiple: bool = False,
                  multiple: bool = False,
@@ -37,8 +33,7 @@ class Upload(DisableableElement):
         :param label: label for the uploader (default: `''`)
         :param label: label for the uploader (default: `''`)
         :param auto_upload: automatically upload files when they are selected (default: `False`)
         :param auto_upload: automatically upload files when they are selected (default: `False`)
         """
         """
-        super().__init__(tag='upload')
-        self.use_component('upload')
+        super().__init__()
         self._props['multiple'] = multiple
         self._props['multiple'] = multiple
         self._props['label'] = label
         self._props['label'] = label
         self._props['auto-upload'] = auto_upload
         self._props['auto-upload'] = auto_upload

+ 2 - 6
nicegui/elements/video.py

@@ -3,13 +3,10 @@ from pathlib import Path
 from typing import Union
 from typing import Union
 
 
 from .. import globals
 from .. import globals
-from ..dependencies import register_vue_component
 from ..element import Element
 from ..element import Element
 
 
-register_vue_component('video', Path(__file__).parent / 'video.js')
 
 
-
-class Video(Element):
+class Video(Element, component='video.js'):
 
 
     def __init__(self, src: Union[str, Path], *,
     def __init__(self, src: Union[str, Path], *,
                  controls: bool = True,
                  controls: bool = True,
@@ -29,7 +26,7 @@ class Video(Element):
         See `here <https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video#events>`_
         See `here <https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video#events>`_
         for a list of events you can subscribe to using the generic event subscription `on()`.
         for a list of events you can subscribe to using the generic event subscription `on()`.
         """
         """
-        super().__init__('video')
+        super().__init__()
         if Path(src).is_file():
         if Path(src).is_file():
             src = globals.app.add_media_file(local_file=src)
             src = globals.app.add_media_file(local_file=src)
         self._props['src'] = src
         self._props['src'] = src
@@ -37,7 +34,6 @@ class Video(Element):
         self._props['autoplay'] = autoplay
         self._props['autoplay'] = autoplay
         self._props['muted'] = muted
         self._props['muted'] = muted
         self._props['loop'] = loop
         self._props['loop'] = loop
-        self.use_component('video')
 
 
         if type:
         if type:
             url = f'https://github.com/zauberzeug/nicegui/pull/624'
             url = f'https://github.com/zauberzeug/nicegui/pull/624'

+ 5 - 7
nicegui/functions/refreshable.py

@@ -1,16 +1,12 @@
 from dataclasses import dataclass
 from dataclasses import dataclass
-from pathlib import Path
 from typing import Any, Awaitable, Callable, Dict, List, Tuple, Union
 from typing import Any, Awaitable, Callable, Dict, List, Tuple, Union
 
 
 from typing_extensions import Self
 from typing_extensions import Self
 
 
 from .. import background_tasks, globals
 from .. import background_tasks, globals
-from ..dependencies import register_vue_component
 from ..element import Element
 from ..element import Element
 from ..helpers import KWONLY_SLOTS, is_coroutine_function
 from ..helpers import KWONLY_SLOTS, is_coroutine_function
 
 
-register_vue_component('refreshable', Path(__file__).parent / 'refreshable.js')
-
 
 
 @dataclass(**KWONLY_SLOTS)
 @dataclass(**KWONLY_SLOTS)
 class RefreshableTarget:
 class RefreshableTarget:
@@ -37,6 +33,10 @@ class RefreshableTarget:
             return None  # required by mypy
             return None  # required by mypy
 
 
 
 
+class RefreshableContainer(Element, component='refreshable.js'):
+    pass
+
+
 class refreshable:
 class refreshable:
 
 
     def __init__(self, func: Callable[..., Any]) -> None:
     def __init__(self, func: Callable[..., Any]) -> None:
@@ -55,9 +55,7 @@ class refreshable:
 
 
     def __call__(self, *args: Any, **kwargs: Any) -> Union[None, Awaitable]:
     def __call__(self, *args: Any, **kwargs: Any) -> Union[None, Awaitable]:
         self.prune()
         self.prune()
-        container = Element('refreshable')
-        container.use_component('refreshable')
-        target = RefreshableTarget(container=container, instance=self.instance, args=args, kwargs=kwargs)
+        target = RefreshableTarget(container=RefreshableContainer(), instance=self.instance, args=args, kwargs=kwargs)
         self.targets.append(target)
         self.targets.append(target)
         return target.run(self.func)
         return target.run(self.func)
 
 

+ 14 - 15
nicegui/nicegui.py

@@ -44,21 +44,20 @@ def index(request: Request) -> Response:
     return globals.index_client.build_response(request)
     return globals.index_client.build_response(request)
 
 
 
 
-@app.get(f'/_nicegui/{__version__}' + '/library/{name}/{file}')
-def get_dependencies(name: str, file: str):
-    if name in libraries and libraries[name]['path'].exists():
-        filepath = Path(libraries[name]['path']).parent / file
-        if filepath.exists() and not filepath.is_dir():
-            return FileResponse(filepath, media_type='text/javascript')
-        return FileResponse(libraries[name]['path'], media_type='text/javascript')
-    raise HTTPException(status_code=404, detail=f'dependency "{name}" not found')
-
-
-@app.get(f'/_nicegui/{__version__}' + '/components/{name}')
-def get_components(name: str):
-    if name in js_components and js_components[name]['path'].exists():
-        return FileResponse(js_components[name]['path'], media_type='text/javascript')
-    raise HTTPException(status_code=404, detail=f'library "{name}" not found')
+@app.get(f'/_nicegui/{__version__}' + '/libraries/{key:path}')
+def get_library(key: str) -> FileResponse:
+    if key in libraries and libraries[key].path.exists():
+        headers = {'Cache-Control': 'public, max-age=3600'}
+        return FileResponse(libraries[key].path, media_type='text/javascript', headers=headers)
+    raise HTTPException(status_code=404, detail=f'library "{key}" not found')
+
+
+@app.get(f'/_nicegui/{__version__}' + '/components/{key:path}')
+def get_component(key: str) -> FileResponse:
+    if key in js_components and js_components[key].path.exists():
+        headers = {'Cache-Control': 'public, max-age=3600'}
+        return FileResponse(js_components[key].path, media_type='text/javascript', headers=headers)
+    raise HTTPException(status_code=404, detail=f'component "{key}" not found')
 
 
 
 
 @app.on_event('startup')
 @app.on_event('startup')

+ 68 - 42
nicegui/templates/index.html

@@ -19,7 +19,9 @@
     <script src="{{ prefix | safe }}/_nicegui/{{version}}/static/vue.global.prod.js"></script>
     <script src="{{ prefix | safe }}/_nicegui/{{version}}/static/vue.global.prod.js"></script>
     <script src="{{ prefix | safe }}/_nicegui/{{version}}/static/quasar.umd.prod.js"></script>
     <script src="{{ prefix | safe }}/_nicegui/{{version}}/static/quasar.umd.prod.js"></script>
     <script src="{{ prefix | safe }}/_nicegui/{{version}}/static/quasar.{{ language }}.umd.prod.js"></script>
     <script src="{{ prefix | safe }}/_nicegui/{{version}}/static/quasar.{{ language }}.umd.prod.js"></script>
-    {{ import_maps | safe }}
+    <script type="importmap">
+      {"imports": {{ imports | safe }}}
+    </script>
 
 
     <!-- NOTE: force Prettier to keep the line break -->
     <!-- NOTE: force Prettier to keep the line break -->
     {{ body_html | safe }}
     {{ body_html | safe }}
@@ -103,8 +105,8 @@
         }
         }
 
 
         // @todo: Try avoid this with better handling of initial page load.
         // @todo: Try avoid this with better handling of initial page load.
-        element['components'].forEach((component) => loaded_components.add(component));
-        element['libraries'].forEach((library) => loaded_libraries.add(library));
+        if (element.component) loaded_components.add(element.component.name);
+        element.libraries.forEach((library) => loaded_libraries.add(library.name));
 
 
         const props = {
         const props = {
           id: 'c' + element.id,
           id: 'c' + element.id,
@@ -151,7 +153,7 @@
                 props: { props: { type: Object, default: {} } },
                 props: { props: { type: Object, default: {} } },
                 template: data.template,
                 template: data.template,
               }, {
               }, {
-                props: props
+                props: props,
               }));
               }));
             }
             }
             const children = data.ids.map(id => renderRecursively(elements, id));
             const children = data.ids.map(id => renderRecursively(elements, id));
@@ -188,17 +190,19 @@
       }
       }
 
 
       async function loadDependencies(element) {
       async function loadDependencies(element) {
-        for (const name of element['libraries']) {
+        if (element.component) {
+          const {name, key, tag} = element.component;
+          if (!loaded_components.has(name) && !key.endsWith('.vue')) {
+            const component = await import(`{{ prefix | safe }}/_nicegui/{{version}}/components/${key}`);
+            app = app.component(tag, component.default);
+            loaded_components.add(name);
+          }
+        }
+        for (const {name, key} of element.libraries) {
           if (loaded_libraries.has(name)) continue;
           if (loaded_libraries.has(name)) continue;
-          await import(`{{ prefix | safe }}/_nicegui/{{version}}/library/${name}/include`);
+          await import(`{{ prefix | safe }}/_nicegui/{{version}}/libraries/${key}`);
           loaded_libraries.add(name);
           loaded_libraries.add(name);
         }
         }
-        for (const name of element['components']) {
-          if (loaded_components.has(name)) continue;
-          const component = (await import(`{{ prefix | safe }}/_nicegui/{{version}}/components/${name}`)).default;
-          app = app.component(name, component);
-          loaded_components.add(name);
-        }
       }
       }
 
 
       let app = Vue.createApp({
       let app = Vue.createApp({
@@ -218,37 +222,59 @@
           const transports = ['websocket', 'polling'];
           const transports = ['websocket', 'polling'];
           window.path_prefix = "{{ prefix | safe }}";
           window.path_prefix = "{{ prefix | safe }}";
           window.socket = io(url, { path: "{{ prefix | safe }}/_nicegui_ws/socket.io", query, extraHeaders, transports });
           window.socket = io(url, { path: "{{ prefix | safe }}/_nicegui_ws/socket.io", query, extraHeaders, transports });
-          window.socket.on("connect", () => {
-            window.socket.emit("handshake", (ok) => {
-              if (!ok) window.location.reload();
-              document.getElementById('popup').style.opacity = 0;
+          const messageHandlers = {
+            connect: () => {
+              window.socket.emit("handshake", (ok) => {
+                if (!ok) window.location.reload();
+                document.getElementById('popup').style.opacity = 0;
+              });
+            },
+            connect_error: (err) => {
+              if (err.message == 'timeout') window.location.reload(); // see https://github.com/zauberzeug/nicegui/issues/198
+            },
+            disconnect: () => {
+              document.getElementById('popup').style.opacity = 1;
+            },
+            update: async (msg) => {
+              for (const [id, element] of Object.entries(msg)) {
+                await loadDependencies(element);
+                this.elements[element.id] = element;
+              }
+            },
+            run_method: (msg) => {
+              const element = getElement(msg.id);
+              if (element === null || element === undefined) return;
+              if (msg.name in element) {
+                element[msg.name](...msg.args);
+              } else {
+                element.$refs.qRef[msg.name](...msg.args);
+              }
+            },
+            run_javascript: (msg) => runJavascript(msg['code'], msg['request_id']),
+            open: (msg) => (location.href = msg.startsWith('/') ? "{{ prefix | safe }}" + msg : msg),
+            download: (msg) => download(msg.url, msg.filename),
+            notify: (msg) => Quasar.Notify.create(msg),
+          };
+          const socketMessageQueue = [];
+          let isProcessingSocketMessage = false;
+          for (const [event, handler] of Object.entries(messageHandlers)) {
+            window.socket.on(event, async (...args) => {
+              socketMessageQueue.push(() => handler(...args));
+              if (!isProcessingSocketMessage) {
+                while (socketMessageQueue.length > 0) {
+                  const handler = socketMessageQueue.shift()
+                  isProcessingSocketMessage = true;
+                  try {
+                    await handler();
+                  }
+                  catch (e) {
+                    console.error(e);
+                  }
+                  isProcessingSocketMessage = false;
+                }
+              }
             });
             });
-          });
-          window.socket.on("connect_error", (err) => {
-            if (err.message == 'timeout') window.location.reload(); // see https://github.com/zauberzeug/nicegui/issues/198
-          });
-          window.socket.on("disconnect", () => {
-            document.getElementById('popup').style.opacity = 1;
-          });
-          window.socket.on("update", async (msg) => {
-            for (const [id, element] of Object.entries(msg)) {
-              await loadDependencies(element);
-              this.elements[element.id] = element;
-            }
-          });
-          window.socket.on("run_method", (msg) => {
-            const element = getElement(msg.id);
-            if (element === null || element === undefined) return;
-            if (msg.name in element) {
-              element[msg.name](...msg.args);
-            } else {
-              element.$refs.qRef[msg.name](...msg.args);
-            }
-          });
-          window.socket.on("run_javascript", (msg) => runJavascript(msg['code'], msg['request_id']));
-          window.socket.on("open", (msg) => (location.href = msg.startsWith('/') ? "{{ prefix | safe }}" + msg : msg));
-          window.socket.on("download", (msg) => download(msg.url, msg.filename));
-          window.socket.on("notify", (msg) => Quasar.Notify.create(msg));
+          }
         },
         },
       }).use(Quasar, {
       }).use(Quasar, {
         config: {
         config: {

+ 18 - 0
tests/test_aggrid.py

@@ -1,4 +1,5 @@
 from selenium.webdriver.common.action_chains import ActionChains
 from selenium.webdriver.common.action_chains import ActionChains
+from selenium.webdriver.common.by import By
 from selenium.webdriver.common.keys import Keys
 from selenium.webdriver.common.keys import Keys
 
 
 from nicegui import ui
 from nicegui import ui
@@ -142,3 +143,20 @@ def test_create_from_pandas(screen: Screen):
     screen.should_contain('Bob')
     screen.should_contain('Bob')
     screen.should_contain('18')
     screen.should_contain('18')
     screen.should_contain('21')
     screen.should_contain('21')
+
+
+def test_create_dynamically(screen: Screen):
+    ui.button('Create', on_click=lambda: ui.aggrid({'columnDefs': [{'field': 'name'}], 'rowData': [{'name': 'Alice'}]}))
+
+    screen.open('/')
+    screen.click('Create')
+    screen.should_contain('Alice')
+
+
+def test_api_method_after_creation(screen: Screen):
+    options = {'columnDefs': [{'field': 'name'}], 'rowData': [{'name': 'Alice'}]}
+    ui.button('Create', on_click=lambda: ui.aggrid(options).call_api_method('selectAll'))
+
+    screen.open('/')
+    screen.click('Create')
+    assert screen.selenium.find_element(By.CLASS_NAME, 'ag-row-selected')

+ 15 - 6
tests/test_chart.py

@@ -73,18 +73,19 @@ def test_removing_chart_series(screen: Screen):
     assert len(screen.selenium.find_elements(By.CSS_SELECTOR, '.highcharts-point')) == 3
     assert len(screen.selenium.find_elements(By.CSS_SELECTOR, '.highcharts-point')) == 3
 
 
 
 
-def test_extra(screen: Screen):
-    ui.chart({'chart': {'type': 'solidgauge'}}, extras=['solid-gauge'])
+def test_missing_extra(screen: Screen):
+    # NOTE: This test does not work after test_extra() has been run, because conftest won't reset libraries correctly.
+    ui.chart({'chart': {'type': 'solidgauge'}})
 
 
     screen.open('/')
     screen.open('/')
-    assert screen.selenium.find_elements(By.CSS_SELECTOR, '.highcharts-pane')
+    assert not screen.selenium.find_elements(By.CSS_SELECTOR, '.highcharts-pane')
 
 
 
 
-def test_missing_extra(screen: Screen):
-    ui.chart({'chart': {'type': 'solidgauge'}})
+def test_extra(screen: Screen):
+    ui.chart({'chart': {'type': 'solidgauge'}}, extras=['solid-gauge'])
 
 
     screen.open('/')
     screen.open('/')
-    assert not screen.selenium.find_elements(By.CSS_SELECTOR, '.highcharts-pane')
+    assert screen.selenium.find_elements(By.CSS_SELECTOR, '.highcharts-pane')
 
 
 
 
 def test_stock_chart(screen: Screen):
 def test_stock_chart(screen: Screen):
@@ -131,3 +132,11 @@ def test_stock_chart(screen: Screen):
     screen.wait(0.5)
     screen.wait(0.5)
     screen.should_not_contain('alice')
     screen.should_not_contain('alice')
     screen.should_not_contain('bob')
     screen.should_not_contain('bob')
+
+
+def test_create_dynamically(screen: Screen):
+    ui.button('Create', on_click=lambda: ui.chart({}))
+
+    screen.open('/')
+    screen.click('Create')
+    screen.should_contain('Chart title')

+ 9 - 0
tests/test_mermaid.py

@@ -53,5 +53,14 @@ def test_replace_mermaid(screen: Screen):
     screen.open('/')
     screen.open('/')
     screen.should_contain('Node_A')
     screen.should_contain('Node_A')
     screen.click('Replace')
     screen.click('Replace')
+    screen.wait(0.5)
     screen.should_contain('Node_B')
     screen.should_contain('Node_B')
     screen.should_not_contain('Node_A')
     screen.should_not_contain('Node_A')
+
+
+def test_create_dynamically(screen: Screen):
+    ui.button('Create', on_click=lambda: ui.mermaid('graph LR; Node'))
+
+    screen.open('/')
+    screen.click('Create')
+    screen.should_contain('Node')

+ 8 - 0
tests/test_plotly.py

@@ -40,3 +40,11 @@ def test_replace_plotly(screen: Screen):
     screen.click('Replace')
     screen.click('Replace')
     screen.wait(0.5)
     screen.wait(0.5)
     assert screen.find_by_tag('text').text == 'B'
     assert screen.find_by_tag('text').text == 'B'
+
+
+def test_create_dynamically(screen: Screen):
+    ui.button('Create', on_click=lambda: ui.plotly(go.Figure(go.Scatter(x=[], y=[]))))
+
+    screen.open('/')
+    screen.click('Create')
+    assert screen.find_by_tag('svg')

+ 8 - 0
tests/test_scene.py

@@ -93,3 +93,11 @@ def test_replace_scene(screen: Screen):
     screen.click('Replace scene')
     screen.click('Replace scene')
     screen.wait(0.5)
     screen.wait(0.5)
     assert screen.selenium.execute_script(f'return scene_c{scene.id}.children[4].name') == 'box'
     assert screen.selenium.execute_script(f'return scene_c{scene.id}.children[4].name') == 'box'
+
+
+def test_create_dynamically(screen: Screen):
+    ui.button('Create', on_click=lambda: ui.scene())
+
+    screen.open('/')
+    screen.click('Create')
+    assert screen.find_by_tag('canvas')

+ 2 - 7
website/intersection_observer.py

@@ -1,21 +1,16 @@
-from pathlib import Path
 from typing import Callable
 from typing import Callable
 
 
-from nicegui.dependencies import register_vue_component
 from nicegui.element import Element
 from nicegui.element import Element
 from nicegui.events import EventArguments, handle_event
 from nicegui.events import EventArguments, handle_event
 
 
-register_vue_component('intersection_observer', Path(__file__).parent / 'intersection_observer.js')
 
 
-
-class IntersectionObserver(Element):
+class IntersectionObserver(Element, component='intersection_observer.js'):
 
 
     def __init__(self, *, on_intersection: Callable) -> None:
     def __init__(self, *, on_intersection: Callable) -> None:
-        super().__init__('intersection_observer')
+        super().__init__()
         self.on_intersection = on_intersection
         self.on_intersection = on_intersection
         self.active = True
         self.active = True
         self.on('intersection', self.handle_intersection, [])
         self.on('intersection', self.handle_intersection, [])
-        self.use_component('intersection_observer')
 
 
     def handle_intersection(self, _) -> None:
     def handle_intersection(self, _) -> None:
         self.run_method('stop')
         self.run_method('stop')