1
0
Эх сурвалжийг харах

Merge commit '4c5745b1f9c46ef8a40dfa98dd278e8fcc1185dc' into on-air

Rodja Trappe 1 жил өмнө
parent
commit
cb1eff86fa

+ 2 - 7
examples/custom_vue_component/counter.py

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

+ 2 - 8
examples/map/leaflet.py

@@ -1,18 +1,12 @@
-from pathlib import Path
 from typing import Tuple
 
 from nicegui import ui
-from nicegui.dependencies import register_vue_component
-from nicegui.element import Element
 
-component = register_vue_component(Path('leaflet.js'), base_path=Path(__file__).parent)
 
-
-class leaflet(Element):
+class leaflet(ui.element, component='leaflet.js'):
 
     def __init__(self) -> None:
-        super().__init__(component.tag)
-        self.use_component(component)
+        super().__init__()
         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>')
 

+ 4 - 6
examples/single_page_app/router.py

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

+ 12 - 6
nicegui/dependencies.py

@@ -1,13 +1,17 @@
+from __future__ import annotations
+
 from dataclasses import dataclass
 from pathlib import Path
-from typing import Dict, List, Set, Tuple
+from typing import TYPE_CHECKING, Dict, List, Set, Tuple
 
 import vbuild
 
 from . import __version__
-from .element import Element
 from .helpers import KWONLY_SLOTS
 
+if TYPE_CHECKING:
+    from .element import Element
+
 
 @dataclass(**KWONLY_SLOTS)
 class Component:
@@ -53,7 +57,7 @@ def register_vue_component(location: Path, base_path: Path = Path(__file__).pare
 
     :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`
+    :return: registered component
     """
     path, key, name, suffix = deconstruct_location(location, base_path)
     if suffix == '.vue':
@@ -75,7 +79,7 @@ def register_library(location: Path, base_path: Path = Path(__file__).parent / '
     :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`
+    :return: registered library
     """
     path, key, name, suffix = deconstruct_location(location, base_path)
     if suffix in {'.js', '.mjs'}:
@@ -87,7 +91,9 @@ def register_library(location: Path, base_path: Path = Path(__file__).parent / '
 
 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()
+    abs_path = location if location.is_absolute() else base_path / location
+    rel_path = location if not location.is_absolute() else location.relative_to(base_path)
+    return abs_path, str(rel_path), location.name.split('.', 1)[0], location.suffix.lower()
 
 
 def generate_resources(prefix: str, elements: List[Element]) -> Tuple[List[str],
@@ -126,7 +132,7 @@ def generate_resources(prefix: str, elements: List[Element]) -> Tuple[List[str],
                     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:
+            if component.key not in done_components and component.path.suffix.lower() == '.js':
                 js_imports.extend([
                     f'import {{ default as {component.name} }} from "{prefix}/_nicegui/{__version__}/components/{component.key}";',
                     f'app.component("{component.tag}", {component.name});',

+ 22 - 15
nicegui/element.py

@@ -1,7 +1,9 @@
 from __future__ import annotations
 
+import inspect
 import re
 from copy import deepcopy
+from pathlib import Path
 from typing import TYPE_CHECKING, Any, Callable, Dict, Iterator, List, Optional, Union
 
 from typing_extensions import Self
@@ -9,6 +11,7 @@ from typing_extensions import Self
 from nicegui import json
 
 from . import binding, events, globals, outbox, storage
+from .dependencies import JsComponent, Library, register_library, register_vue_component
 from .elements.mixins.visibility import Visibility
 from .event_listener import EventListener
 from .slot import Slot
@@ -16,14 +19,17 @@ from .tailwind import Tailwind
 
 if TYPE_CHECKING:
     from .client import Client
-    from .dependencies import JsComponent, Library
 
 PROPS_PATTERN = re.compile(r'([:\w\-]+)(?:=(?:("[^"\\]*(?:\\.[^"\\]*)*")|([\w\-.%:\/]+)))?(?:$|\s)')
 
 
 class Element(Visibility):
+    components: List[JsComponent] = []
+    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
 
         This class is the base class for all other UI elements.
@@ -36,14 +42,12 @@ class Element(Visibility):
         self.client = _client or globals.get_client()
         self.id = self.client.next_element_id
         self.client.next_element_id += 1
-        self.tag = tag
+        self.tag = tag if tag else self.components[0].tag if self.components else 'div'
         self._classes: List[str] = []
         self._style: Dict[str, str] = {}
         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[JsComponent] = []
-        self.libraries: List[Library] = []
         self.slots: Dict[str, Slot] = {}
         self.default_slot = self.add_slot('default')
 
@@ -60,6 +64,19 @@ class Element(Visibility):
         if self.parent_slot:
             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
+        cls.components = [register_vue_component(Path(component), base)] if component else []
+        cls.libraries = [register_library(Path(library), base) for library in libraries]
+        cls.extra_libraries = [register_library(Path(library), base) for library in extra_libraries]
+        cls.exposed_libraries = [register_library(Path(library), base, expose=True) for library in exposed_libraries]
+
     def add_slot(self, name: str, template: Optional[str] = None) -> Slot:
         """Add a slot to the element.
 
@@ -307,13 +324,3 @@ class Element(Visibility):
 
         Can be overridden to perform cleanup.
         """
-
-    def use_component(self, component: JsComponent) -> Self:
-        """Register a ``*.js`` Vue component to be used by this element."""
-        self.components.append(component)
-        return self
-
-    def use_library(self, library: Library) -> Self:
-        """Register a JavaScript library to be used by this element."""
-        self.libraries.append(library)
-        return self

+ 2 - 9
nicegui/elements/aggrid.py

@@ -1,17 +1,12 @@
 from __future__ import annotations
 
-from pathlib import Path
 from typing import Dict, List, Optional, cast
 
-from ..dependencies import register_library, register_vue_component
 from ..element import Element
 from ..functions.javascript import run_javascript
 
-component = register_vue_component(Path('aggrid.js'))
-library = register_library(Path('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:
         """AG Grid
@@ -24,12 +19,10 @@ 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__(component.tag)
+        super().__init__()
         self._props['options'] = options
         self._props['html_columns'] = html_columns
         self._classes = ['nicegui-aggrid', f'ag-theme-{theme}']
-        self.use_component(component)
-        self.use_library(library)
 
     @staticmethod
     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 .. import globals
-from ..dependencies import register_vue_component
 from ..element import Element
 
-component = register_vue_component(Path('audio.js'))
 
-
-class Audio(Element):
+class Audio(Element, component='audio.js'):
 
     def __init__(self, src: Union[str, Path], *,
                  controls: bool = True,
@@ -29,7 +26,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__(component.tag)
+        super().__init__()
         if Path(src).is_file():
             src = globals.app.add_media_file(local_file=src)
         self._props['src'] = src
@@ -37,7 +34,6 @@ class Audio(Element):
         self._props['autoplay'] = autoplay
         self._props['muted'] = muted
         self._props['loop'] = loop
-        self.use_component(component)
 
         if type:
             url = f'https://github.com/zauberzeug/nicegui/pull/624'

+ 6 - 16
nicegui/elements/chart.py

@@ -1,20 +1,14 @@
 from pathlib import Path
 from typing import Dict, List
 
-from ..dependencies import register_library, register_vue_component
 from ..element import Element
 
-component = register_vue_component(Path('chart.js'))
+base = Path(__file__).parent
+libraries = [p.relative_to(base) for p in sorted((base / 'lib' / 'highcharts').glob('*.js'), key=lambda p: p.stem)]
+modules = {p.stem: p.relative_to(base) for p in sorted((base / 'lib' / 'highcharts' / 'modules').glob('*.js'))}
 
-core_dependencies: List[Path] = []
-base = Path(__file__).parent / 'lib'
-for path in sorted((base / 'highcharts').glob('*.js'), key=lambda p: p.stem):
-    core_dependencies.append(register_library(path.relative_to(base)))
-for path in sorted((base / 'highcharts' / 'modules').glob('*.js'), key=lambda p: p.stem):
-    register_library(path.relative_to(base))
 
-
-class Chart(Element):
+class Chart(Element, component='chart.js', libraries=libraries, extra_libraries=list(modules.values())):
 
     def __init__(self, options: Dict, *, type: str = 'chart', extras: List[str] = []) -> None:
         """Chart
@@ -30,15 +24,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__(component.tag)
+        super().__init__()
         self._props['type'] = type
         self._props['options'] = options
         self._props['extras'] = extras
-        self.use_component(component)
-        for dependency in core_dependencies:
-            self.use_library(dependency)
-        for extra in extras:
-            self.use_library(f'highcharts/modules/{extra}.js')
+        self.libraries.extend(library for library in self.extra_libraries if library.path.stem in extras)
 
     @property
     def options(self) -> Dict:

+ 2 - 7
nicegui/elements/chat_message.py

@@ -1,14 +1,10 @@
 import html
-from pathlib import Path
 from typing import List, Optional, Union
 
-from ..dependencies import register_vue_component
 from ..element import Element
 
-component = register_vue_component(Path('chat_message.js'))
 
-
-class ChatMessage(Element):
+class ChatMessage(Element, component='chat_message.js'):
 
     def __init__(self,
                  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 text_html: render text as HTML (default: False)
         """
-        super().__init__(component.tag)
-        self.use_component(component)
+        super().__init__()
 
         if isinstance(text, str):
             text = [text]

+ 1 - 1
nicegui/elements/choice_element.py

@@ -6,7 +6,7 @@ from .mixins.value_element import ValueElement
 class ChoiceElement(ValueElement):
 
     def __init__(self, *,
-                 tag: str,
+                 tag: Optional[str] = None,
                  options: Union[List, Dict],
                  value: Any,
                  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
 
-component = register_vue_component(Path('colors.js'))
-
 
-class Colors(Element):
+class Colors(Element, component='colors.js'):
 
     def __init__(self, *,
                  primary='#5898d4',
@@ -21,8 +16,7 @@ class Colors(Element):
 
         Sets the main colors (primary, secondary, accent, ...) used by `Quasar <https://quasar.dev/>`_.
         """
-        super().__init__(component.tag)
-        self.use_component(component)
+        super().__init__()
         self._props['primary'] = primary
         self._props['secondary'] = secondary
         self._props['accent'] = accent

+ 2 - 7
nicegui/elements/dark_mode.py

@@ -1,13 +1,9 @@
-from pathlib import Path
 from typing import Optional
 
-from ..dependencies import register_vue_component
 from .mixins.value_element import ValueElement
 
-component = register_vue_component(Path('dark_mode.js'))
 
-
-class DarkMode(ValueElement):
+class DarkMode(ValueElement, component='dark_mode.js'):
     VALUE_PROP = 'value'
 
     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.
         """
-        super().__init__(tag=component.tag, value=value, on_value_change=None)
-        self.use_component(component)
+        super().__init__(value=value, on_value_change=None)
 
     def enable(self) -> None:
         """Enable dark mode."""

+ 2 - 7
nicegui/elements/image.py

@@ -1,14 +1,10 @@
 from pathlib import Path
 from typing import Union
 
-from nicegui.dependencies import register_vue_component
-
 from .mixins.source_element import SourceElement
 
-component = register_vue_component(Path('image.js'))
-
 
-class Image(SourceElement):
+class Image(SourceElement, component='image.js'):
 
     def __init__(self, source: Union[str, Path] = '') -> None:
         """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
         """
-        super().__init__(tag=component.tag, source=source)
-        self.use_component(component)
+        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 ..dependencies import register_vue_component
 from .icon import Icon
 from .mixins.disableable_element import DisableableElement
 from .mixins.validation_element import ValidationElement
 
-component = register_vue_component(Path('input.js'))
 
-
-class Input(ValidationElement, DisableableElement):
+class Input(ValidationElement, DisableableElement, component='input.js'):
     VALUE_PROP: str = 'value'
     LOOPBACK = False
 
@@ -42,7 +38,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=component.tag, 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:
             self._props['label'] = label
         if placeholder is not None:
@@ -59,8 +55,6 @@ class Input(ValidationElement, DisableableElement):
 
         self._props['autocomplete'] = autocomplete or []
 
-        self.use_component(component)
-
     def set_autocomplete(self, autocomplete: Optional[List[str]]) -> None:
         """Set the autocomplete list."""
         self._props['autocomplete'] = autocomplete

+ 2 - 6
nicegui/elements/interactive_image.py

@@ -3,15 +3,12 @@ from __future__ import annotations
 from pathlib import Path
 from typing import Any, Callable, List, Optional, Union
 
-from ..dependencies import register_vue_component
 from ..events import GenericEventArguments, MouseEventArguments, handle_event
 from .mixins.content_element import ContentElement
 from .mixins.source_element import SourceElement
 
-component = register_vue_component(Path('interactive_image.js'))
 
-
-class InteractiveImage(SourceElement, ContentElement):
+class InteractiveImage(SourceElement, ContentElement, component='interactive_image.js'):
     CONTENT_PROP = 'content'
 
     def __init__(self,
@@ -35,10 +32,9 @@ 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=component.tag, source=source, content=content)
+        super().__init__(source=source, content=content)
         self._props['events'] = events
         self._props['cross'] = cross
-        self.use_component(component)
 
         def handle_mouse(e: GenericEventArguments) -> 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 ..dependencies import register_library, register_vue_component
 from ..element import Element
 from ..events import GenericEventArguments, JoystickEventArguments, handle_event
 
-component = register_vue_component(Path('joystick.vue'))
-library = register_library(Path('nipplejs', 'nipplejs.js'))
 
-
-class Joystick(Element):
+class Joystick(Element, component='joystick.vue', libraries=['lib/nipplejs/nipplejs.js']):
 
     def __init__(self, *,
                  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 options: arguments like `color` which should be passed to the `underlying nipple.js library <https://github.com/yoannmoinet/nipplejs#options>`_
         """
-        super().__init__('nicegui-joystick')
-        self.use_library(library)
+        super().__init__()
         self._props['options'] = options
         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_extensions import Literal
 
 from ..binding import BindableProperty
-from ..dependencies import register_vue_component
 from ..element import Element
 from ..events import (GenericEventArguments, KeyboardAction, KeyboardKey, KeyboardModifiers, KeyEventArguments,
                       handle_event)
 
-component = register_vue_component(Path('keyboard.js'))
 
-
-class Keyboard(Element):
+class Keyboard(Element, component='keyboard.js'):
     active = BindableProperty()
 
     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 ignore: ignore keys when one of these element types is focussed (default: `['input', 'select', 'button', 'textarea']`)
         """
-        super().__init__(component.tag)
+        super().__init__()
         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(component)
 
     def handle_key(self, e: GenericEventArguments) -> None:
         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 .. import globals
-from ..dependencies import register_vue_component
 from ..element import Element
 from .mixins.text_element import TextElement
 
-component = register_vue_component(Path('link.js'))
 
-
-class Link(TextElement):
+class Link(TextElement, component='link.js'):
 
     def __init__(self,
                  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 new_tab: open link in new tab (default: False)
         """
-        super().__init__(tag=component.tag, text=text)
+        super().__init__(text=text)
         if isinstance(target, str):
             self._props['href'] = target
         elif isinstance(target, Element):
@@ -36,7 +32,6 @@ 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(component)
 
 
 class LinkTarget(Element):

+ 2 - 7
nicegui/elements/log.py

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

+ 4 - 9
nicegui/elements/markdown.py

@@ -1,20 +1,16 @@
 import os
 import re
 from functools import lru_cache
-from pathlib import Path
 from typing import List
 
 import markdown2
 from pygments.formatters import HtmlFormatter
 
-from ..dependencies import register_vue_component
-from .mermaid import library as mermaid_library
+from .mermaid import Mermaid
 from .mixins.content_element import ContentElement
 
-component = register_vue_component(Path('markdown.js'))
 
-
-class Markdown(ContentElement):
+class Markdown(ContentElement, component='markdown.js'):
 
     def __init__(self, content: str = '', *, extras: List[str] = ['fenced-code-blocks', 'tables']) -> None:
         """Markdown Element
@@ -25,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']`)
         """
         self.extras = extras
-        super().__init__(tag=component.tag, content=content)
+        super().__init__(content=content)
         self._classes = ['nicegui-markdown']
         self._props['codehilite_css'] = HtmlFormatter(nobackground=True).get_style_defs('.codehilite')
-        self.use_component(component)
         if 'mermaid' in extras:
             self._props['use_mermaid'] = True
-            self.use_library(mermaid_library)
+            self.libraries.append(Mermaid.exposed_libraries[0])
 
     def on_content_change(self, content: str) -> None:
         html = prepare_content(content, extras=' '.join(self.extras))

+ 6 - 10
nicegui/elements/mermaid.py

@@ -1,16 +1,14 @@
 from pathlib import Path
 
-from ..dependencies import register_library, register_vue_component
 from .mixins.content_element import ContentElement
 
-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))
+base = Path(__file__).parent
 
 
-class Mermaid(ContentElement):
+class Mermaid(ContentElement,
+              component='mermaid.js',
+              exposed_libraries=['lib/mermaid/mermaid.esm.min.mjs'],
+              extra_libraries=[p.relative_to(base) for p in (base / 'lib' / 'mermaid').glob('*.js')]):
     CONTENT_PROP = 'content'
 
     def __init__(self, content: str) -> None:
@@ -21,9 +19,7 @@ class Mermaid(ContentElement):
 
         :param content: the Mermaid content to be displayed
         '''
-        super().__init__(tag=component.tag, content=content)
-        self.use_component(component)
-        self.use_library(library)
+        super().__init__(content=content)
 
     def on_content_change(self, content: str) -> None:
         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
 
 import plotly.graph_objects as go
 
-from ..dependencies import register_library, register_vue_component
 from ..element import Element
 
-component = register_vue_component(Path('plotly.vue'))
-library = register_library(Path('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:
         """Plotly Element
@@ -27,8 +22,7 @@ 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__(component.tag)
-        self.use_library(library)
+        super().__init__()
 
         self.figure = figure
         self.update()

+ 2 - 7
nicegui/elements/query.py

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

+ 11 - 17
nicegui/elements/scene.py

@@ -1,24 +1,12 @@
 from dataclasses import dataclass
-from pathlib import Path
 from typing import Any, Callable, Dict, List, Optional, Union
 
 from .. import binding, globals
-from ..dependencies import register_library, register_vue_component
 from ..element import Element
 from ..events import GenericEventArguments, SceneClickEventArguments, SceneClickHit, handle_event
 from ..helpers import KWONLY_SLOTS
 from .scene_object3d import Object3D
 
-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')),
-]
-
 
 @dataclass(**KWONLY_SLOTS)
 class SceneCamera:
@@ -38,7 +26,16 @@ class SceneObject:
     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 Curve as curve
     from .scene_objects import Cylinder as cylinder
@@ -73,7 +70,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__(component.tag)
+        super().__init__()
         self._props['width'] = width
         self._props['height'] = height
         self._props['grid'] = grid
@@ -84,9 +81,6 @@ class Scene(Element):
         self.is_initialized = False
         self.on('init', self.handle_init)
         self.on('click3d', self.handle_click)
-        self.use_component(component)
-        for library in libraries:
-            self.use_library(library)
 
     def handle_init(self, e: GenericEventArguments) -> None:
         self.is_initialized = True

+ 2 - 7
nicegui/elements/select.py

@@ -1,17 +1,13 @@
 import re
 from copy import deepcopy
-from pathlib import Path
 from typing import Any, Callable, Dict, List, Optional, Union
 
-from ..dependencies import register_vue_component
 from ..events import GenericEventArguments
 from .choice_element import ChoiceElement
 from .mixins.disableable_element import DisableableElement
 
-component = register_vue_component(Path('select.js'))
 
-
-class Select(ChoiceElement, DisableableElement):
+class Select(ChoiceElement, DisableableElement, component='select.js'):
 
     def __init__(self,
                  options: Union[List, Dict], *,
@@ -40,8 +36,7 @@ class Select(ChoiceElement, DisableableElement):
                 value = []
             elif not isinstance(value, list):
                 value = [value]
-        super().__init__(tag=component.tag, options=options, value=value, on_change=on_change)
-        self.use_component(component)
+        super().__init__(options=options, value=value, on_change=on_change)
         if label is not None:
             self._props['label'] = label
         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_extensions import Literal
 
-from ..dependencies import register_vue_component
 from ..element import Element
 from ..events import GenericEventArguments, TableSelectionEventArguments, handle_event
 from .mixins.filter_element import FilterElement
 
-component = register_vue_component(Path('table.js'))
 
-
-class Table(FilterElement):
+class Table(FilterElement, component='table.js'):
 
     def __init__(self,
                  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.
         """
-        super().__init__(tag=component.tag)
+        super().__init__()
 
         self.rows = rows
         self.row_key = row_key
@@ -63,8 +59,6 @@ class Table(FilterElement):
             handle_event(on_select, arguments)
         self.on('selection', handle_selection, ['added', 'rows', 'keys'])
 
-        self.use_component(component)
-
     def add_rows(self, *rows: Dict) -> None:
         """Add rows to the table."""
         self.rows.extend(rows)

+ 2 - 7
nicegui/elements/upload.py

@@ -1,18 +1,14 @@
-from pathlib import Path
 from typing import Any, Callable, Dict, Optional
 
 from fastapi import Request
 from starlette.datastructures import UploadFile
 
-from ..dependencies import register_vue_component
 from ..events import EventArguments, UploadEventArguments, handle_event
 from ..nicegui import app
 from .mixins.disableable_element import DisableableElement
 
-component = register_vue_component(Path('upload.js'))
 
-
-class Upload(DisableableElement):
+class Upload(DisableableElement, component='upload.js'):
 
     def __init__(self, *,
                  multiple: bool = False,
@@ -37,8 +33,7 @@ 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=component.tag)
-        self.use_component(component)
+        super().__init__()
         self._props['multiple'] = multiple
         self._props['label'] = label
         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 .. import globals
-from ..dependencies import register_vue_component
 from ..element import Element
 
-component = register_vue_component(Path('video.js'))
 
-
-class Video(Element):
+class Video(Element, component='video.js'):
 
     def __init__(self, src: Union[str, Path], *,
                  controls: bool = True,
@@ -29,7 +26,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__(component.tag)
+        super().__init__()
         if Path(src).is_file():
             src = globals.app.add_media_file(local_file=src)
         self._props['src'] = src
@@ -37,7 +34,6 @@ class Video(Element):
         self._props['autoplay'] = autoplay
         self._props['muted'] = muted
         self._props['loop'] = loop
-        self.use_component(component)
 
         if type:
             url = f'https://github.com/zauberzeug/nicegui/pull/624'

+ 5 - 6
nicegui/functions/refreshable.py

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

+ 2 - 2
nicegui/templates/index.html

@@ -105,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.name));
+        element.libraries.forEach((library) => loaded_libraries.add(library.name));
 
         const props = {
           id: 'c' + element.id,

+ 2 - 7
website/intersection_observer.py

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