From 454b5798cb717eb0dd8001bff7aa53e4077bc7ee Mon Sep 17 00:00:00 2001 From: Rodja Trappe Date: Thu, 6 Jul 2023 14:58:45 +0200 Subject: [PATCH 01/24] using hierachical path names to identify js dependencies --- nicegui/dependencies.py | 21 +++++++++++++-------- nicegui/elements/aggrid.py | 4 ++-- nicegui/elements/chart.py | 14 +++++++------- nicegui/elements/joystick.py | 4 ++-- nicegui/elements/mermaid.py | 4 ++-- nicegui/elements/plotly.py | 4 ++-- nicegui/elements/scene.py | 23 +++++++++++------------ nicegui/nicegui.py | 13 +++++++------ nicegui/templates/index.html | 2 +- 9 files changed, 47 insertions(+), 42 deletions(-) diff --git a/nicegui/dependencies.py b/nicegui/dependencies.py index 1b140d281..8fcde8afe 100644 --- a/nicegui/dependencies.py +++ b/nicegui/dependencies.py @@ -31,15 +31,21 @@ def register_vue_component(name: str, path: Path) -> None: js_components[name] = {'name': name, 'path': path} -def register_library(name: str, path: Path, *, expose: bool = False) -> None: +def register_library( + location: Path, base_path: Path = Path(__file__).parent / 'elements' / 'lib', *, expose: bool = False) -> str: """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 location: the location to the library you want to register relative to the base_path. This is also used as the resource identifier (used in element's `use_library`) 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 path.suffix == '.js' or path.suffix == '.mjs', 'Only JS dependencies are supported.' - libraries[name] = {'name': name, 'path': path, 'expose': expose} + if isinstance(location, str): + return + assert location.suffix == '.js' or location.suffix == '.mjs', 'Only JS dependencies are supported.' + name = str(location) + libraries[name] = {'name': name, 'path': base_path / location, 'expose': expose} + return name def generate_resources(prefix: str, elements: List[Element]) -> Tuple[str, str, str, str, str]: @@ -55,7 +61,7 @@ def generate_resources(prefix: str, elements: List[Element]) -> Tuple[str, str, 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' + import_maps['imports'][name] = f'{prefix}/_nicegui/{__version__}/library/{key}' done_libraries.add(key) # Build the none optimized component (ie, the vue component). for key in vue_components: @@ -70,7 +76,7 @@ def generate_resources(prefix: str, elements: List[Element]) -> Tuple[str, str, 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' + js_imports += f'import "{prefix}/_nicegui/{__version__}/library/{key}";\n' done_libraries.add(key) for key in element.components: if key in js_components and key not in done_components: @@ -79,7 +85,6 @@ def generate_resources(prefix: str, elements: List[Element]) -> Tuple[str, str, 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'' import_maps = f'' return vue_html, vue_styles, vue_scripts, import_maps, js_imports diff --git a/nicegui/elements/aggrid.py b/nicegui/elements/aggrid.py index f275732c2..81e3df329 100644 --- a/nicegui/elements/aggrid.py +++ b/nicegui/elements/aggrid.py @@ -8,7 +8,7 @@ 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') +library_name = register_library(Path('aggrid') / 'ag-grid-community.min.js') class AgGrid(Element): @@ -29,7 +29,7 @@ def __init__(self, options: Dict, *, html_columns: List[int] = [], theme: str = self._props['html_columns'] = html_columns self._classes = ['nicegui-aggrid', f'ag-theme-{theme}'] self.use_component('aggrid') - self.use_library('aggrid') + self.use_library(library_name) @staticmethod def from_pandas(df: 'pandas.DataFrame', *, theme: str = 'balham') -> AgGrid: diff --git a/nicegui/elements/chart.py b/nicegui/elements/chart.py index 0ebd0f424..0dfebf72b 100644 --- a/nicegui/elements/chart.py +++ b/nicegui/elements/chart.py @@ -7,11 +7,11 @@ 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) +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): @@ -36,9 +36,9 @@ def __init__(self, options: Dict, *, type: str = 'chart', extras: List[str] = [] self._props['extras'] = extras self.use_component('chart') for dependency in core_dependencies: - self.use_library(dependency.stem) + self.use_library(dependency) for extra in extras: - self.use_library(extra) + self.use_library(f'highcharts/modules/{extra}.js') @property def options(self) -> Dict: diff --git a/nicegui/elements/joystick.py b/nicegui/elements/joystick.py index 4ba40ff0e..3dbc9b10e 100644 --- a/nicegui/elements/joystick.py +++ b/nicegui/elements/joystick.py @@ -6,7 +6,7 @@ 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') +library_name = register_library(Path('nipplejs') / 'nipplejs.js') class Joystick(Element): @@ -28,7 +28,7 @@ def __init__(self, *, :param options: arguments like `color` which should be passed to the `underlying nipple.js library `_ """ super().__init__('joystick') - self.use_library('nipplejs') + self.use_library(library_name) self._props['options'] = options self.active = False diff --git a/nicegui/elements/mermaid.py b/nicegui/elements/mermaid.py index f2fa9a97f..390e54cd9 100644 --- a/nicegui/elements/mermaid.py +++ b/nicegui/elements/mermaid.py @@ -4,7 +4,7 @@ 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) +library_name = register_library(Path('mermaid') / 'mermaid.esm.min.mjs', expose=True) class Mermaid(ContentElement): @@ -20,7 +20,7 @@ def __init__(self, content: str) -> None: ''' super().__init__(tag='mermaid', content=content) self.use_component('mermaid') - self.use_library('mermaid') + self.use_library(library_name) def on_content_change(self, content: str) -> None: self._props[self.CONTENT_PROP] = content.strip() diff --git a/nicegui/elements/plotly.py b/nicegui/elements/plotly.py index d0b488ff5..ae9209496 100644 --- a/nicegui/elements/plotly.py +++ b/nicegui/elements/plotly.py @@ -7,7 +7,7 @@ from ..element import Element register_vue_component('plotly', Path(__file__).parent / 'plotly.vue') -register_library('plotly', Path(__file__).parent / 'lib' / 'plotly' / 'plotly.min.js') +library_name = register_library(Path('plotly') / 'plotly.min.js') class Plotly(Element): @@ -28,7 +28,7 @@ def __init__(self, figure: Union[Dict, go.Figure]) -> None: a `dict` object with keys `data`, `layout`, `config` (optional). """ super().__init__('plotly') - self.use_library('plotly') + self.use_library(library_name) self.figure = figure self.update() diff --git a/nicegui/elements/scene.py b/nicegui/elements/scene.py index 92b99aaae..0e6bbd55f 100644 --- a/nicegui/elements/scene.py +++ b/nicegui/elements/scene.py @@ -10,12 +10,15 @@ 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') +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'), +] @dataclass(**KWONLY_SLOTS) @@ -83,12 +86,8 @@ def __init__(self, self.on('init', self.handle_init) 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') + for library_name in library_names: + self.use_library(library_name) def handle_init(self, e: GenericEventArguments) -> None: self.is_initialized = True diff --git a/nicegui/nicegui.py b/nicegui/nicegui.py index c5697e87e..669cf9972 100644 --- a/nicegui/nicegui.py +++ b/nicegui/nicegui.py @@ -44,13 +44,14 @@ def index(request: Request) -> Response: return globals.index_client.build_response(request) -@app.get(f'/_nicegui/{__version__}' + '/library/{name}/{file}') -def get_dependencies(name: str, file: str): +@app.get(f'/_nicegui/{__version__}' + '/library/{name:path}') +def get_dependencies(name: 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') + 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') diff --git a/nicegui/templates/index.html b/nicegui/templates/index.html index ec6c497e8..66ca2181d 100644 --- a/nicegui/templates/index.html +++ b/nicegui/templates/index.html @@ -190,7 +190,7 @@ async function loadDependencies(element) { for (const name of element['libraries']) { if (loaded_libraries.has(name)) continue; - await import(`{{ prefix | safe }}/_nicegui/{{version}}/library/${name}/include`); + await import(`{{ prefix | safe }}/_nicegui/{{version}}/library/${name}`); loaded_libraries.add(name); } for (const name of element['components']) { From 1b2de8663fe5c1062ba91ca286db8ecc2f09aa8c Mon Sep 17 00:00:00 2001 From: Rodja Trappe Date: Fri, 7 Jul 2023 06:15:27 +0200 Subject: [PATCH 02/24] fixed mermaid by adapting "register_vue_component" --- nicegui/dependencies.py | 72 +++++++++++++++++++++---------------- nicegui/elements/mermaid.js | 2 +- nicegui/elements/mermaid.py | 7 ++-- nicegui/nicegui.py | 14 +++++--- 4 files changed, 57 insertions(+), 38 deletions(-) diff --git a/nicegui/dependencies.py b/nicegui/dependencies.py index 8fcde8afe..39ec74183 100644 --- a/nicegui/dependencies.py +++ b/nicegui/dependencies.py @@ -1,4 +1,5 @@ import json +import logging from pathlib import Path from typing import Any, Dict, List, Set, Tuple @@ -12,14 +13,22 @@ libraries: Dict[str, Any] = {} -def register_vue_component(name: str, path: Path) -> None: +def register_vue_component(location: Path, + base_path: Path = Path(__file__).parent / 'elements', *, expose: bool = False + ) -> str: """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 + :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` """ - suffix = path.suffix.lower() + 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 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: @@ -28,14 +37,16 @@ def register_vue_component(name: str, path: Path) -> None: 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} + 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: +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 (used in element's `use_library`) and must therefore be url-safe. + :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` @@ -44,6 +55,7 @@ def register_library( 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 @@ -58,33 +70,33 @@ def generate_resources(prefix: str, elements: List[Element]) -> Tuple[str, str, 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}' - done_libraries.add(key) + 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 key in vue_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' - done_components.add(key) + 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. 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}";\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' + 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(key) + done_components.add(resource) vue_styles = f'' import_maps = f'' return vue_html, vue_styles, vue_scripts, import_maps, js_imports diff --git a/nicegui/elements/mermaid.js b/nicegui/elements/mermaid.js index 06bbe0747..bc499ca56 100644 --- a/nicegui/elements/mermaid.js +++ b/nicegui/elements/mermaid.js @@ -1,4 +1,4 @@ -import mermaid from "mermaid"; +import mermaid from "mermaid/mermaid.esm.min.mjs"; export default { template: `
`, mounted() { diff --git a/nicegui/elements/mermaid.py b/nicegui/elements/mermaid.py index 390e54cd9..34d1406d4 100644 --- a/nicegui/elements/mermaid.py +++ b/nicegui/elements/mermaid.py @@ -3,8 +3,11 @@ from ..dependencies import register_library, register_vue_component from .mixins.content_element import ContentElement -register_vue_component('mermaid', Path(__file__).parent / 'mermaid.js') +component_name = register_vue_component(Path('mermaid.js')) library_name = 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)) class Mermaid(ContentElement): @@ -19,7 +22,7 @@ def __init__(self, content: str) -> None: :param content: the Mermaid content to be displayed ''' super().__init__(tag='mermaid', content=content) - self.use_component('mermaid') + self.use_component(component_name) self.use_library(library_name) def on_content_change(self, content: str) -> None: diff --git a/nicegui/nicegui.py b/nicegui/nicegui.py index 669cf9972..d516054de 100644 --- a/nicegui/nicegui.py +++ b/nicegui/nicegui.py @@ -55,11 +55,15 @@ def get_dependencies(name: str): 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__}' + '/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.on_event('startup') From f3dbca960bd21d601ac149a85098395c169e85cb Mon Sep 17 00:00:00 2001 From: Rodja Trappe Date: Fri, 7 Jul 2023 07:41:16 +0200 Subject: [PATCH 03/24] import mapping is not longer needed --- nicegui/templates/index.html | 1 - 1 file changed, 1 deletion(-) diff --git a/nicegui/templates/index.html b/nicegui/templates/index.html index 66ca2181d..4b0054f39 100644 --- a/nicegui/templates/index.html +++ b/nicegui/templates/index.html @@ -19,7 +19,6 @@ - {{ import_maps | safe }} {{ body_html | safe }} From 4792acc3effe7207f2ffdc83b4d151bb1b074015 Mon Sep 17 00:00:00 2001 From: Rodja Trappe Date: Fri, 7 Jul 2023 14:02:40 +0200 Subject: [PATCH 04/24] adapting joystick to use new registration schema --- nicegui/elements/joystick.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nicegui/elements/joystick.py b/nicegui/elements/joystick.py index 3dbc9b10e..9ec13cd0d 100644 --- a/nicegui/elements/joystick.py +++ b/nicegui/elements/joystick.py @@ -5,7 +5,7 @@ from ..element import Element from ..events import GenericEventArguments, JoystickEventArguments, handle_event -register_vue_component('joystick', Path(__file__).parent / 'joystick.vue') +register_vue_component(Path('joystick.vue')) library_name = register_library(Path('nipplejs') / 'nipplejs.js') From 96c2df71aca9ddc1d732d590f656511e6f36e646 Mon Sep 17 00:00:00 2001 From: Falko Schindler Date: Fri, 7 Jul 2023 13:48:40 +0200 Subject: [PATCH 05/24] update remaining register_vue_component calls --- nicegui/dependencies.py | 12 +++--------- nicegui/elements/aggrid.py | 8 ++++---- nicegui/elements/audio.py | 4 ++-- nicegui/elements/chart.py | 4 ++-- nicegui/elements/chat_message.py | 4 ++-- nicegui/elements/colors.py | 4 ++-- nicegui/elements/dark_mode.py | 4 ++-- nicegui/elements/image.py | 4 ++-- nicegui/elements/input.py | 4 ++-- nicegui/elements/interactive_image.py | 4 ++-- nicegui/elements/joystick.py | 6 +++--- nicegui/elements/keyboard.py | 4 ++-- nicegui/elements/link.py | 4 ++-- nicegui/elements/log.py | 4 ++-- nicegui/elements/markdown.py | 4 ++-- nicegui/elements/mermaid.py | 8 ++++---- nicegui/elements/plotly.py | 6 +++--- nicegui/elements/query.py | 4 ++-- nicegui/elements/scene.py | 10 +++++----- nicegui/elements/select.py | 4 ++-- nicegui/elements/table.py | 4 ++-- nicegui/elements/upload.py | 4 ++-- nicegui/elements/video.py | 4 ++-- nicegui/functions/refreshable.py | 4 ++-- 24 files changed, 58 insertions(+), 64 deletions(-) diff --git a/nicegui/dependencies.py b/nicegui/dependencies.py index 39ec74183..dea27200b 100644 --- a/nicegui/dependencies.py +++ b/nicegui/dependencies.py @@ -1,5 +1,4 @@ import json -import logging from pathlib import Path from typing import Any, Dict, List, Set, Tuple @@ -13,18 +12,14 @@ libraries: Dict[str, Any] = {} -def register_vue_component(location: Path, - base_path: Path = Path(__file__).parent / 'elements', *, expose: bool = False - ) -> str: +def register_vue_component(location: Path, base_path: Path = Path(__file__).parent / 'elements') -> str: """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` """ - if isinstance(location, str): - logging.warning('register_vue_component: location is a string, did you mean to use register_library?') - return + assert isinstance(location, Path) suffix = location.suffix.lower() assert suffix in {'.vue', '.js', '.mjs'}, 'Only VUE and JS components are supported.' name = location.stem @@ -51,8 +46,7 @@ def register_library(location: Path, :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` """ - if isinstance(location, str): - return + assert isinstance(location, Path) 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}' diff --git a/nicegui/elements/aggrid.py b/nicegui/elements/aggrid.py index 81e3df329..361456647 100644 --- a/nicegui/elements/aggrid.py +++ b/nicegui/elements/aggrid.py @@ -7,8 +7,8 @@ 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): @@ -28,8 +28,8 @@ def __init__(self, options: Dict, *, html_columns: List[int] = [], theme: str = 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: diff --git a/nicegui/elements/audio.py b/nicegui/elements/audio.py index a9f838ca5..2eb89c334 100644 --- a/nicegui/elements/audio.py +++ b/nicegui/elements/audio.py @@ -6,7 +6,7 @@ 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): @@ -37,7 +37,7 @@ def __init__(self, src: Union[str, Path], *, 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' diff --git a/nicegui/elements/chart.py b/nicegui/elements/chart.py index 0dfebf72b..cb41f6528 100644 --- a/nicegui/elements/chart.py +++ b/nicegui/elements/chart.py @@ -4,7 +4,7 @@ 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' @@ -34,7 +34,7 @@ def __init__(self, options: Dict, *, type: str = 'chart', extras: List[str] = [] 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: diff --git a/nicegui/elements/chat_message.py b/nicegui/elements/chat_message.py index 9745c123c..7a71cc562 100644 --- a/nicegui/elements/chat_message.py +++ b/nicegui/elements/chat_message.py @@ -5,7 +5,7 @@ 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): @@ -32,7 +32,7 @@ def __init__(self, :param text_html: render text as HTML (default: False) """ super().__init__('chat_message') - self.use_component('chat_message') + self.use_component(component) if isinstance(text, str): text = [text] diff --git a/nicegui/elements/colors.py b/nicegui/elements/colors.py index 37303963e..ce748aae4 100644 --- a/nicegui/elements/colors.py +++ b/nicegui/elements/colors.py @@ -3,7 +3,7 @@ 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): @@ -22,7 +22,7 @@ def __init__(self, *, Sets the main colors (primary, secondary, accent, ...) used by `Quasar `_. """ super().__init__('colors') - self.use_component('colors') + self.use_component(component) self._props['primary'] = primary self._props['secondary'] = secondary self._props['accent'] = accent diff --git a/nicegui/elements/dark_mode.py b/nicegui/elements/dark_mode.py index 3a20b9057..aaf62ad6b 100644 --- a/nicegui/elements/dark_mode.py +++ b/nicegui/elements/dark_mode.py @@ -4,7 +4,7 @@ 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): @@ -21,7 +21,7 @@ def __init__(self, value: Optional[bool] = False) -> None: :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') + self.use_component(component) def enable(self) -> None: """Enable dark mode.""" diff --git a/nicegui/elements/image.py b/nicegui/elements/image.py index 6186982cc..8e308eb45 100644 --- a/nicegui/elements/image.py +++ b/nicegui/elements/image.py @@ -5,7 +5,7 @@ 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): @@ -18,4 +18,4 @@ def __init__(self, source: Union[str, Path] = '') -> None: :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') + self.use_component(component) diff --git a/nicegui/elements/input.py b/nicegui/elements/input.py index 4bd5ce594..d4ecf9061 100644 --- a/nicegui/elements/input.py +++ b/nicegui/elements/input.py @@ -6,7 +6,7 @@ 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): @@ -59,7 +59,7 @@ def toggle_type(_): 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.""" diff --git a/nicegui/elements/interactive_image.py b/nicegui/elements/interactive_image.py index 91978df2e..dc1198c8b 100644 --- a/nicegui/elements/interactive_image.py +++ b/nicegui/elements/interactive_image.py @@ -8,7 +8,7 @@ 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): @@ -38,7 +38,7 @@ def __init__(self, super().__init__(tag='interactive_image', 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: diff --git a/nicegui/elements/joystick.py b/nicegui/elements/joystick.py index 3dbc9b10e..d8f1937d5 100644 --- a/nicegui/elements/joystick.py +++ b/nicegui/elements/joystick.py @@ -5,8 +5,8 @@ from ..element import Element from ..events import GenericEventArguments, JoystickEventArguments, handle_event -register_vue_component('joystick', Path(__file__).parent / '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): @@ -28,7 +28,7 @@ def __init__(self, *, :param options: arguments like `color` which should be passed to the `underlying nipple.js library `_ """ super().__init__('joystick') - self.use_library(library_name) + self.use_library(library) self._props['options'] = options self.active = False diff --git a/nicegui/elements/keyboard.py b/nicegui/elements/keyboard.py index dfb4b2625..3220f5d05 100644 --- a/nicegui/elements/keyboard.py +++ b/nicegui/elements/keyboard.py @@ -9,7 +9,7 @@ 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): @@ -37,7 +37,7 @@ def __init__(self, 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: diff --git a/nicegui/elements/link.py b/nicegui/elements/link.py index 08c50d587..c13b3328e 100644 --- a/nicegui/elements/link.py +++ b/nicegui/elements/link.py @@ -6,7 +6,7 @@ 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): @@ -36,7 +36,7 @@ def __init__(self, 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): diff --git a/nicegui/elements/log.py b/nicegui/elements/log.py index 1b43392e1..a6a600570 100644 --- a/nicegui/elements/log.py +++ b/nicegui/elements/log.py @@ -6,7 +6,7 @@ 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): @@ -23,7 +23,7 @@ def __init__(self, max_lines: Optional[int] = None) -> None: 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: diff --git a/nicegui/elements/markdown.py b/nicegui/elements/markdown.py index 7bc42036e..2b7b85b76 100644 --- a/nicegui/elements/markdown.py +++ b/nicegui/elements/markdown.py @@ -10,7 +10,7 @@ from ..dependencies import register_vue_component 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): @@ -27,7 +27,7 @@ def __init__(self, content: str = '', *, extras: List[str] = ['fenced-code-block super().__init__(tag='markdown', 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') diff --git a/nicegui/elements/mermaid.py b/nicegui/elements/mermaid.py index 34d1406d4..d96e01277 100644 --- a/nicegui/elements/mermaid.py +++ b/nicegui/elements/mermaid.py @@ -3,8 +3,8 @@ 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)) @@ -22,8 +22,8 @@ def __init__(self, content: str) -> None: :param content: the Mermaid content to be displayed ''' super().__init__(tag='mermaid', content=content) - self.use_component(component_name) - self.use_library(library_name) + self.use_component(component) + self.use_library(library) def on_content_change(self, content: str) -> None: self._props[self.CONTENT_PROP] = content.strip() diff --git a/nicegui/elements/plotly.py b/nicegui/elements/plotly.py index ae9209496..d71ae67bc 100644 --- a/nicegui/elements/plotly.py +++ b/nicegui/elements/plotly.py @@ -6,8 +6,8 @@ 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): @@ -28,7 +28,7 @@ def __init__(self, figure: Union[Dict, go.Figure]) -> None: a `dict` object with keys `data`, `layout`, `config` (optional). """ super().__init__('plotly') - self.use_library(library_name) + self.use_library(library) self.figure = figure self.update() diff --git a/nicegui/elements/query.py b/nicegui/elements/query.py index b1b342d5b..1f6e093aa 100644 --- a/nicegui/elements/query.py +++ b/nicegui/elements/query.py @@ -7,7 +7,7 @@ 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): @@ -18,7 +18,7 @@ def __init__(self, selector: str) -> None: 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: diff --git a/nicegui/elements/scene.py b/nicegui/elements/scene.py index 0e6bbd55f..95238043f 100644 --- a/nicegui/elements/scene.py +++ b/nicegui/elements/scene.py @@ -9,9 +9,9 @@ from ..helpers import KWONLY_SLOTS from .scene_object3d import Object3D -register_vue_component('scene', Path(__file__).parent / 'scene.js') +component = register_vue_component(Path('scene.js')) lib = Path('three') -library_names = [ +libraries = [ register_library(lib / 'three.module.js', expose=True), register_library(lib / 'modules' / 'CSS2DRenderer.js', expose=True), register_library(lib / 'modules' / 'CSS3DRenderer.js', expose=True), @@ -85,9 +85,9 @@ def __init__(self, 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 diff --git a/nicegui/elements/select.py b/nicegui/elements/select.py index 7e7fd7f29..2a8906145 100644 --- a/nicegui/elements/select.py +++ b/nicegui/elements/select.py @@ -8,7 +8,7 @@ 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): @@ -41,7 +41,7 @@ def __init__(self, elif not isinstance(value, list): value = [value] super().__init__(tag='select', options=options, value=value, on_change=on_change) - self.use_component('select') + self.use_component(component) if label is not None: self._props['label'] = label if with_input: diff --git a/nicegui/elements/table.py b/nicegui/elements/table.py index 8dcd315e2..af734f21e 100644 --- a/nicegui/elements/table.py +++ b/nicegui/elements/table.py @@ -8,7 +8,7 @@ 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): @@ -63,7 +63,7 @@ def handle_selection(e: GenericEventArguments) -> None: 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.""" diff --git a/nicegui/elements/upload.py b/nicegui/elements/upload.py index ea25e4274..95f63fd4d 100644 --- a/nicegui/elements/upload.py +++ b/nicegui/elements/upload.py @@ -9,7 +9,7 @@ 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): @@ -38,7 +38,7 @@ def __init__(self, *, :param auto_upload: automatically upload files when they are selected (default: `False`) """ super().__init__(tag='upload') - self.use_component('upload') + self.use_component(component) self._props['multiple'] = multiple self._props['label'] = label self._props['auto-upload'] = auto_upload diff --git a/nicegui/elements/video.py b/nicegui/elements/video.py index f232e8b2d..cfc452ec3 100644 --- a/nicegui/elements/video.py +++ b/nicegui/elements/video.py @@ -6,7 +6,7 @@ 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): @@ -37,7 +37,7 @@ def __init__(self, src: Union[str, Path], *, 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' diff --git a/nicegui/functions/refreshable.py b/nicegui/functions/refreshable.py index 9f4d4be78..d5b93de10 100644 --- a/nicegui/functions/refreshable.py +++ b/nicegui/functions/refreshable.py @@ -9,7 +9,7 @@ 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')) @dataclass(**KWONLY_SLOTS) @@ -56,7 +56,7 @@ def __get__(self, instance, _) -> Self: def __call__(self, *args: Any, **kwargs: Any) -> Union[None, Awaitable]: self.prune() container = Element('refreshable') - container.use_component('refreshable') + container.use_component(component) target = RefreshableTarget(container=container, instance=self.instance, args=args, kwargs=kwargs) self.targets.append(target) return target.run(self.func) From 13d32bd05dd9e3334881e723512687cb5eac9881 Mon Sep 17 00:00:00 2001 From: Falko Schindler Date: Fri, 7 Jul 2023 15:04:22 +0200 Subject: [PATCH 06/24] various dependency improvements --- nicegui/dependencies.py | 58 +++++++++++++++++++----------------- nicegui/elements/scene.py | 13 ++++---- nicegui/nicegui.py | 34 +++++++++------------ nicegui/templates/index.html | 3 +- 4 files changed, 52 insertions(+), 56 deletions(-) diff --git a/nicegui/dependencies.py b/nicegui/dependencies.py index dea27200b..bd644dd39 100644 --- a/nicegui/dependencies.py +++ b/nicegui/dependencies.py @@ -15,43 +15,45 @@ def register_vue_component(location: Path, base_path: Path = Path(__file__).parent / 'elements') -> str: """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` """ - assert isinstance(location, Path) suffix = location.suffix.lower() assert suffix in {'.vue', '.js', '.mjs'}, 'Only VUE and JS components are supported.' name = location.stem + key = str(location) path = base_path / location 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()) + assert key not in vue_components, f'Duplicate VUE component {key}' + vue_components[key] = 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) + assert key not in js_components, f'Duplicate JS component {key}' + js_components[key] = {'name': name, 'path': path} + return key -def register_library(location: Path, - base_path: Path = Path(__file__).parent / 'elements' / 'lib', *, expose: bool = False - ) -> str: - """Register a new external library. +def register_library(location: Path, base_path: Path = Path(__file__).parent / 'elements' / 'lib', *, + expose: bool = False) -> str: + """Register a *.js 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` + :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` """ - assert isinstance(location, Path) - 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 + suffix = location.suffix.lower() + assert suffix in {'.js', '.mjs'}, 'Only JS dependencies are supported.' + name = location.stem + key = str(location) + path = base_path / location + assert key not in libraries, f'Duplicate js library {key}' + libraries[key] = {'name': name, 'path': path, 'expose': expose} + return key def generate_resources(prefix: str, elements: List[Element]) -> Tuple[str, str, str, str, str]: @@ -67,7 +69,7 @@ def generate_resources(prefix: str, elements: List[Element]) -> Tuple[str, str, 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}' + import_maps['imports'][name] = f'{prefix}/_nicegui/{__version__}/libraries/{resource}' done_libraries.add(resource) # Build the none optimized component (ie, the vue component). for resource in vue_components: @@ -82,7 +84,7 @@ def generate_resources(prefix: str, elements: List[Element]) -> Tuple[str, str, 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' + js_imports += f'import "{prefix}/_nicegui/{__version__}/libraries/{resource}";\n' done_libraries.add(resource) for resource in element.components: if resource in js_components and resource not in done_components: diff --git a/nicegui/elements/scene.py b/nicegui/elements/scene.py index 95238043f..79f1eab06 100644 --- a/nicegui/elements/scene.py +++ b/nicegui/elements/scene.py @@ -10,14 +10,13 @@ from .scene_object3d import Object3D component = register_vue_component(Path('scene.js')) -lib = Path('three') libraries = [ - 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'), + 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')), ] diff --git a/nicegui/nicegui.py b/nicegui/nicegui.py index d516054de..af96abaac 100644 --- a/nicegui/nicegui.py +++ b/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') diff --git a/nicegui/templates/index.html b/nicegui/templates/index.html index 4b0054f39..3dd672fd1 100644 --- a/nicegui/templates/index.html +++ b/nicegui/templates/index.html @@ -19,6 +19,7 @@ + {{ import_maps | safe }} {{ body_html | safe }} @@ -189,7 +190,7 @@ async function loadDependencies(element) { for (const name of element['libraries']) { if (loaded_libraries.has(name)) continue; - await import(`{{ prefix | safe }}/_nicegui/{{version}}/library/${name}`); + await import(`{{ prefix | safe }}/_nicegui/{{version}}/libraries/${name}`); loaded_libraries.add(name); } for (const name of element['components']) { From ad7e1668509eb6e11d37884900aa1e9ee5a573b8 Mon Sep 17 00:00:00 2001 From: Falko Schindler Date: Fri, 7 Jul 2023 15:35:44 +0200 Subject: [PATCH 07/24] fix automatically generated library names --- nicegui/dependencies.py | 15 +++++++-------- nicegui/elements/mermaid.py | 2 +- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/nicegui/dependencies.py b/nicegui/dependencies.py index bd644dd39..fc30dd796 100644 --- a/nicegui/dependencies.py +++ b/nicegui/dependencies.py @@ -23,11 +23,8 @@ def register_vue_component(location: Path, base_path: Path = Path(__file__).pare :param base_path: base path where your libraries are located :return: resource identifier to be used in element's `use_component` """ - suffix = location.suffix.lower() + path, key, name, suffix = deconstruct_location(location, base_path) assert suffix in {'.vue', '.js', '.mjs'}, 'Only VUE and JS components are supported.' - name = location.stem - key = str(location) - path = base_path / location if suffix == '.vue': assert key not in vue_components, f'Duplicate VUE component {key}' vue_components[key] = vbuild.VBuild(name, path.read_text()) @@ -46,16 +43,18 @@ def register_library(location: Path, base_path: Path = Path(__file__).parent / ' :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` """ - suffix = location.suffix.lower() + path, key, name, suffix = deconstruct_location(location, base_path) assert suffix in {'.js', '.mjs'}, 'Only JS dependencies are supported.' - name = location.stem - key = str(location) - path = base_path / location assert key not in libraries, f'Duplicate js library {key}' libraries[key] = {'name': name, 'path': path, 'expose': expose} return key +def deconstruct_location(location: Path, base_path: Path) -> Tuple[str, 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]: done_libraries: Set[str] = set() done_components: Set[str] = set() diff --git a/nicegui/elements/mermaid.py b/nicegui/elements/mermaid.py index d96e01277..a28617253 100644 --- a/nicegui/elements/mermaid.py +++ b/nicegui/elements/mermaid.py @@ -4,7 +4,7 @@ from .mixins.content_element import ContentElement component = register_vue_component(Path('mermaid.js')) -library = register_library(Path('mermaid') / 'mermaid.esm.min.mjs', expose=True) +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)) From 40adfdd70bddf8dc11f0e8064c24653131631e9d Mon Sep 17 00:00:00 2001 From: Falko Schindler Date: Fri, 7 Jul 2023 16:45:20 +0200 Subject: [PATCH 08/24] introduce dataclasses for components and libraries --- nicegui/client.py | 4 +- nicegui/dependencies.py | 140 ++++++++++++++++---------- nicegui/element.py | 9 +- nicegui/elements/aggrid.py | 2 +- nicegui/elements/audio.py | 2 +- nicegui/elements/chart.py | 2 +- nicegui/elements/chat_message.py | 2 +- nicegui/elements/colors.py | 2 +- nicegui/elements/dark_mode.py | 2 +- nicegui/elements/image.py | 2 +- nicegui/elements/input.py | 2 +- nicegui/elements/interactive_image.py | 2 +- nicegui/elements/joystick.py | 2 +- nicegui/elements/keyboard.py | 2 +- nicegui/elements/link.py | 2 +- nicegui/elements/log.py | 2 +- nicegui/elements/markdown.py | 2 +- nicegui/elements/mermaid.py | 2 +- nicegui/elements/plotly.py | 2 +- nicegui/elements/query.py | 2 +- nicegui/elements/scene.py | 2 +- nicegui/elements/select.py | 2 +- nicegui/elements/table.py | 2 +- nicegui/elements/upload.py | 2 +- nicegui/elements/video.py | 2 +- nicegui/functions/refreshable.py | 3 +- nicegui/nicegui.py | 8 +- nicegui/templates/index.html | 6 +- website/intersection_observer.py | 6 +- 29 files changed, 129 insertions(+), 91 deletions(-) diff --git a/nicegui/client.py b/nicegui/client.py index 635888cf8..150d0f883 100644 --- a/nicegui/client.py +++ b/nicegui/client.py @@ -70,7 +70,7 @@ def __exit__(self, *_): 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__, @@ -79,7 +79,7 @@ def build_response(self, request: Request, status_code: int = 200) -> Response: '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, + 'imports': json.dumps(imports), 'js_imports': js_imports, 'title': self.page.resolve_title(), 'viewport': self.page.resolve_viewport(), diff --git a/nicegui/dependencies.py b/nicegui/dependencies.py index fc30dd796..73d26741c 100644 --- a/nicegui/dependencies.py +++ b/nicegui/dependencies.py @@ -1,18 +1,50 @@ -import json +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') -> 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. Single-file components (.vue) are built right away @@ -24,18 +56,20 @@ def register_vue_component(location: Path, base_path: Path = Path(__file__).pare :return: resource identifier to be used in element's `use_component` """ path, key, name, suffix = deconstruct_location(location, base_path) - assert suffix in {'.vue', '.js', '.mjs'}, 'Only VUE and JS components are supported.' if suffix == '.vue': assert key not in vue_components, f'Duplicate VUE component {key}' - vue_components[key] = vbuild.VBuild(name, path.read_text()) - elif suffix == '.js': + 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] = {'name': name, 'path': path} - return 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) -> str: + 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) @@ -44,13 +78,14 @@ def register_library(location: Path, base_path: Path = Path(__file__).parent / ' :return: resource identifier to be used in element's `use_library` """ path, key, name, suffix = deconstruct_location(location, base_path) - assert suffix in {'.js', '.mjs'}, 'Only JS dependencies are supported.' - assert key not in libraries, f'Duplicate js library {key}' - libraries[key] = {'name': name, 'path': path, 'expose': expose} - return key + 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[str, str, str, str]: +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() @@ -58,40 +93,41 @@ def deconstruct_location(location: Path, base_path: Path) -> Tuple[str, str, str def generate_resources(prefix: str, elements: List[Element]) -> Tuple[str, str, str, str, 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__}/libraries/{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: str = '' + vue_html: str = '' + vue_styles: str = '' + js_imports: 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 += f'{component.html}\n' + vue_scripts += component.script.replace(f"Vue.component('{component.name}',", + f"app.component('{component.tag}',", 1) + '\n' + vue_styles += f'{component.style}\n' + 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__}/libraries/{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) + if key.startswith('nipple'): + print(key, flush=True) + 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__}/libraries/{key}";\n' + done_libraries.add(key) + for key in element.components: + if key in js_components and key not in done_components: + component = js_components[key] + js_imports += f'import {{ default as {component.name} }} from "{prefix}/_nicegui/{__version__}/components/{key}";\n' + js_imports += f'app.component("{component.tag}", {component.name});\n' + done_components.add(key) vue_styles = f'' - import_maps = f'' - return vue_html, vue_styles, vue_scripts, import_maps, js_imports + return vue_html, vue_styles, vue_scripts, imports, js_imports diff --git a/nicegui/element.py b/nicegui/element.py index 8b57321b4..4b60267b9 100644 --- a/nicegui/element.py +++ b/nicegui/element.py @@ -16,6 +16,7 @@ if TYPE_CHECKING: from .client import Client + from .dependencies import JsComponent, Library PROPS_PATTERN = re.compile(r'([:\w\-]+)(?:=(?:("[^"\\]*(?:\\.[^"\\]*)*")|([\w\-.%:\/]+)))?(?:$|\s)') @@ -307,12 +308,12 @@ def delete(self) -> None: 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.key) 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.key) return self diff --git a/nicegui/elements/aggrid.py b/nicegui/elements/aggrid.py index 361456647..62215a64a 100644 --- a/nicegui/elements/aggrid.py +++ b/nicegui/elements/aggrid.py @@ -24,7 +24,7 @@ def __init__(self, options: Dict, *, html_columns: List[int] = [], theme: str = :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}'] diff --git a/nicegui/elements/audio.py b/nicegui/elements/audio.py index 2eb89c334..77affd0c9 100644 --- a/nicegui/elements/audio.py +++ b/nicegui/elements/audio.py @@ -29,7 +29,7 @@ def __init__(self, src: Union[str, Path], *, See `here `_ 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 diff --git a/nicegui/elements/chart.py b/nicegui/elements/chart.py index cb41f6528..1fc0fd044 100644 --- a/nicegui/elements/chart.py +++ b/nicegui/elements/chart.py @@ -30,7 +30,7 @@ def __init__(self, options: Dict, *, type: str = 'chart', extras: List[str] = [] :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 diff --git a/nicegui/elements/chat_message.py b/nicegui/elements/chat_message.py index 7a71cc562..fbee09789 100644 --- a/nicegui/elements/chat_message.py +++ b/nicegui/elements/chat_message.py @@ -31,7 +31,7 @@ def __init__(self, :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') + super().__init__(component.tag) self.use_component(component) if isinstance(text, str): diff --git a/nicegui/elements/colors.py b/nicegui/elements/colors.py index ce748aae4..fb2bd1414 100644 --- a/nicegui/elements/colors.py +++ b/nicegui/elements/colors.py @@ -21,7 +21,7 @@ def __init__(self, *, Sets the main colors (primary, secondary, accent, ...) used by `Quasar `_. """ - super().__init__('colors') + super().__init__(component.tag) self.use_component(component) self._props['primary'] = primary self._props['secondary'] = secondary diff --git a/nicegui/elements/dark_mode.py b/nicegui/elements/dark_mode.py index aaf62ad6b..bbd0f9c2e 100644 --- a/nicegui/elements/dark_mode.py +++ b/nicegui/elements/dark_mode.py @@ -20,7 +20,7 @@ def __init__(self, value: Optional[bool] = False) -> None: :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) + super().__init__(tag=component.tag, value=value, on_value_change=None) self.use_component(component) def enable(self) -> None: diff --git a/nicegui/elements/image.py b/nicegui/elements/image.py index 8e308eb45..a5d428da2 100644 --- a/nicegui/elements/image.py +++ b/nicegui/elements/image.py @@ -17,5 +17,5 @@ def __init__(self, source: Union[str, Path] = '') -> None: :param source: the source of the image; can be a URL, local file path or a base64 string """ - super().__init__(tag='image', source=source) + super().__init__(tag=component.tag, source=source) self.use_component(component) diff --git a/nicegui/elements/input.py b/nicegui/elements/input.py index d4ecf9061..01cf69ede 100644 --- a/nicegui/elements/input.py +++ b/nicegui/elements/input.py @@ -42,7 +42,7 @@ def __init__(self, :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: diff --git a/nicegui/elements/interactive_image.py b/nicegui/elements/interactive_image.py index dc1198c8b..3ed5d54c6 100644 --- a/nicegui/elements/interactive_image.py +++ b/nicegui/elements/interactive_image.py @@ -35,7 +35,7 @@ def __init__(self, :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(component) diff --git a/nicegui/elements/joystick.py b/nicegui/elements/joystick.py index d8f1937d5..7d8d031ef 100644 --- a/nicegui/elements/joystick.py +++ b/nicegui/elements/joystick.py @@ -27,7 +27,7 @@ def __init__(self, *, :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 `_ """ - super().__init__('joystick') + super().__init__('nicegui-joystick') self.use_library(library) self._props['options'] = options self.active = False diff --git a/nicegui/elements/keyboard.py b/nicegui/elements/keyboard.py index 3220f5d05..a1f3e41f0 100644 --- a/nicegui/elements/keyboard.py +++ b/nicegui/elements/keyboard.py @@ -30,7 +30,7 @@ def __init__(self, :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'] diff --git a/nicegui/elements/link.py b/nicegui/elements/link.py index c13b3328e..c42158e21 100644 --- a/nicegui/elements/link.py +++ b/nicegui/elements/link.py @@ -27,7 +27,7 @@ def __init__(self, :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): diff --git a/nicegui/elements/log.py b/nicegui/elements/log.py index a6a600570..80e51e689 100644 --- a/nicegui/elements/log.py +++ b/nicegui/elements/log.py @@ -18,7 +18,7 @@ def __init__(self, max_lines: Optional[int] = None) -> None: :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'] diff --git a/nicegui/elements/markdown.py b/nicegui/elements/markdown.py index 2b7b85b76..eacad1d60 100644 --- a/nicegui/elements/markdown.py +++ b/nicegui/elements/markdown.py @@ -24,7 +24,7 @@ def __init__(self, content: str = '', *, extras: List[str] = ['fenced-code-block :param extras: list of `markdown2 extensions `_ (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(component) diff --git a/nicegui/elements/mermaid.py b/nicegui/elements/mermaid.py index a28617253..281e893f7 100644 --- a/nicegui/elements/mermaid.py +++ b/nicegui/elements/mermaid.py @@ -21,7 +21,7 @@ def __init__(self, content: str) -> None: :param content: the Mermaid content to be displayed ''' - super().__init__(tag='mermaid', content=content) + super().__init__(tag=component.tag, content=content) self.use_component(component) self.use_library(library) diff --git a/nicegui/elements/plotly.py b/nicegui/elements/plotly.py index d71ae67bc..103b473ff 100644 --- a/nicegui/elements/plotly.py +++ b/nicegui/elements/plotly.py @@ -27,7 +27,7 @@ def __init__(self, figure: Union[Dict, go.Figure]) -> None: :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') + super().__init__('nicegui-plotly') self.use_library(library) self.figure = figure diff --git a/nicegui/elements/query.py b/nicegui/elements/query.py index 1f6e093aa..8267cb356 100644 --- a/nicegui/elements/query.py +++ b/nicegui/elements/query.py @@ -13,7 +13,7 @@ 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'] = {} diff --git a/nicegui/elements/scene.py b/nicegui/elements/scene.py index 79f1eab06..e2abc8271 100644 --- a/nicegui/elements/scene.py +++ b/nicegui/elements/scene.py @@ -73,7 +73,7 @@ def __init__(self, :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 diff --git a/nicegui/elements/select.py b/nicegui/elements/select.py index 2a8906145..da79fd0b1 100644 --- a/nicegui/elements/select.py +++ b/nicegui/elements/select.py @@ -40,7 +40,7 @@ def __init__(self, value = [] elif not isinstance(value, list): value = [value] - super().__init__(tag='select', options=options, value=value, on_change=on_change) + 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 diff --git a/nicegui/elements/table.py b/nicegui/elements/table.py index af734f21e..4c7c92ee7 100644 --- a/nicegui/elements/table.py +++ b/nicegui/elements/table.py @@ -36,7 +36,7 @@ def __init__(self, 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 diff --git a/nicegui/elements/upload.py b/nicegui/elements/upload.py index 95f63fd4d..e7f18c063 100644 --- a/nicegui/elements/upload.py +++ b/nicegui/elements/upload.py @@ -37,7 +37,7 @@ def __init__(self, *, :param label: label for the uploader (default: `''`) :param auto_upload: automatically upload files when they are selected (default: `False`) """ - super().__init__(tag='upload') + super().__init__(tag=component.tag) self.use_component(component) self._props['multiple'] = multiple self._props['label'] = label diff --git a/nicegui/elements/video.py b/nicegui/elements/video.py index cfc452ec3..91e19d3d4 100644 --- a/nicegui/elements/video.py +++ b/nicegui/elements/video.py @@ -29,7 +29,7 @@ def __init__(self, src: Union[str, Path], *, See `here `_ 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 diff --git a/nicegui/functions/refreshable.py b/nicegui/functions/refreshable.py index d5b93de10..d88e9966e 100644 --- a/nicegui/functions/refreshable.py +++ b/nicegui/functions/refreshable.py @@ -55,8 +55,7 @@ def __get__(self, instance, _) -> Self: def __call__(self, *args: Any, **kwargs: Any) -> Union[None, Awaitable]: self.prune() - container = Element('refreshable') - container.use_component(component) + 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) diff --git a/nicegui/nicegui.py b/nicegui/nicegui.py index af96abaac..4b767aca3 100644 --- a/nicegui/nicegui.py +++ b/nicegui/nicegui.py @@ -46,17 +46,17 @@ def index(request: Request) -> Response: @app.get(f'/_nicegui/{__version__}' + '/libraries/{key:path}') def get_library(key: str) -> FileResponse: - if key in libraries and libraries[key]['path'].exists(): + 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) + 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(): + 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) + return FileResponse(js_components[key].path, media_type='text/javascript', headers=headers) raise HTTPException(status_code=404, detail=f'component "{key}" not found') diff --git a/nicegui/templates/index.html b/nicegui/templates/index.html index 3dd672fd1..01f677349 100644 --- a/nicegui/templates/index.html +++ b/nicegui/templates/index.html @@ -19,7 +19,9 @@ - {{ import_maps | safe }} + {{ body_html | safe }} @@ -151,7 +153,7 @@ props: { props: { type: Object, default: {} } }, template: data.template, }, { - props: props + props: props, })); } const children = data.ids.map(id => renderRecursively(elements, id)); diff --git a/website/intersection_observer.py b/website/intersection_observer.py index f18a36df0..a82eedc44 100644 --- a/website/intersection_observer.py +++ b/website/intersection_observer.py @@ -5,17 +5,17 @@ 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') From 2671059537538be668606aa4e214b2b86809d66d Mon Sep 17 00:00:00 2001 From: Falko Schindler Date: Fri, 7 Jul 2023 17:23:36 +0200 Subject: [PATCH 09/24] fix website --- nicegui/client.py | 6 ++-- nicegui/dependencies.py | 48 +++++++++++++++++--------------- nicegui/element.py | 12 ++++---- nicegui/elements/markdown.py | 3 +- nicegui/elements/mermaid.js | 2 +- nicegui/elements/plotly.py | 2 +- nicegui/functions/refreshable.py | 2 +- nicegui/templates/index.html | 20 ++++++------- 8 files changed, 49 insertions(+), 46 deletions(-) diff --git a/nicegui/client.py b/nicegui/client.py index 150d0f883..c9c9f4bca 100644 --- a/nicegui/client.py +++ b/nicegui/client.py @@ -77,10 +77,10 @@ def build_response(self, request: Request, status_code: int = 200) -> Response: '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, + 'body_html': '\n' + self.body_html + '\n' + '\n'.join(vue_html), + 'vue_scripts': '\n'.join(vue_scripts), 'imports': json.dumps(imports), - 'js_imports': js_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), diff --git a/nicegui/dependencies.py b/nicegui/dependencies.py index 73d26741c..6279c3824 100644 --- a/nicegui/dependencies.py +++ b/nicegui/dependencies.py @@ -90,13 +90,17 @@ def deconstruct_location(location: Path, base_path: Path) -> Tuple[Path, str, st 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: str = '' - vue_html: str = '' - vue_styles: str = '' - js_imports: str = '' + 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 @@ -108,26 +112,24 @@ def generate_resources(prefix: str, elements: List[Element]) -> Tuple[str, str, # 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 += f'{component.html}\n' - vue_scripts += component.script.replace(f"Vue.component('{component.name}',", - f"app.component('{component.tag}',", 1) + '\n' - vue_styles += f'{component.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) # build the resources associated with the elements for element in elements: - if key.startswith('nipple'): - print(key, flush=True) - 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__}/libraries/{key}";\n' - done_libraries.add(key) - for key in element.components: - if key in js_components and key not in done_components: - component = js_components[key] - js_imports += f'import {{ default as {component.name} }} from "{prefix}/_nicegui/{__version__}/components/{key}";\n' - js_imports += f'app.component("{component.tag}", {component.name});\n' - done_components.add(key) - vue_styles = f'' + 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 diff --git a/nicegui/element.py b/nicegui/element.py index 4b60267b9..9f2910dc8 100644 --- a/nicegui/element.py +++ b/nicegui/element.py @@ -42,8 +42,8 @@ def __init__(self, tag: str, *, _client: Optional[Client] = None) -> None: 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') @@ -98,8 +98,8 @@ def _to_dict(self) -> Dict[str, Any]: '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 @@ -310,10 +310,10 @@ def delete(self) -> None: def use_component(self, component: JsComponent) -> Self: """Register a ``*.js`` Vue component to be used by this element.""" - self.components.append(component.key) + 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.key) + self.libraries.append(library) return self diff --git a/nicegui/elements/markdown.py b/nicegui/elements/markdown.py index eacad1d60..7d5db6c23 100644 --- a/nicegui/elements/markdown.py +++ b/nicegui/elements/markdown.py @@ -8,6 +8,7 @@ from pygments.formatters import HtmlFormatter from ..dependencies import register_vue_component +from .mermaid import library as mermaid_library from .mixins.content_element import ContentElement component = register_vue_component(Path('markdown.js')) @@ -30,7 +31,7 @@ def __init__(self, content: str = '', *, extras: List[str] = ['fenced-code-block 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)) diff --git a/nicegui/elements/mermaid.js b/nicegui/elements/mermaid.js index bc499ca56..06bbe0747 100644 --- a/nicegui/elements/mermaid.js +++ b/nicegui/elements/mermaid.js @@ -1,4 +1,4 @@ -import mermaid from "mermaid/mermaid.esm.min.mjs"; +import mermaid from "mermaid"; export default { template: `
`, mounted() { diff --git a/nicegui/elements/plotly.py b/nicegui/elements/plotly.py index 103b473ff..2696d4a40 100644 --- a/nicegui/elements/plotly.py +++ b/nicegui/elements/plotly.py @@ -27,7 +27,7 @@ def __init__(self, figure: Union[Dict, go.Figure]) -> None: :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__('nicegui-plotly') + super().__init__(component.tag) self.use_library(library) self.figure = figure diff --git a/nicegui/functions/refreshable.py b/nicegui/functions/refreshable.py index d88e9966e..c7564182e 100644 --- a/nicegui/functions/refreshable.py +++ b/nicegui/functions/refreshable.py @@ -9,7 +9,7 @@ from ..element import Element from ..helpers import KWONLY_SLOTS, is_coroutine_function -component = register_vue_component(Path('refreshable.js')) +component = register_vue_component(Path('refreshable.js'), base_path=Path(__file__).parent.parent / 'functions') @dataclass(**KWONLY_SLOTS) diff --git a/nicegui/templates/index.html b/nicegui/templates/index.html index 01f677349..501769863 100644 --- a/nicegui/templates/index.html +++ b/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)); + element.libraries.forEach((library) => loaded_libraries.add(library)); const props = { id: 'c' + element.id, @@ -190,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}}/libraries/${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({ From 121943882ee4e5cc9268374db87a5ecc17564454 Mon Sep 17 00:00:00 2001 From: Falko Schindler Date: Fri, 7 Jul 2023 18:07:59 +0200 Subject: [PATCH 10/24] update examples --- examples/custom_vue_component/counter.py | 6 +++--- examples/custom_vue_component/main.py | 2 +- examples/map/leaflet.py | 6 +++--- examples/single_page_app/router.py | 6 +++--- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/examples/custom_vue_component/counter.py b/examples/custom_vue_component/counter.py index 4235ff857..157bc6740 100644 --- a/examples/custom_vue_component/counter.py +++ b/examples/custom_vue_component/counter.py @@ -4,16 +4,16 @@ 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') diff --git a/examples/custom_vue_component/main.py b/examples/custom_vue_component/main.py index 93aba486d..bd373eecf 100755 --- a/examples/custom_vue_component/main.py +++ b/examples/custom_vue_component/main.py @@ -14,4 +14,4 @@ ui.button('Reset', on_click=counter.reset).props('small outline') -ui.run(port=1234) +ui.run() diff --git a/examples/map/leaflet.py b/examples/map/leaflet.py index 3cbfad942..4cf694428 100644 --- a/examples/map/leaflet.py +++ b/examples/map/leaflet.py @@ -5,14 +5,14 @@ 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('') ui.add_head_html('') diff --git a/examples/single_page_app/router.py b/examples/single_page_app/router.py index 780b16396..5ea9a3ac2 100644 --- a/examples/single_page_app/router.py +++ b/examples/single_page_app/router.py @@ -4,7 +4,7 @@ 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 @@ async def build() -> None: 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 From f40b4fdd1188b4f664f62e290c482512df3ce890 Mon Sep 17 00:00:00 2001 From: Falko Schindler Date: Sat, 8 Jul 2023 15:42:59 +0200 Subject: [PATCH 11/24] fix pytests --- nicegui/elements/chart.py | 9 +++++---- nicegui/templates/index.html | 4 ++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/nicegui/elements/chart.py b/nicegui/elements/chart.py index 1fc0fd044..cd6715b11 100644 --- a/nicegui/elements/chart.py +++ b/nicegui/elements/chart.py @@ -1,17 +1,18 @@ from pathlib import Path from typing import Dict, List -from ..dependencies import register_library, register_vue_component +from ..dependencies import Library, register_library, register_vue_component from ..element import Element component = register_vue_component(Path('chart.js')) -core_dependencies: List[Path] = [] +core_dependencies: List[Library] = [] +extra_dependencies: Dict[str, Library] = {} 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)) + extra_dependencies[path.stem] = register_library(path.relative_to(base)) class Chart(Element): @@ -38,7 +39,7 @@ def __init__(self, options: Dict, *, type: str = 'chart', extras: List[str] = [] for dependency in core_dependencies: self.use_library(dependency) for extra in extras: - self.use_library(f'highcharts/modules/{extra}.js') + self.use_library(extra_dependencies[extra]) @property def options(self) -> Dict: diff --git a/nicegui/templates/index.html b/nicegui/templates/index.html index 501769863..4b8a79f80 100644 --- a/nicegui/templates/index.html +++ b/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, From 4c5745b1f9c46ef8a40dfa98dd278e8fcc1185dc Mon Sep 17 00:00:00 2001 From: Falko Schindler Date: Sun, 9 Jul 2023 18:07:44 +0200 Subject: [PATCH 12/24] register components and libraries while subclassing --- examples/custom_vue_component/counter.py | 9 ++---- examples/map/leaflet.py | 10 ++----- examples/single_page_app/router.py | 10 +++---- nicegui/dependencies.py | 18 ++++++++---- nicegui/element.py | 37 ++++++++++++++---------- nicegui/elements/aggrid.py | 11 ++----- nicegui/elements/audio.py | 8 ++--- nicegui/elements/chart.py | 23 ++++----------- nicegui/elements/chat_message.py | 9 ++---- nicegui/elements/choice_element.py | 2 +- nicegui/elements/colors.py | 10 ++----- nicegui/elements/dark_mode.py | 9 ++---- nicegui/elements/image.py | 9 ++---- nicegui/elements/input.py | 10 ++----- nicegui/elements/interactive_image.py | 8 ++--- nicegui/elements/joystick.py | 10 ++----- nicegui/elements/keyboard.py | 9 ++---- nicegui/elements/link.py | 9 ++---- nicegui/elements/log.py | 9 ++---- nicegui/elements/markdown.py | 13 +++------ nicegui/elements/mermaid.py | 16 ++++------ nicegui/elements/plotly.py | 10 ++----- nicegui/elements/query.py | 9 ++---- nicegui/elements/scene.py | 28 +++++++----------- nicegui/elements/select.py | 9 ++---- nicegui/elements/table.py | 10 ++----- nicegui/elements/upload.py | 9 ++---- nicegui/elements/video.py | 8 ++--- nicegui/functions/refreshable.py | 11 ++++--- website/intersection_observer.py | 9 ++---- 30 files changed, 113 insertions(+), 239 deletions(-) diff --git a/examples/custom_vue_component/counter.py b/examples/custom_vue_component/counter.py index 157bc6740..c2a23bdd2 100644 --- a/examples/custom_vue_component/counter.py +++ b/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') diff --git a/examples/map/leaflet.py b/examples/map/leaflet.py index 4cf694428..6f7ac8403 100644 --- a/examples/map/leaflet.py +++ b/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('') ui.add_head_html('') diff --git a/examples/single_page_app/router.py b/examples/single_page_app/router.py index 5ea9a3ac2..458a3e592 100644 --- a/examples/single_page_app/router.py +++ b/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 @@ async def build() -> None: 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 diff --git a/nicegui/dependencies.py b/nicegui/dependencies.py index 6279c3824..169ac8beb 100644 --- a/nicegui/dependencies.py +++ b/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});', diff --git a/nicegui/element.py b/nicegui/element.py index 9f2910dc8..a1330e645 100644 --- a/nicegui/element.py +++ b/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 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 @@ 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 @@ def __init__(self, tag: str, *, _client: Optional[Client] = None) -> None: 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 @@ def __init__(self, tag: str, *, _client: Optional[Client] = None) -> None: 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 @@ def delete(self) -> None: 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 diff --git a/nicegui/elements/aggrid.py b/nicegui/elements/aggrid.py index 62215a64a..059a743fc 100644 --- a/nicegui/elements/aggrid.py +++ b/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 @@ def __init__(self, options: Dict, *, html_columns: List[int] = [], theme: str = :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: diff --git a/nicegui/elements/audio.py b/nicegui/elements/audio.py index 77affd0c9..589d94971 100644 --- a/nicegui/elements/audio.py +++ b/nicegui/elements/audio.py @@ -3,13 +3,10 @@ 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 @@ def __init__(self, src: Union[str, Path], *, See `here `_ 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 @@ def __init__(self, src: Union[str, Path], *, 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' diff --git a/nicegui/elements/chart.py b/nicegui/elements/chart.py index cd6715b11..0325c1d5a 100644 --- a/nicegui/elements/chart.py +++ b/nicegui/elements/chart.py @@ -1,21 +1,14 @@ from pathlib import Path from typing import Dict, List -from ..dependencies import Library, 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[Library] = [] -extra_dependencies: Dict[str, Library] = {} -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): - extra_dependencies[path.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 @@ -31,15 +24,11 @@ def __init__(self, options: Dict, *, type: str = 'chart', extras: List[str] = [] :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(extra_dependencies[extra]) + self.libraries.extend(library for library in self.extra_libraries if library.path.stem in extras) @property def options(self) -> Dict: diff --git a/nicegui/elements/chat_message.py b/nicegui/elements/chat_message.py index fbee09789..523d15559 100644 --- a/nicegui/elements/chat_message.py +++ b/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 @@ def __init__(self, :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] diff --git a/nicegui/elements/choice_element.py b/nicegui/elements/choice_element.py index 1e1ce6653..c4647adb6 100644 --- a/nicegui/elements/choice_element.py +++ b/nicegui/elements/choice_element.py @@ -6,7 +6,7 @@ class ChoiceElement(ValueElement): def __init__(self, *, - tag: str, + tag: Optional[str] = None, options: Union[List, Dict], value: Any, on_change: Optional[Callable[..., Any]] = None, diff --git a/nicegui/elements/colors.py b/nicegui/elements/colors.py index fb2bd1414..fff7d2d7e 100644 --- a/nicegui/elements/colors.py +++ b/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 @@ def __init__(self, *, Sets the main colors (primary, secondary, accent, ...) used by `Quasar `_. """ - super().__init__(component.tag) - self.use_component(component) + super().__init__() self._props['primary'] = primary self._props['secondary'] = secondary self._props['accent'] = accent diff --git a/nicegui/elements/dark_mode.py b/nicegui/elements/dark_mode.py index bbd0f9c2e..eddab8f00 100644 --- a/nicegui/elements/dark_mode.py +++ b/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 @@ def __init__(self, value: Optional[bool] = False) -> None: :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.""" diff --git a/nicegui/elements/image.py b/nicegui/elements/image.py index a5d428da2..0f6f2a928 100644 --- a/nicegui/elements/image.py +++ b/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 @@ def __init__(self, source: Union[str, Path] = '') -> None: :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) diff --git a/nicegui/elements/input.py b/nicegui/elements/input.py index 01cf69ede..75a46e3f8 100644 --- a/nicegui/elements/input.py +++ b/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 @@ def __init__(self, :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 @@ def toggle_type(_): 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 diff --git a/nicegui/elements/interactive_image.py b/nicegui/elements/interactive_image.py index 3ed5d54c6..0cd20eecd 100644 --- a/nicegui/elements/interactive_image.py +++ b/nicegui/elements/interactive_image.py @@ -3,15 +3,12 @@ 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 @@ def __init__(self, :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: diff --git a/nicegui/elements/joystick.py b/nicegui/elements/joystick.py index 7d8d031ef..a345ae796 100644 --- a/nicegui/elements/joystick.py +++ b/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 @@ def __init__(self, *, :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 `_ """ - super().__init__('nicegui-joystick') - self.use_library(library) + super().__init__() self._props['options'] = options self.active = False diff --git a/nicegui/elements/keyboard.py b/nicegui/elements/keyboard.py index a1f3e41f0..8ccefd08e 100644 --- a/nicegui/elements/keyboard.py +++ b/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 @@ def __init__(self, :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: diff --git a/nicegui/elements/link.py b/nicegui/elements/link.py index c42158e21..0a7d96915 100644 --- a/nicegui/elements/link.py +++ b/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 @@ def __init__(self, :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 @@ def __init__(self, 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): diff --git a/nicegui/elements/log.py b/nicegui/elements/log.py index 80e51e689..09559c4ad 100644 --- a/nicegui/elements/log.py +++ b/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 @@ def __init__(self, max_lines: Optional[int] = None) -> None: :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: diff --git a/nicegui/elements/markdown.py b/nicegui/elements/markdown.py index 7d5db6c23..afb4db484 100644 --- a/nicegui/elements/markdown.py +++ b/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 @@ def __init__(self, content: str = '', *, extras: List[str] = ['fenced-code-block :param extras: list of `markdown2 extensions `_ (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)) diff --git a/nicegui/elements/mermaid.py b/nicegui/elements/mermaid.py index 281e893f7..8806688a9 100644 --- a/nicegui/elements/mermaid.py +++ b/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 @@ def __init__(self, content: str) -> None: :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() diff --git a/nicegui/elements/plotly.py b/nicegui/elements/plotly.py index 2696d4a40..78714bd6c 100644 --- a/nicegui/elements/plotly.py +++ b/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 @@ def __init__(self, figure: Union[Dict, go.Figure]) -> None: :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() diff --git a/nicegui/elements/query.py b/nicegui/elements/query.py index 8267cb356..f0339933a 100644 --- a/nicegui/elements/query.py +++ b/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: diff --git a/nicegui/elements/scene.py b/nicegui/elements/scene.py index e2abc8271..97d182df5 100644 --- a/nicegui/elements/scene.py +++ b/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 @@ def __init__(self, :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 @@ def __init__(self, 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 diff --git a/nicegui/elements/select.py b/nicegui/elements/select.py index da79fd0b1..edb5a012b 100644 --- a/nicegui/elements/select.py +++ b/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 @@ def __init__(self, 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: diff --git a/nicegui/elements/table.py b/nicegui/elements/table.py index 4c7c92ee7..c9fce88fc 100644 --- a/nicegui/elements/table.py +++ b/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 @@ def __init__(self, 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 @@ def handle_selection(e: GenericEventArguments) -> None: 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) diff --git a/nicegui/elements/upload.py b/nicegui/elements/upload.py index e7f18c063..d8035c90d 100644 --- a/nicegui/elements/upload.py +++ b/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 @@ def __init__(self, *, :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 diff --git a/nicegui/elements/video.py b/nicegui/elements/video.py index 91e19d3d4..e5e147dc3 100644 --- a/nicegui/elements/video.py +++ b/nicegui/elements/video.py @@ -3,13 +3,10 @@ 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 @@ def __init__(self, src: Union[str, Path], *, See `here `_ 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 @@ def __init__(self, src: Union[str, Path], *, 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' diff --git a/nicegui/functions/refreshable.py b/nicegui/functions/refreshable.py index c7564182e..c615c779b 100644 --- a/nicegui/functions/refreshable.py +++ b/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 @@ async def wait_for_result() -> None: 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 @@ def __get__(self, instance, _) -> Self: 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) diff --git a/website/intersection_observer.py b/website/intersection_observer.py index a82eedc44..0b219c3e5 100644 --- a/website/intersection_observer.py +++ b/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') From 9ce0b0e4a605d134c9c91062c36eebc50774fe55 Mon Sep 17 00:00:00 2001 From: Falko Schindler Date: Mon, 10 Jul 2023 06:53:55 +0200 Subject: [PATCH 13/24] fix test_chart --- tests/test_chart.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/test_chart.py b/tests/test_chart.py index 1a7930d2c..e8feebd30 100644 --- a/tests/test_chart.py +++ b/tests/test_chart.py @@ -73,18 +73,19 @@ def remove(): 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('/') - 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('/') - 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): From 3e1a023a2c7984917e23d57a5426c2f6dd9f519e Mon Sep 17 00:00:00 2001 From: Falko Schindler Date: Mon, 10 Jul 2023 10:53:28 +0200 Subject: [PATCH 14/24] refactoring --- nicegui/templates/index.html | 67 +++++++++++++++++++----------------- 1 file changed, 36 insertions(+), 31 deletions(-) diff --git a/nicegui/templates/index.html b/nicegui/templates/index.html index 4b8a79f80..5b8b4aa89 100644 --- a/nicegui/templates/index.html +++ b/nicegui/templates/index.html @@ -220,37 +220,42 @@ const transports = ['websocket', 'polling']; window.path_prefix = "{{ prefix | safe }}"; 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; - }); - }); - 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)); + 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), + }; + for (const [event, handler] of Object.entries(messageHandlers)) { + window.socket.on(event, handler); + } }, }).use(Quasar, { config: { From e9d14274b2a002e83ecd5cfffbed6923b141fd27 Mon Sep 17 00:00:00 2001 From: Falko Schindler Date: Mon, 10 Jul 2023 11:25:42 +0200 Subject: [PATCH 15/24] various corrections --- nicegui/dependencies.py | 12 ++++++++++-- nicegui/elements/mermaid.js | 2 +- nicegui/elements/textarea.py | 2 +- nicegui/templates/index.html | 20 +++++++++++++++++++- tests/test_mermaid.py | 1 + 5 files changed, 32 insertions(+), 5 deletions(-) diff --git a/nicegui/dependencies.py b/nicegui/dependencies.py index 169ac8beb..d3d16cfc2 100644 --- a/nicegui/dependencies.py +++ b/nicegui/dependencies.py @@ -17,6 +17,7 @@ class Component: key: str name: str + path: Path @property def tag(self) -> str: @@ -32,7 +33,7 @@ class VueComponent(Component): @dataclass(**KWONLY_SLOTS) class JsComponent(Component): - path: Path + pass @dataclass(**KWONLY_SLOTS) @@ -61,11 +62,16 @@ def register_vue_component(location: Path, base_path: Path = Path(__file__).pare """ path, key, name, suffix = deconstruct_location(location, base_path) if 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}' build = vbuild.VBuild(name, path.read_text()) - vue_components[key] = VueComponent(key=key, name=name, html=build.html, script=build.script, style=build.style) + vue_components[key] = VueComponent(key=key, name=name, path=path, + html=build.html, script=build.script, style=build.style) return vue_components[key] if 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] @@ -83,6 +89,8 @@ def register_library(location: Path, base_path: Path = Path(__file__).parent / ' """ path, key, name, suffix = deconstruct_location(location, base_path) if 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] diff --git a/nicegui/elements/mermaid.js b/nicegui/elements/mermaid.js index 06bbe0747..7b8e0be23 100644 --- a/nicegui/elements/mermaid.js +++ b/nicegui/elements/mermaid.js @@ -8,7 +8,7 @@ export default { async update(content) { this.$el.innerHTML = content; this.$el.removeAttribute("data-processed"); - await mermaid.run({ nodes: [this.$el] }); + this.$nextTick(() => mermaid.run({ nodes: [this.$el] })); }, }, props: { diff --git a/nicegui/elements/textarea.py b/nicegui/elements/textarea.py index 0c02ded20..cfcdbf976 100644 --- a/nicegui/elements/textarea.py +++ b/nicegui/elements/textarea.py @@ -3,7 +3,7 @@ from .input import Input -class Textarea(Input): +class Textarea(Input, component='input.js'): def __init__(self, label: Optional[str] = None, *, diff --git a/nicegui/templates/index.html b/nicegui/templates/index.html index 5b8b4aa89..92792d99d 100644 --- a/nicegui/templates/index.html +++ b/nicegui/templates/index.html @@ -192,6 +192,7 @@ async function loadDependencies(element) { for (const {name, key, tag} of element.components) { if (loaded_components.has(name)) continue; + if (key.endsWith('.vue')) continue; const component = (await import(`{{ prefix | safe }}/_nicegui/{{version}}/components/${key}`)).default; app = app.component(tag, component); loaded_components.add(name); @@ -253,8 +254,25 @@ 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, handler); + 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; + } + } + }); } }, }).use(Quasar, { diff --git a/tests/test_mermaid.py b/tests/test_mermaid.py index 6d77ade10..1e7b9eabe 100644 --- a/tests/test_mermaid.py +++ b/tests/test_mermaid.py @@ -53,5 +53,6 @@ def replace(): screen.open('/') screen.should_contain('Node_A') screen.click('Replace') + screen.wait(0.5) screen.should_contain('Node_B') screen.should_not_contain('Node_A') From e2674e1a45ba4c58a293f3bee2280f6b63872982 Mon Sep 17 00:00:00 2001 From: Falko Schindler Date: Tue, 11 Jul 2023 07:22:12 +0200 Subject: [PATCH 16/24] replace element.components with element.component --- nicegui/dependencies.py | 3 ++- nicegui/element.py | 19 ++++++++++++++----- nicegui/templates/index.html | 12 +++++------- 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/nicegui/dependencies.py b/nicegui/dependencies.py index d3d16cfc2..8f243c152 100644 --- a/nicegui/dependencies.py +++ b/nicegui/dependencies.py @@ -139,7 +139,8 @@ def generate_resources(prefix: str, elements: List[Element]) -> Tuple[List[str], 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 element.component: + component = element.component 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}";', diff --git a/nicegui/element.py b/nicegui/element.py index a1330e645..ffcaf5711 100644 --- a/nicegui/element.py +++ b/nicegui/element.py @@ -24,7 +24,7 @@ class Element(Visibility): - components: List[JsComponent] = [] + component: Optional[JsComponent] = None libraries: List[Library] = [] extra_libraries: List[Library] = [] exposed_libraries: List[Library] = [] @@ -42,7 +42,7 @@ def __init__(self, tag: Optional[str] = None, *, _client: Optional[Client] = Non self.client = _client or globals.get_client() self.id = self.client.next_element_id self.client.next_element_id += 1 - self.tag = tag if tag else self.components[0].tag if self.components else 'div' + self.tag = tag if tag else self.component.tag if self.component 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 @@ -72,7 +72,7 @@ def __init_subclass__(cls, *, ) -> None: super().__init_subclass__() base = Path(inspect.getfile(cls)).parent - cls.components = [register_vue_component(Path(component), base)] if component else [] + cls.component = register_vue_component(Path(component), base) if component else None 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] @@ -115,8 +115,17 @@ def _to_dict(self) -> Dict[str, Any]: 'text': self._text, 'slots': self._collect_slot_dict(), 'events': [listener.to_dict() for listener in self._event_listeners.values()], - '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], + '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 diff --git a/nicegui/templates/index.html b/nicegui/templates/index.html index 92792d99d..478371d2d 100644 --- a/nicegui/templates/index.html +++ b/nicegui/templates/index.html @@ -105,7 +105,7 @@ } // @todo: Try avoid this with better handling of initial page load. - element.components.forEach((component) => loaded_components.add(component.name)); + if (element.component) loaded_components.add(element.component.name); element.libraries.forEach((library) => loaded_libraries.add(library.name)); const props = { @@ -190,12 +190,10 @@ } async function loadDependencies(element) { - for (const {name, key, tag} of element.components) { - if (loaded_components.has(name)) continue; - if (key.endsWith('.vue')) continue; - const component = (await import(`{{ prefix | safe }}/_nicegui/{{version}}/components/${key}`)).default; - app = app.component(tag, component); - loaded_components.add(name); + if (element.component && !loaded_components.has(element.component.name) && !element.component.key.endsWith('.vue')) { + const component = (await import(`{{ prefix | safe }}/_nicegui/{{version}}/components/${element.component.key}`)).default; + app = app.component(element.component.tag, component); + loaded_components.add(element.component.name); } for (const {name, key} of element.libraries) { if (loaded_libraries.has(name)) continue; From 14a0dda98bdcfbe5835441824f1e3b54c7a24345 Mon Sep 17 00:00:00 2001 From: Falko Schindler Date: Tue, 11 Jul 2023 15:03:48 +0200 Subject: [PATCH 17/24] use hash of parent path as dependency identifier --- nicegui/dependencies.py | 67 ++++++++++++++++++------------------ nicegui/element.py | 11 +++--- nicegui/templates/index.html | 11 +++--- 3 files changed, 47 insertions(+), 42 deletions(-) diff --git a/nicegui/dependencies.py b/nicegui/dependencies.py index 8f243c152..e83271b53 100644 --- a/nicegui/dependencies.py +++ b/nicegui/dependencies.py @@ -1,5 +1,6 @@ from __future__ import annotations +import hashlib from dataclasses import dataclass from pathlib import Path from typing import TYPE_CHECKING, Dict, List, Set, Tuple @@ -49,59 +50,57 @@ class Library: libraries: Dict[str, Library] = {} -def register_vue_component(location: Path, base_path: Path = Path(__file__).parent / 'elements') -> Component: +def register_vue_component(path: Path) -> Component: """Register a .vue or .js Vue 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: registered component """ - path, key, name, suffix = deconstruct_location(location, base_path) - if suffix == '.vue': + 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}' - build = vbuild.VBuild(name, path.read_text()) - vue_components[key] = VueComponent(key=key, name=name, path=path, - html=build.html, script=build.script, style=build.style) + 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 suffix == '.js': + 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 "{suffix}"') - + raise ValueError(f'Unsupported component type "{path.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: registered library - """ - path, key, name, suffix = deconstruct_location(location, base_path) - if suffix in {'.js', '.mjs'}: +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 "{suffix}"') + 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. + """ + nicegui_base = Path(__file__).parent + if path.is_relative_to(nicegui_base): + path = path.relative_to(nicegui_base) + return f'{hashlib.sha256(str(path.parent).encode()).hexdigest()}/{path.name}' -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.""" - 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 get_name(path: Path) -> str: + return path.name.split('.', 1)[0] def generate_resources(prefix: str, elements: List[Element]) -> Tuple[List[str], @@ -137,14 +136,14 @@ def generate_resources(prefix: str, elements: List[Element]) -> Tuple[List[str], 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}";') + 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': - js_imports.extend([ - f'import {{ default as {component.name} }} from "{prefix}/_nicegui/{__version__}/components/{component.key}";', - f'app.component("{component.tag}", {component.name});', - ]) + 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 diff --git a/nicegui/element.py b/nicegui/element.py index ffcaf5711..b65242a5f 100644 --- a/nicegui/element.py +++ b/nicegui/element.py @@ -70,12 +70,15 @@ def __init_subclass__(cls, *, exposed_libraries: List[Union[str, Path]] = [], extra_libraries: List[Union[str, Path]] = [], ) -> None: + def abs_path(file: Union[str, Path]) -> Path: + p = Path(file) + return p if p.is_absolute() else base / p super().__init_subclass__() base = Path(inspect.getfile(cls)).parent - cls.component = register_vue_component(Path(component), base) if component else None - 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] + cls.component = register_vue_component(abs_path(component)) if component else None + cls.libraries = [register_library(abs_path(library)) for library in libraries] + cls.extra_libraries = [register_library(abs_path(library)) for library in extra_libraries] + cls.exposed_libraries = [register_library(abs_path(library), expose=True) for library in exposed_libraries] def add_slot(self, name: str, template: Optional[str] = None) -> Slot: """Add a slot to the element. diff --git a/nicegui/templates/index.html b/nicegui/templates/index.html index 478371d2d..7e03902ab 100644 --- a/nicegui/templates/index.html +++ b/nicegui/templates/index.html @@ -190,10 +190,13 @@ } async function loadDependencies(element) { - if (element.component && !loaded_components.has(element.component.name) && !element.component.key.endsWith('.vue')) { - const component = (await import(`{{ prefix | safe }}/_nicegui/{{version}}/components/${element.component.key}`)).default; - app = app.component(element.component.tag, component); - loaded_components.add(element.component.name); + 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; From 4210e7c0133dd38d77ae49211497ffd953c1d2e9 Mon Sep 17 00:00:00 2001 From: Falko Schindler Date: Tue, 11 Jul 2023 15:24:09 +0200 Subject: [PATCH 18/24] cleanup --- nicegui/elements/markdown.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/nicegui/elements/markdown.js b/nicegui/elements/markdown.js index 24c0198bb..24bcb9376 100644 --- a/nicegui/elements/markdown.js +++ b/nicegui/elements/markdown.js @@ -37,9 +37,3 @@ export default { }, }, }; - -function decodeHtml(html) { - const txt = document.createElement("textarea"); - txt.innerHTML = html; - return txt.value; -} From 258269873e99a730667b5eb09cc81977cdd9a33c Mon Sep 17 00:00:00 2001 From: Falko Schindler Date: Tue, 11 Jul 2023 15:50:27 +0200 Subject: [PATCH 19/24] avoid rendering mermaid twice --- nicegui/elements/mermaid.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/nicegui/elements/mermaid.js b/nicegui/elements/mermaid.js index 7b8e0be23..dd91556cc 100644 --- a/nicegui/elements/mermaid.js +++ b/nicegui/elements/mermaid.js @@ -1,14 +1,19 @@ import mermaid from "mermaid"; export default { template: `
`, + data: () => ({ + last_content: "", + }), mounted() { this.update(this.content); }, methods: { - async update(content) { + update(content) { + if (this.last_content === content) return; + this.last_content = content; this.$el.innerHTML = content; this.$el.removeAttribute("data-processed"); - this.$nextTick(() => mermaid.run({ nodes: [this.$el] })); + mermaid.run({ nodes: [this.$el] }); }, }, props: { From 78cb9a6d777ce2d52afcf41ff7e6f8f8b0a9e956 Mon Sep 17 00:00:00 2001 From: Falko Schindler Date: Tue, 11 Jul 2023 16:07:28 +0200 Subject: [PATCH 20/24] fix dependency key for Python 3.8 --- nicegui/dependencies.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/nicegui/dependencies.py b/nicegui/dependencies.py index e83271b53..f2c6be7de 100644 --- a/nicegui/dependencies.py +++ b/nicegui/dependencies.py @@ -94,8 +94,10 @@ def compute_key(path: Path) -> str: If the path is relative to the NiceGUI base directory, the key is computed from the relative path. """ nicegui_base = Path(__file__).parent - if path.is_relative_to(nicegui_base): + try: path = path.relative_to(nicegui_base) + except ValueError: + pass return f'{hashlib.sha256(str(path.parent).encode()).hexdigest()}/{path.name}' From d86cc1d9309a32212a7e220df9d19fd02c8131ac Mon Sep 17 00:00:00 2001 From: Falko Schindler Date: Tue, 11 Jul 2023 16:33:50 +0200 Subject: [PATCH 21/24] avoid running mermaid renderings simultaneously --- nicegui/elements/mermaid.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/nicegui/elements/mermaid.js b/nicegui/elements/mermaid.js index dd91556cc..37e9c9bef 100644 --- a/nicegui/elements/mermaid.js +++ b/nicegui/elements/mermaid.js @@ -1,4 +1,8 @@ import mermaid from "mermaid"; + +let is_running = false; +const queue = []; + export default { template: `
`, data: () => ({ @@ -8,12 +12,18 @@ export default { this.update(this.content); }, methods: { - update(content) { + async update(content) { if (this.last_content === content) return; this.last_content = content; this.$el.innerHTML = content; this.$el.removeAttribute("data-processed"); - 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: { From 873999e08ae8809480d4d5b393f46f37826c11df Mon Sep 17 00:00:00 2001 From: Falko Schindler Date: Tue, 11 Jul 2023 17:29:14 +0200 Subject: [PATCH 22/24] test dynamic creation of elements with dependencies --- tests/test_aggrid.py | 8 ++++++++ tests/test_chart.py | 8 ++++++++ tests/test_mermaid.py | 8 ++++++++ tests/test_plotly.py | 8 ++++++++ tests/test_scene.py | 8 ++++++++ 5 files changed, 40 insertions(+) diff --git a/tests/test_aggrid.py b/tests/test_aggrid.py index db30718e3..082745b3a 100644 --- a/tests/test_aggrid.py +++ b/tests/test_aggrid.py @@ -142,3 +142,11 @@ def test_create_from_pandas(screen: Screen): screen.should_contain('Bob') screen.should_contain('18') 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') diff --git a/tests/test_chart.py b/tests/test_chart.py index e8feebd30..31eebf94c 100644 --- a/tests/test_chart.py +++ b/tests/test_chart.py @@ -132,3 +132,11 @@ def test_stock_chart(screen: Screen): screen.wait(0.5) screen.should_not_contain('alice') 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') diff --git a/tests/test_mermaid.py b/tests/test_mermaid.py index 1e7b9eabe..138f3a9e7 100644 --- a/tests/test_mermaid.py +++ b/tests/test_mermaid.py @@ -56,3 +56,11 @@ def replace(): screen.wait(0.5) screen.should_contain('Node_B') 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') diff --git a/tests/test_plotly.py b/tests/test_plotly.py index c57e90188..222265a27 100644 --- a/tests/test_plotly.py +++ b/tests/test_plotly.py @@ -40,3 +40,11 @@ def replace(): screen.click('Replace') screen.wait(0.5) 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') diff --git a/tests/test_scene.py b/tests/test_scene.py index 0174e0c78..74e161f3b 100644 --- a/tests/test_scene.py +++ b/tests/test_scene.py @@ -93,3 +93,11 @@ def replace(): screen.click('Replace scene') screen.wait(0.5) assert screen.selenium.execute_script(f'return scene_c{scene.id}.children[4].name') == 'box' + + +def test_create_dynamically_s(screen: Screen): + ui.button('Create', on_click=lambda: ui.scene()) + + screen.open('/') + screen.click('Create') + assert screen.find_by_tag('canvas') From efb4026bd012d2af00cd45b99b944f99d7ef6565 Mon Sep 17 00:00:00 2001 From: Falko Schindler Date: Tue, 11 Jul 2023 17:34:47 +0200 Subject: [PATCH 23/24] test calling API method right after creating a new AG Grid --- tests/test_aggrid.py | 10 ++++++++++ tests/test_scene.py | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/test_aggrid.py b/tests/test_aggrid.py index 082745b3a..d5b3044ad 100644 --- a/tests/test_aggrid.py +++ b/tests/test_aggrid.py @@ -1,4 +1,5 @@ from selenium.webdriver.common.action_chains import ActionChains +from selenium.webdriver.common.by import By from selenium.webdriver.common.keys import Keys from nicegui import ui @@ -150,3 +151,12 @@ def test_create_dynamically(screen: Screen): 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') diff --git a/tests/test_scene.py b/tests/test_scene.py index 74e161f3b..5db72a06d 100644 --- a/tests/test_scene.py +++ b/tests/test_scene.py @@ -95,7 +95,7 @@ def replace(): assert screen.selenium.execute_script(f'return scene_c{scene.id}.children[4].name') == 'box' -def test_create_dynamically_s(screen: Screen): +def test_create_dynamically(screen: Screen): ui.button('Create', on_click=lambda: ui.scene()) screen.open('/') From 605ca9245cb83fe1824796193470f4d03343cda3 Mon Sep 17 00:00:00 2001 From: Falko Schindler Date: Tue, 11 Jul 2023 17:48:22 +0200 Subject: [PATCH 24/24] support globbing when defining dependencies --- nicegui/element.py | 33 ++++++++++++++++++++++++++------- nicegui/elements/chart.py | 10 ++++------ nicegui/elements/mermaid.py | 6 +----- 3 files changed, 31 insertions(+), 18 deletions(-) diff --git a/nicegui/element.py b/nicegui/element.py index b65242a5f..fa059c23b 100644 --- a/nicegui/element.py +++ b/nicegui/element.py @@ -70,15 +70,34 @@ def __init_subclass__(cls, *, exposed_libraries: List[Union[str, Path]] = [], extra_libraries: List[Union[str, Path]] = [], ) -> None: - def abs_path(file: Union[str, Path]) -> Path: - p = Path(file) - return p if p.is_absolute() else base / p super().__init_subclass__() base = Path(inspect.getfile(cls)).parent - cls.component = register_vue_component(abs_path(component)) if component else None - cls.libraries = [register_library(abs_path(library)) for library in libraries] - cls.extra_libraries = [register_library(abs_path(library)) for library in extra_libraries] - cls.exposed_libraries = [register_library(abs_path(library), expose=True) for library in exposed_libraries] + + 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: """Add a slot to the element. diff --git a/nicegui/elements/chart.py b/nicegui/elements/chart.py index 0325c1d5a..bfb1f3a34 100644 --- a/nicegui/elements/chart.py +++ b/nicegui/elements/chart.py @@ -1,14 +1,12 @@ -from pathlib import Path from typing import Dict, List from ..element import Element -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'))} - -class Chart(Element, component='chart.js', libraries=libraries, extra_libraries=list(modules.values())): +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: """Chart diff --git a/nicegui/elements/mermaid.py b/nicegui/elements/mermaid.py index 8806688a9..46f458eb3 100644 --- a/nicegui/elements/mermaid.py +++ b/nicegui/elements/mermaid.py @@ -1,14 +1,10 @@ -from pathlib import Path - from .mixins.content_element import ContentElement -base = Path(__file__).parent - 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')]): + extra_libraries=['lib/mermaid/*.js']): CONTENT_PROP = 'content' def __init__(self, content: str) -> None: