Explorar el Código

Merge branch 'path_improvements' into on-air

Rodja Trappe hace 1 año
padre
commit
9c318a440c

+ 3 - 3
examples/custom_vue_component/counter.py

@@ -4,16 +4,16 @@ from typing import Callable, Optional
 from nicegui.dependencies import register_vue_component
 from nicegui.element import Element
 
-register_vue_component('counter', Path(__file__).parent / 'counter.js')
+component = register_vue_component(Path('counter.js'), base_path=Path(__file__).parent)
 
 
 class Counter(Element):
 
     def __init__(self, title: str, *, on_change: Optional[Callable] = None) -> None:
-        super().__init__('counter')
+        super().__init__(component.tag)
         self._props['title'] = title
         self.on('change', on_change)
-        self.use_component('counter')
+        self.use_component(component)
 
     def reset(self) -> None:
         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.run(port=1234)
+ui.run()

+ 3 - 3
examples/map/leaflet.py

@@ -5,14 +5,14 @@ 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')
+component = register_vue_component(Path('leaflet.js'), base_path=Path(__file__).parent)
 
 
 class leaflet(Element):
 
     def __init__(self) -> None:
-        super().__init__('leaflet')
-        self.use_component('leaflet')
+        super().__init__(component.tag)
+        self.use_component(component)
         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>')
 

+ 3 - 3
examples/single_page_app/router.py

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

+ 5 - 5
nicegui/client.py

@@ -71,17 +71,17 @@ class Client:
     def build_response(self, request: Request, status_code: int = 200) -> Response:
         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()})
-        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', {
             'request': request,
             'version': __version__,
             'client_id': str(self.id),
             'elements': elements,
             '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(),
             'viewport': self.page.resolve_viewport(),
             'favicon_url': get_favicon_url(self.page, prefix),

+ 116 - 83
nicegui/dependencies.py

@@ -1,102 +1,135 @@
-import json
-import logging
+from dataclasses import dataclass
 from pathlib import Path
-from typing import Any, Dict, List, Set, Tuple
+from typing import Dict, List, Set, Tuple
 
 import vbuild
 
 from . import __version__
 from .element import Element
+from .helpers import KWONLY_SLOTS
 
-vue_components: Dict[str, Any] = {}
-js_components: Dict[str, Any] = {}
-libraries: Dict[str, Any] = {}
 
+@dataclass(**KWONLY_SLOTS)
+class Component:
+    key: str
+    name: str
 
-def register_vue_component(location: Path,
-                           base_path: Path = Path(__file__).parent / 'elements', *, expose: bool = False
-                           ) -> str:
+    @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):
+    path: Path
+
+
+@dataclass(**KWONLY_SLOTS)
+class Library:
+    key: str
+    name: str
+    path: Path
+    expose: bool
+
+
+vue_components: Dict[str, VueComponent] = {}
+js_components: Dict[str, JsComponent] = {}
+libraries: Dict[str, Library] = {}
+
+
+def register_vue_component(location: Path, base_path: Path = Path(__file__).parent / 'elements') -> Component:
     """Register a .vue or .js Vue component.
 
-    :param location: the location to the library you want to register relative to the base_path. This is also used as the resource identifier and must therefore be url-safe.
-    :param base_path: the base path where your libraries are located
-    :return: the resource identifier library name to be used in element's `use_component`        
+    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.
+
+    :param location: location to the library relative to the base_path (used as the resource identifier, must be URL-safe)
+    :param base_path: base path where your libraries are located
+    :return: resource identifier to be used in element's `use_component`
     """
-    if isinstance(location, str):
-        logging.warning('register_vue_component: location is a string, did you mean to use register_library?')
-        return
-    suffix = location.suffix.lower()
-    assert suffix in {'.vue', '.js', '.mjs'}, 'Only VUE and JS components are supported.'
-    name = location.stem
-    path = base_path / location
+    path, key, name, suffix = deconstruct_location(location, base_path)
     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[str(location)] = {'name': name, 'path': path}
-    return str(location)
-
-
-def register_library(location: Path,
-                     base_path: Path = Path(__file__).parent / 'elements' / 'lib', *, expose: bool = False
-                     ) -> str:
-    """Register a new external library.
-
-    :param location: the location to the library you want to register relative to the base_path. This is also used as the resource identifier and must therefore be url-safe.
-    :param base_path: the base path where your libraries are located
-    :param expose: if True, this will be exposed as an ESM module but NOT imported
-    :return: the resource identifier library name to be used in element's `use_library`
+        assert key not in vue_components, f'Duplicate VUE component {key}'
+        build = vbuild.VBuild(name, path.read_text())
+        vue_components[key] = VueComponent(key=key, name=name, html=build.html, script=build.script, style=build.style)
+        return vue_components[key]
+    if suffix == '.js':
+        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 "{suffix}"')
+
+
+def register_library(location: Path, base_path: Path = Path(__file__).parent / 'elements' / 'lib', *,
+                     expose: bool = False) -> Library:
+    """Register a *.js library.
+
+    :param location: location to the library relative to the base_path (used as the resource identifier, must be URL-safe)
+    :param base_path: base path where your libraries are located
+    :param expose: whether to expose library as an ESM module (exposed modules will NOT be imported)
+    :return: resource identifier to be used in element's `use_library`
     """
-    if isinstance(location, str):
-        return
-    assert location.suffix == '.js' or location.suffix == '.mjs', 'Only JS dependencies are supported.'
-    name = str(location)
-    assert name not in libraries, f'Duplicate js library name {name}'
-    libraries[name] = {'name': name, 'path': base_path / location,  'expose': expose}
-    return name
+    path, key, name, suffix = deconstruct_location(location, base_path)
+    if suffix in {'.js', '.mjs'}:
+        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 "{suffix}"')
+
 
+def deconstruct_location(location: Path, base_path: Path) -> Tuple[Path, str, str, str]:
+    """Deconstruct a location into its parts: full path, relative path, name, suffix."""
+    return base_path / location, str(location), location.name.split('.', 1)[0], location.suffix.lower()
 
-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_components: Set[str] = set()
-    vue_scripts = ''
-    vue_html = ''
-    vue_styles = ''
-    js_imports = ''
-    import_maps = {'imports': {}}
-
-    # Build the importmap structure for exposed libraries.
-    for resource in libraries:
-        if resource not in done_libraries and libraries[resource]['expose']:
-            name = libraries[resource]['name']
-            import_maps['imports'][name] = f'{prefix}/_nicegui/{__version__}/library/{resource}'
-            done_libraries.add(resource)
-    # Build the none optimized component (ie, the vue component).
-    for resource in vue_components:
-        if resource not in done_components:
-            vue_html += f'{vue_components[resource].html}\n'
-            vue_scripts += f'{vue_components[resource].script.replace("Vue.component", "app.component", 1)}\n'
-            vue_styles += f'{vue_components[resource].style}\n'
-            done_components.add(resource)
-
-    # Build the resources associated with the elements.
+    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)
+
+    # build the none-optimized component (i.e. the Vue component)
+    for key, component in vue_components.items():
+        if key not in done_components:
+            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)
+
+    # build the resources associated with the elements
     for element in elements:
-        for resource in element.libraries:
-            if resource in libraries and resource not in done_libraries:
-                if not libraries[resource]['expose']:
-                    js_imports += f'import "{prefix}/_nicegui/{__version__}/library/{resource}";\n'
-                done_libraries.add(resource)
-        for resource in element.components:
-            if resource in js_components and resource not in done_components:
-                name = js_components[resource]['name']
-                var = name.replace('-', '_')
-                js_imports += f'import {{ default as {var} }} from "{prefix}/_nicegui/{__version__}/components/{resource}";\n'
-                js_imports += f'app.component("{name}", {var});\n'
-                done_components.add(resource)
-    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:
+                    js_imports.append(f'import "{prefix}/_nicegui/{__version__}/libraries/{library.key}";')
+                done_libraries.add(library.key)
+        for component in element.components:
+            if component.key not in done_components:
+                js_imports.extend([
+                    f'import {{ default as {component.name} }} from "{prefix}/_nicegui/{__version__}/components/{component.key}";',
+                    f'app.component("{component.tag}", {component.name});',
+                ])
+                done_components.add(component.key)
+    return vue_html, vue_styles, vue_scripts, imports, js_imports

+ 9 - 8
nicegui/element.py

@@ -16,6 +16,7 @@ from .tailwind import Tailwind
 
 if TYPE_CHECKING:
     from .client import Client
+    from .dependencies import JsComponent, Library
 
 PROPS_PATTERN = re.compile(r'([:\w\-]+)(?:=(?:("[^"\\]*(?:\\.[^"\\]*)*")|([\w\-.%:\/]+)))?(?:$|\s)')
 
@@ -41,8 +42,8 @@ class Element(Visibility):
         self._props: Dict[str, Any] = {'key': self.id}  # HACK: workaround for #600 and #898
         self._event_listeners: Dict[str, EventListener] = {}
         self._text: Optional[str] = None
-        self.components: List[str] = []
-        self.libraries: List[str] = []
+        self.components: List[JsComponent] = []
+        self.libraries: List[Library] = []
         self.slots: Dict[str, Slot] = {}
         self.default_slot = self.add_slot('default')
 
@@ -97,8 +98,8 @@ class Element(Visibility):
             'text': self._text,
             'slots': self._collect_slot_dict(),
             'events': [listener.to_dict() for listener in self._event_listeners.values()],
-            'libraries': self.libraries,
-            'components': self.components,
+            'components': [{'key': c.key, 'name': c.name, 'tag': c.tag} for c in self.components],
+            'libraries': [{'key': l.key, 'name': l.name} for l in self.libraries],
         }
 
     @staticmethod
@@ -307,12 +308,12 @@ class Element(Visibility):
         Can be overridden to perform cleanup.
         """
 
-    def use_component(self, name: str) -> Self:
+    def use_component(self, component: JsComponent) -> Self:
         """Register a ``*.js`` Vue component to be used by this element."""
-        self.components.append(name)
+        self.components.append(component)
         return self
 
-    def use_library(self, name: str) -> Self:
+    def use_library(self, library: Library) -> Self:
         """Register a JavaScript library to be used by this element."""
-        self.libraries.append(name)
+        self.libraries.append(library)
         return self

+ 5 - 5
nicegui/elements/aggrid.py

@@ -7,8 +7,8 @@ from ..dependencies import register_library, register_vue_component
 from ..element import Element
 from ..functions.javascript import run_javascript
 
-register_vue_component('aggrid', Path(__file__).parent / 'aggrid.js')
-library_name = register_library(Path('aggrid') / 'ag-grid-community.min.js')
+component = register_vue_component(Path('aggrid.js'))
+library = register_library(Path('aggrid', 'ag-grid-community.min.js'))
 
 
 class AgGrid(Element):
@@ -24,12 +24,12 @@ class AgGrid(Element):
         :param html_columns: list of columns that should be rendered as HTML (default: `[]`)
         :param theme: AG Grid theme (default: 'balham')
         """
-        super().__init__('aggrid')
+        super().__init__(component.tag)
         self._props['options'] = options
         self._props['html_columns'] = html_columns
         self._classes = ['nicegui-aggrid', f'ag-theme-{theme}']
-        self.use_component('aggrid')
-        self.use_library(library_name)
+        self.use_component(component)
+        self.use_library(library)
 
     @staticmethod
     def from_pandas(df: 'pandas.DataFrame', *, theme: str = 'balham') -> AgGrid:

+ 3 - 3
nicegui/elements/audio.py

@@ -6,7 +6,7 @@ from .. import globals
 from ..dependencies import register_vue_component
 from ..element import Element
 
-register_vue_component('audio', Path(__file__).parent / 'audio.js')
+component = register_vue_component(Path('audio.js'))
 
 
 class Audio(Element):
@@ -29,7 +29,7 @@ class Audio(Element):
         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()`.
         """
-        super().__init__('audio')
+        super().__init__(component.tag)
         if Path(src).is_file():
             src = globals.app.add_media_file(local_file=src)
         self._props['src'] = src
@@ -37,7 +37,7 @@ class Audio(Element):
         self._props['autoplay'] = autoplay
         self._props['muted'] = muted
         self._props['loop'] = loop
-        self.use_component('audio')
+        self.use_component(component)
 
         if type:
             url = f'https://github.com/zauberzeug/nicegui/pull/624'

+ 3 - 3
nicegui/elements/chart.py

@@ -4,7 +4,7 @@ from typing import Dict, List
 from ..dependencies import register_library, register_vue_component
 from ..element import Element
 
-register_vue_component('chart', Path(__file__).parent / 'chart.js')
+component = register_vue_component(Path('chart.js'))
 
 core_dependencies: List[Path] = []
 base = Path(__file__).parent / 'lib'
@@ -30,11 +30,11 @@ class Chart(Element):
         :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", ...)
         """
-        super().__init__('chart')
+        super().__init__(component.tag)
         self._props['type'] = type
         self._props['options'] = options
         self._props['extras'] = extras
-        self.use_component('chart')
+        self.use_component(component)
         for dependency in core_dependencies:
             self.use_library(dependency)
         for extra in extras:

+ 3 - 3
nicegui/elements/chat_message.py

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

+ 3 - 3
nicegui/elements/colors.py

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

+ 3 - 3
nicegui/elements/dark_mode.py

@@ -4,7 +4,7 @@ from typing import Optional
 from ..dependencies import register_vue_component
 from .mixins.value_element import ValueElement
 
-register_vue_component('dark_mode', Path(__file__).parent / 'dark_mode.js')
+component = register_vue_component(Path('dark_mode.js'))
 
 
 class DarkMode(ValueElement):
@@ -20,8 +20,8 @@ class DarkMode(ValueElement):
 
         :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__(tag=component.tag, value=value, on_value_change=None)
+        self.use_component(component)
 
     def enable(self) -> None:
         """Enable dark mode."""

+ 3 - 3
nicegui/elements/image.py

@@ -5,7 +5,7 @@ from nicegui.dependencies import register_vue_component
 
 from .mixins.source_element import SourceElement
 
-register_vue_component('image', Path(__file__).parent / 'image.js')
+component = register_vue_component(Path('image.js'))
 
 
 class Image(SourceElement):
@@ -17,5 +17,5 @@ class Image(SourceElement):
 
         :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__(tag=component.tag, source=source)
+        self.use_component(component)

+ 3 - 3
nicegui/elements/input.py

@@ -6,7 +6,7 @@ from .icon import Icon
 from .mixins.disableable_element import DisableableElement
 from .mixins.validation_element import ValidationElement
 
-register_vue_component('nicegui-input', Path(__file__).parent / 'input.js')
+component = register_vue_component(Path('input.js'))
 
 
 class Input(ValidationElement, DisableableElement):
@@ -42,7 +42,7 @@ class Input(ValidationElement, DisableableElement):
         :param autocomplete: optional list of strings for autocompletion
         :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__(tag=component.tag, value=value, on_value_change=on_change, validation=validation)
         if label is not None:
             self._props['label'] = label
         if placeholder is not None:
@@ -59,7 +59,7 @@ class Input(ValidationElement, DisableableElement):
 
         self._props['autocomplete'] = autocomplete or []
 
-        self.use_component('nicegui-input')
+        self.use_component(component)
 
     def set_autocomplete(self, autocomplete: Optional[List[str]]) -> None:
         """Set the autocomplete list."""

+ 3 - 3
nicegui/elements/interactive_image.py

@@ -8,7 +8,7 @@ from ..events import GenericEventArguments, MouseEventArguments, handle_event
 from .mixins.content_element import ContentElement
 from .mixins.source_element import SourceElement
 
-register_vue_component('interactive_image', Path(__file__).parent / 'interactive_image.js')
+component = register_vue_component(Path('interactive_image.js'))
 
 
 class InteractiveImage(SourceElement, ContentElement):
@@ -35,10 +35,10 @@ class InteractiveImage(SourceElement, ContentElement):
         :param events: list of JavaScript events to subscribe to (default: `['click']`)
         :param cross: whether to show crosshairs (default: `False`)
         """
-        super().__init__(tag='interactive_image', source=source, content=content)
+        super().__init__(tag=component.tag, source=source, content=content)
         self._props['events'] = events
         self._props['cross'] = cross
-        self.use_component('interactive_image')
+        self.use_component(component)
 
         def handle_mouse(e: GenericEventArguments) -> None:
             if on_mouse is None:

+ 4 - 4
nicegui/elements/joystick.py

@@ -5,8 +5,8 @@ from ..dependencies import register_library, register_vue_component
 from ..element import Element
 from ..events import GenericEventArguments, JoystickEventArguments, handle_event
 
-register_vue_component(Path('joystick.vue'))
-library_name = register_library(Path('nipplejs') / 'nipplejs.js')
+component = register_vue_component(Path('joystick.vue'))
+library = register_library(Path('nipplejs', 'nipplejs.js'))
 
 
 class Joystick(Element):
@@ -27,8 +27,8 @@ class Joystick(Element):
         :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>`_
         """
-        super().__init__('joystick')
-        self.use_library(library_name)
+        super().__init__('nicegui-joystick')
+        self.use_library(library)
         self._props['options'] = options
         self.active = False
 

+ 3 - 3
nicegui/elements/keyboard.py

@@ -9,7 +9,7 @@ from ..element import Element
 from ..events import (GenericEventArguments, KeyboardAction, KeyboardKey, KeyboardModifiers, KeyEventArguments,
                       handle_event)
 
-register_vue_component('keyboard', Path(__file__).parent / 'keyboard.js')
+component = register_vue_component(Path('keyboard.js'))
 
 
 class Keyboard(Element):
@@ -30,14 +30,14 @@ class Keyboard(Element):
         :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']`)
         """
-        super().__init__('keyboard')
+        super().__init__(component.tag)
         self.key_handler = on_key
         self.active = active
         self._props['events'] = ['keydown', 'keyup']
         self._props['repeating'] = repeating
         self._props['ignore'] = ignore
         self.on('key', self.handle_key)
-        self.use_component('keyboard')
+        self.use_component(component)
 
     def handle_key(self, e: GenericEventArguments) -> None:
         if not self.active:

+ 3 - 3
nicegui/elements/link.py

@@ -6,7 +6,7 @@ from ..dependencies import register_vue_component
 from ..element import Element
 from .mixins.text_element import TextElement
 
-register_vue_component('link', Path(__file__).parent / 'link.js')
+component = register_vue_component(Path('link.js'))
 
 
 class Link(TextElement):
@@ -27,7 +27,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 new_tab: open link in new tab (default: False)
         """
-        super().__init__(tag='link', text=text)
+        super().__init__(tag=component.tag, text=text)
         if isinstance(target, str):
             self._props['href'] = target
         elif isinstance(target, Element):
@@ -36,7 +36,7 @@ class Link(TextElement):
             self._props['href'] = globals.page_routes[target]
         self._props['target'] = '_blank' if new_tab else '_self'
         self._classes = ['nicegui-link']
-        self.use_component('link')
+        self.use_component(component)
 
 
 class LinkTarget(Element):

+ 3 - 3
nicegui/elements/log.py

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

+ 5 - 4
nicegui/elements/markdown.py

@@ -8,9 +8,10 @@ import markdown2
 from pygments.formatters import HtmlFormatter
 
 from ..dependencies import register_vue_component
+from .mermaid import library as mermaid_library
 from .mixins.content_element import ContentElement
 
-register_vue_component('markdown', Path(__file__).parent / 'markdown.js')
+component = register_vue_component(Path('markdown.js'))
 
 
 class Markdown(ContentElement):
@@ -24,13 +25,13 @@ class Markdown(ContentElement):
         :param extras: list of `markdown2 extensions <https://github.com/trentm/python-markdown2/wiki/Extras#implemented-extras>`_ (default: `['fenced-code-blocks', 'tables']`)
         """
         self.extras = extras
-        super().__init__(tag='markdown', content=content)
+        super().__init__(tag=component.tag, content=content)
         self._classes = ['nicegui-markdown']
         self._props['codehilite_css'] = HtmlFormatter(nobackground=True).get_style_defs('.codehilite')
-        self.use_component('markdown')
+        self.use_component(component)
         if 'mermaid' in extras:
             self._props['use_mermaid'] = True
-            self.use_library('mermaid')
+            self.use_library(mermaid_library)
 
     def on_content_change(self, content: str) -> None:
         html = prepare_content(content, extras=' '.join(self.extras))

+ 1 - 1
nicegui/elements/mermaid.js

@@ -1,4 +1,4 @@
-import mermaid from "mermaid/mermaid.esm.min.mjs";
+import mermaid from "mermaid";
 export default {
   template: `<div></div>`,
   mounted() {

+ 5 - 5
nicegui/elements/mermaid.py

@@ -3,8 +3,8 @@ from pathlib import Path
 from ..dependencies import register_library, register_vue_component
 from .mixins.content_element import ContentElement
 
-component_name = register_vue_component(Path('mermaid.js'))
-library_name = register_library(Path('mermaid') / 'mermaid.esm.min.mjs', expose=True)
+component = register_vue_component(Path('mermaid.js'))
+library = register_library(Path('mermaid', 'mermaid.esm.min.mjs'), expose=True)
 extras_path = Path(__file__).parent / 'lib' / 'mermaid'
 for path in extras_path.glob('*.js'):
     register_library(path.relative_to(extras_path.parent))
@@ -21,9 +21,9 @@ class Mermaid(ContentElement):
 
         :param content: the Mermaid content to be displayed
         '''
-        super().__init__(tag='mermaid', content=content)
-        self.use_component(component_name)
-        self.use_library(library_name)
+        super().__init__(tag=component.tag, content=content)
+        self.use_component(component)
+        self.use_library(library)
 
     def on_content_change(self, content: str) -> None:
         self._props[self.CONTENT_PROP] = content.strip()

+ 4 - 4
nicegui/elements/plotly.py

@@ -6,8 +6,8 @@ import plotly.graph_objects as go
 from ..dependencies import register_library, register_vue_component
 from ..element import Element
 
-register_vue_component('plotly', Path(__file__).parent / 'plotly.vue')
-library_name = register_library(Path('plotly') / 'plotly.min.js')
+component = register_vue_component(Path('plotly.vue'))
+library = register_library(Path('plotly', 'plotly.min.js'))
 
 
 class Plotly(Element):
@@ -27,8 +27,8 @@ class Plotly(Element):
         :param figure: Plotly figure to be rendered. Can be either a `go.Figure` instance, or
                        a `dict` object with keys `data`, `layout`, `config` (optional).
         """
-        super().__init__('plotly')
-        self.use_library(library_name)
+        super().__init__(component.tag)
+        self.use_library(library)
 
         self.figure = figure
         self.update()

+ 3 - 3
nicegui/elements/query.py

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

+ 12 - 13
nicegui/elements/scene.py

@@ -9,15 +9,14 @@ from ..events import GenericEventArguments, SceneClickEventArguments, SceneClick
 from ..helpers import KWONLY_SLOTS
 from .scene_object3d import Object3D
 
-register_vue_component('scene', Path(__file__).parent / 'scene.js')
-lib = Path('three')
-library_names = [
-    register_library(lib / 'three.module.js', expose=True),
-    register_library(lib / 'modules' / 'CSS2DRenderer.js', expose=True),
-    register_library(lib / 'modules' / 'CSS3DRenderer.js', expose=True),
-    register_library(lib / 'modules' / 'OrbitControls.js', expose=True),
-    register_library(lib / 'modules' / 'STLLoader.js', expose=True),
-    register_library(lib / 'tween' / 'tween.umd.js'),
+component = register_vue_component(Path('scene.js'))
+libraries = [
+    register_library(Path('three', 'three.module.js'), expose=True),
+    register_library(Path('three', 'modules', 'CSS2DRenderer.js'), expose=True),
+    register_library(Path('three', 'modules', 'CSS3DRenderer.js'), expose=True),
+    register_library(Path('three', 'modules', 'OrbitControls.js'), expose=True),
+    register_library(Path('three', 'modules', 'STLLoader.js'), expose=True),
+    register_library(Path('tween', 'tween.umd.js')),
 ]
 
 
@@ -74,7 +73,7 @@ class Scene(Element):
         :param grid: whether to display a grid
         :param on_click: callback to execute when a 3d object is clicked
         """
-        super().__init__('scene')
+        super().__init__(component.tag)
         self._props['width'] = width
         self._props['height'] = height
         self._props['grid'] = grid
@@ -85,9 +84,9 @@ class Scene(Element):
         self.is_initialized = False
         self.on('init', self.handle_init)
         self.on('click3d', self.handle_click)
-        self.use_component('scene')
-        for library_name in library_names:
-            self.use_library(library_name)
+        self.use_component(component)
+        for library in libraries:
+            self.use_library(library)
 
     def handle_init(self, e: GenericEventArguments) -> None:
         self.is_initialized = True

+ 3 - 3
nicegui/elements/select.py

@@ -8,7 +8,7 @@ from ..events import GenericEventArguments
 from .choice_element import ChoiceElement
 from .mixins.disableable_element import DisableableElement
 
-register_vue_component('select', Path(__file__).parent / 'select.js')
+component = register_vue_component(Path('select.js'))
 
 
 class Select(ChoiceElement, DisableableElement):
@@ -40,8 +40,8 @@ class Select(ChoiceElement, DisableableElement):
                 value = []
             elif not isinstance(value, list):
                 value = [value]
-        super().__init__(tag='select', options=options, value=value, on_change=on_change)
-        self.use_component('select')
+        super().__init__(tag=component.tag, options=options, value=value, on_change=on_change)
+        self.use_component(component)
         if label is not None:
             self._props['label'] = label
         if with_input:

+ 3 - 3
nicegui/elements/table.py

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

+ 3 - 3
nicegui/elements/upload.py

@@ -9,7 +9,7 @@ from ..events import EventArguments, UploadEventArguments, handle_event
 from ..nicegui import app
 from .mixins.disableable_element import DisableableElement
 
-register_vue_component('upload', Path(__file__).parent / 'upload.js')
+component = register_vue_component(Path('upload.js'))
 
 
 class Upload(DisableableElement):
@@ -37,8 +37,8 @@ class Upload(DisableableElement):
         :param label: label for the uploader (default: `''`)
         :param auto_upload: automatically upload files when they are selected (default: `False`)
         """
-        super().__init__(tag='upload')
-        self.use_component('upload')
+        super().__init__(tag=component.tag)
+        self.use_component(component)
         self._props['multiple'] = multiple
         self._props['label'] = label
         self._props['auto-upload'] = auto_upload

+ 3 - 3
nicegui/elements/video.py

@@ -6,7 +6,7 @@ from .. import globals
 from ..dependencies import register_vue_component
 from ..element import Element
 
-register_vue_component('video', Path(__file__).parent / 'video.js')
+component = register_vue_component(Path('video.js'))
 
 
 class Video(Element):
@@ -29,7 +29,7 @@ class Video(Element):
         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()`.
         """
-        super().__init__('video')
+        super().__init__(component.tag)
         if Path(src).is_file():
             src = globals.app.add_media_file(local_file=src)
         self._props['src'] = src
@@ -37,7 +37,7 @@ class Video(Element):
         self._props['autoplay'] = autoplay
         self._props['muted'] = muted
         self._props['loop'] = loop
-        self.use_component('video')
+        self.use_component(component)
 
         if type:
             url = f'https://github.com/zauberzeug/nicegui/pull/624'

+ 2 - 3
nicegui/functions/refreshable.py

@@ -9,7 +9,7 @@ from ..dependencies import register_vue_component
 from ..element import Element
 from ..helpers import KWONLY_SLOTS, is_coroutine_function
 
-register_vue_component('refreshable', Path(__file__).parent / 'refreshable.js')
+component = register_vue_component(Path('refreshable.js'), base_path=Path(__file__).parent.parent / 'functions')
 
 
 @dataclass(**KWONLY_SLOTS)
@@ -55,8 +55,7 @@ class refreshable:
 
     def __call__(self, *args: Any, **kwargs: Any) -> Union[None, Awaitable]:
         self.prune()
-        container = Element('refreshable')
-        container.use_component('refreshable')
+        container = Element(component.tag).use_component(component)
         target = RefreshableTarget(container=container, instance=self.instance, args=args, kwargs=kwargs)
         self.targets.append(target)
         return target.run(self.func)

+ 14 - 20
nicegui/nicegui.py

@@ -44,26 +44,20 @@ def index(request: Request) -> Response:
     return globals.index_client.build_response(request)
 
 
-@app.get(f'/_nicegui/{__version__}' + '/library/{name:path}')
-def get_dependencies(name: str):
-    if name in libraries and libraries[name]['path'].exists():
-        return FileResponse(
-            libraries[name]['path'],
-            media_type='text/javascript',
-            headers={'Cache-Control': 'public, max-age=3600'}
-        )
-    raise HTTPException(status_code=404, detail=f'dependency "{name}" not found')
-
-
-@app.get(f'/_nicegui/{__version__}' + '/components/{resource:path}')
-def get_components(resource: str):
-    if resource in js_components and js_components[resource]['path'].exists():
-        return FileResponse(
-            js_components[resource]['path'],
-            media_type='text/javascript',
-            headers={'Cache-Control': 'public, max-age=3600'},
-        )
-    raise HTTPException(status_code=404, detail=f'library "{resource}" 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')

+ 14 - 11
nicegui/templates/index.html

@@ -19,6 +19,9 @@
     <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.{{ language }}.umd.prod.js"></script>
+    <script type="importmap">
+      {"imports": {{ imports | safe }}}
+    </script>
 
     <!-- NOTE: force Prettier to keep the line break -->
     {{ body_html | safe }}
@@ -102,8 +105,8 @@
         }
 
         // @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));
+        element.components.forEach((component) => loaded_components.add(component));
+        element.libraries.forEach((library) => loaded_libraries.add(library));
 
         const props = {
           id: 'c' + element.id,
@@ -150,7 +153,7 @@
                 props: { props: { type: Object, default: {} } },
                 template: data.template,
               }, {
-                props: props
+                props: props,
               }));
             }
             const children = data.ids.map(id => renderRecursively(elements, id));
@@ -187,17 +190,17 @@
       }
 
       async function loadDependencies(element) {
-        for (const name of element['libraries']) {
-          if (loaded_libraries.has(name)) continue;
-          await import(`{{ prefix | safe }}/_nicegui/{{version}}/library/${name}`);
-          loaded_libraries.add(name);
-        }
-        for (const name of element['components']) {
+        for (const {name, key, tag} 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);
+          const component = (await import(`{{ prefix | safe }}/_nicegui/{{version}}/components/${key}`)).default;
+          app = app.component(tag, component);
           loaded_components.add(name);
         }
+        for (const {name, key} of element.libraries) {
+          if (loaded_libraries.has(name)) continue;
+          await import(`{{ prefix | safe }}/_nicegui/{{version}}/libraries/${key}`);
+          loaded_libraries.add(name);
+        }
       }
 
       let app = Vue.createApp({

+ 3 - 3
website/intersection_observer.py

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