Repository: zauberzeug/nicegui
Pull request #3866 (2024-10-11 16:03:56)

Run dynamic argument conversion when using run_grid_method / run_row_method
Following up with https://github.com/zauberzeug/nicegui/pull/3757

Do dynamic property conversion when using `run_grid_method` / `run_row_method`, so you can dynamically add / change callbacks and other things using properties with `:` prefix.




Patch:
diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile
index cff50b49..e35defd5 100644
--- a/.devcontainer/Dockerfile
+++ b/.devcontainer/Dockerfile
@@ -29,6 +29,7 @@ ENV CHROME_BINARY_LOCATION=/usr/bin/chromium
 RUN pip install -U pip && pip install poetry==$POETRY_VERSION
 COPY pyproject.toml poetry.lock README.md ./
 RUN poetry install --all-extras --no-root
+RUN pip install latex2mathml
 
 USER $USERNAME
 
diff --git a/development.dockerfile b/development.dockerfile
index eaa60e34..c4c17109 100644
--- a/development.dockerfile
+++ b/development.dockerfile
@@ -13,4 +13,6 @@ WORKDIR /app
 COPY . .
 RUN poetry install --all-extras
 
+RUN pip install latex2mathml
+
 CMD python3 -m debugpy --listen 5678 main.py
diff --git a/fly.dockerfile b/fly.dockerfile
index 283057cd..8357e3b9 100644
--- a/fly.dockerfile
+++ b/fly.dockerfile
@@ -29,6 +29,8 @@ COPY pyproject.toml poetry.lock*  ./
 
 RUN poetry install --no-root --extras "plotly matplotlib highcharts sass"
 
+RUN pip install latex2mathml
+
 ADD . .
 
 # ensure unique version to not serve cached and hence potentially wrong static files
diff --git a/nicegui/app/app.py b/nicegui/app/app.py
index 4cd7e29e..312a6402 100644
--- a/nicegui/app/app.py
+++ b/nicegui/app/app.py
@@ -9,7 +9,6 @@ from typing import Any, Awaitable, Callable, List, Optional, Union
 
 from fastapi import FastAPI, HTTPException, Request, Response
 from fastapi.responses import FileResponse
-from fastapi.staticfiles import StaticFiles
 
 from .. import background_tasks, helpers
 from ..client import Client
@@ -17,6 +16,7 @@ from ..logging import log
 from ..native import NativeConfig
 from ..observables import ObservableSet
 from ..server import Server
+from ..staticfiles import CacheControlledStaticFiles
 from ..storage import Storage
 from .app_config import AppConfig
 from .range_response import get_range_response
@@ -157,7 +157,7 @@ class App(FastAPI):
         if url_path == '/':
             raise ValueError('''Path cannot be "/", because it would hide NiceGUI's internal "/_nicegui" route.''')
 
-        handler = StaticFiles(directory=local_directory, follow_symlink=follow_symlink)
+        handler = CacheControlledStaticFiles(directory=local_directory, follow_symlink=follow_symlink)
 
         @self.get(url_path + '/{path:path}')
         async def static_file(request: Request, path: str = '') -> Response:
diff --git a/nicegui/elements/aggrid.js b/nicegui/elements/aggrid.js
index 4b280c11..ce444184 100644
--- a/nicegui/elements/aggrid.js
+++ b/nicegui/elements/aggrid.js
@@ -46,9 +46,11 @@ export default {
       this.api.addGlobalListener(this.handle_event);
     },
     run_grid_method(name, ...args) {
+      convertDynamicProperties(args, true);
       return runMethod(this.api, name, args);
     },
     run_row_method(row_id, name, ...args) {
+      convertDynamicProperties(args, true);
       return runMethod(this.api.getRowNode(row_id), name, args);
     },
     handle_event(type, args) {
diff --git a/nicegui/elements/interactive_image.js b/nicegui/elements/interactive_image.js
index 37c68f4d..47985552 100644
--- a/nicegui/elements/interactive_image.js
+++ b/nicegui/elements/interactive_image.js
@@ -11,9 +11,10 @@ export default {
         draggable="false"
       />
       <svg ref="svg" style="position:absolute;top:0;left:0;pointer-events:none" :viewBox="viewBox">
-        <g v-if="cross" :style="{ display: showCross ? 'block' : 'none' }">
-          <line :x1="x" y1="0" :x2="x" y2="100%" :stroke="cross === true ? 'black' : cross" />
-          <line x1="0" :y1="y" x2="100%" :y2="y" :stroke="cross === true ? 'black' : cross" />
+        <g :style="{ display: showCross ? 'block' : 'none' }">
+          <line v-if="cross" :x1="x" y1="0" :x2="x" y2="100%" :stroke="cross === true ? 'black' : cross" />
+          <line v-if="cross" x1="0" :y1="y" x2="100%" :y2="y" :stroke="cross === true ? 'black' : cross" />
+          <slot name="cross" :x="x" :y="y"></slot>
         </g>
         <g v-html="content"></g>
       </svg>
@@ -118,7 +119,7 @@ export default {
   },
   computed: {
     onCrossEvents() {
-      if (!this.cross) return {};
+      if (!this.cross && !this.$slots.cross) return {};
       return {
         mouseenter: () => (this.showCross = true),
         mouseleave: () => (this.showCross = false),
diff --git a/nicegui/elements/scene_object3d.py b/nicegui/elements/scene_object3d.py
index 5cf508bc..67a133ed 100644
--- a/nicegui/elements/scene_object3d.py
+++ b/nicegui/elements/scene_object3d.py
@@ -196,10 +196,14 @@ class Object3D:
             self._draggable()
         return self
 
+    @property
+    def children(self) -> List[Object3D]:
+        """List of children of the object."""
+        return [object for object in self.scene.objects.values() if object.parent == self]
+
     def delete(self) -> None:
         """Delete the object."""
-        children = [object for object in self.scene.objects.values() if object.parent == self]
-        for child in children:
+        for child in self.children:
             child.delete()
         del self.scene.objects[self.id]
         self._delete()
diff --git a/nicegui/nicegui.py b/nicegui/nicegui.py
index 72878a9d..456027eb 100644
--- a/nicegui/nicegui.py
+++ b/nicegui/nicegui.py
@@ -9,7 +9,6 @@ import socketio
 from fastapi import HTTPException, Request
 from fastapi.middleware.gzip import GZipMiddleware
 from fastapi.responses import FileResponse, Response
-from fastapi.staticfiles import StaticFiles
 
 from . import air, background_tasks, binding, core, favicon, helpers, json, run, welcome
 from .app import App
@@ -21,6 +20,7 @@ from .logging import log
 from .middlewares import RedirectWithPrefixMiddleware
 from .page import page
 from .slot import Slot
+from .staticfiles import CacheControlledStaticFiles
 from .version import __version__
 
 
@@ -56,7 +56,7 @@ mimetypes.add_type('text/css', '.css')
 
 app.add_middleware(GZipMiddleware)
 app.add_middleware(RedirectWithPrefixMiddleware)
-static_files = StaticFiles(
+static_files = CacheControlledStaticFiles(
     directory=(Path(__file__).parent / 'static').resolve(),
     follow_symlink=True,
 )
diff --git a/nicegui/staticfiles.py b/nicegui/staticfiles.py
new file mode 100644
index 00000000..731beaba
--- /dev/null
+++ b/nicegui/staticfiles.py
@@ -0,0 +1,11 @@
+from starlette.responses import Response
+from starlette.staticfiles import StaticFiles
+from starlette.types import Scope
+
+
+class CacheControlledStaticFiles(StaticFiles):
+
+    async def get_response(self, path: str, scope: Scope) -> Response:
+        response = await super().get_response(path, scope)
+        response.headers['Cache-Control'] = 'public, max-age=3600'
+        return response
diff --git a/release.dockerfile b/release.dockerfile
index eea0b52f..12506482 100644
--- a/release.dockerfile
+++ b/release.dockerfile
@@ -26,6 +26,7 @@ RUN python -m pip install \
     itsdangerous \
     pytest \
     requests \
+    latex2mathml \
     selenium
 
 WORKDIR /app
diff --git a/website/documentation/content/interactive_image_documentation.py b/website/documentation/content/interactive_image_documentation.py
index c269c8c7..d5ea67d2 100644
--- a/website/documentation/content/interactive_image_documentation.py
+++ b/website/documentation/content/interactive_image_documentation.py
@@ -65,10 +65,19 @@ def loaded_event():
 @doc.demo('Crosshairs', '''
     You can show crosshairs by passing `cross=True`.
     You can also change the color of the crosshairs by passing a color string.
+
+    Alternatively, you can use the `add_slot` method to add a custom "cross" slot with your own SVG template.
+    The `props.x` and `props.y` variables will be available in the template, representing the crosshair position.
 ''')
 def crosshairs():
     ui.interactive_image('https://picsum.photos/id/565/640/360', cross='red')
 
+    ui.interactive_image('https://picsum.photos/id/565/640/360').add_slot('cross', '''
+        <circle :cx="props.x" :cy="props.y" r="30" stroke="red" fill="none" />
+        <line :x1="props.x - 30" :y1="props.y" :x2="props.x + 30" :y2="props.y" stroke="red" />
+        <line :x1="props.x" :y1="props.y - 30" :x2="props.x" :y2="props.y + 30" stroke="red" />
+    ''')
+
 
 @doc.demo('SVG events', '''
     You can subscribe to events of the SVG elements by using the `on` method with an "svg:" prefix.
diff --git a/website/documentation/content/markdown_documentation.py b/website/documentation/content/markdown_documentation.py
index 2a1b80ae..28d7532b 100644
--- a/website/documentation/content/markdown_documentation.py
+++ b/website/documentation/content/markdown_documentation.py
@@ -55,6 +55,18 @@ def markdown_tables():
     ''', extras=['tables'])
 
 
+@doc.demo('LaTeX formulas', '''
+    By activating the "latex" extra, you can use LaTeX formulas.
+    This requires markdown2 version >=2.5 as well as latex2mathml to be installed.
+''')
+def markdown_latex():
+    ui.markdown(r'''
+        Euler's identity:
+
+        $$e^{i\pi} = -1$$
+    ''', extras=['latex'])
+
+
 @doc.demo('Change Markdown content', '''
     You can change the content of a Markdown element by setting its `content` property or calling `set_content`.
 ''')
diff --git a/website/documentation/content/storage_documentation.py b/website/documentation/content/storage_documentation.py
index 02df4340..87794941 100644
--- a/website/documentation/content/storage_documentation.py
+++ b/website/documentation/content/storage_documentation.py
@@ -17,7 +17,7 @@ doc.title('Storage')
     It features five built-in storage types:
 
     - `app.storage.tab`:
-        Stored server-side in memory, this dictionary is unique to each tab session and can hold arbitrary objects.
+        Stored server-side in memory, this dictionary is unique to each non-duplicated tab session and can hold arbitrary objects.
         Data will be lost when restarting the server until <https://github.com/zauberzeug/nicegui/discussions/2841> is implemented.
         This storage is only available within [page builder functions](/documentation/page)
         and requires an established connection, obtainable via [`await client.connected()`](/documentation/page#wait_for_client_connection).


Test patch:
diff --git a/tests/test_serving_files.py b/tests/test_serving_files.py
index 131dd2f0..ebca6e85 100644
--- a/tests/test_serving_files.py
+++ b/tests/test_serving_files.py
@@ -61,6 +61,7 @@ def test_get_from_static_files_dir(url_path: str, screen: Screen):
     with httpx.Client() as http_client:
         r = http_client.get(f'http://localhost:{Screen.PORT}/static/examples/slideshow/slides/slide1.jpg')
         assert r.status_code == 200
+        assert 'max-age=' in r.headers['Cache-Control']
 
 
 def test_404_for_non_existing_static_file(screen: Screen):