Pārlūkot izejas kodu

Unify declaration of third-party dependencies (#2991)

* use "exposes_libraries" instead of "libraries"

* use "exposes_libraries" instead of "extra_libraries"

* add example "signature-pad" on how to use npm dependencies

* try to create example with npm and webpack

* fix import

* reserve bottom space for error message (#3446)

* Revert "reserve bottom space for error message (#3446)"

This reverts commit d405d0d3930876bcbc66a727a91393a06e90be18.

* fix is_even example

* introduce `dependencies` parameter

* improve READMEs

* add new examples to examples.py

* add section about custom components to the documentation
Falko Schindler 9 mēneši atpakaļ
vecāks
revīzija
baa97acfe8

+ 2 - 0
examples/node_module_integration/.gitignore

@@ -0,0 +1,2 @@
+node_modules/
+package-lock.json

+ 30 - 0
examples/node_module_integration/README.md

@@ -0,0 +1,30 @@
+# Use Bundled Node Modules as Third-party Dependencies
+
+This example demonstrates how to use multiple third-party node modules as dependencies in a NiceGUI app.
+The app uses the [is-odd](https://www.npmjs.com/package/is-odd) node modules to check if a number is even or odd.
+We chose this package to demonstrate a very simple node module which has a dependency itself,
+namely the [is-number](https://www.npmjs.com/package/is-number) package.
+Using NPM, we can easily install both packages and bundle them into a single file which can be used in the app.
+The package.json file defines the is-odd dependency and some dev dependencies for bundling the node module,
+the webpack.config.js file specifies the entry point for the node module,
+and number_checker.js as well as number_checker.py define a new UI element to be used in the NiceGUI app main.py.
+
+1. First, install all third-party node modules (assuming you have NPM installed):
+
+   ```bash
+   npm install
+   ```
+
+   This will create a node_modules directory containing the is-odd and is-number modules as well as some dev dependencies.
+
+2. Now bundle the node module:
+
+   ```bash
+   npm run build
+   ```
+
+3. Finally, you can run the app as usual:
+
+   ```bash
+   python3 main.py
+   ```

+ 19 - 0
examples/node_module_integration/main.py

@@ -0,0 +1,19 @@
+#!/usr/bin/env python3
+from number_checker import NumberChecker
+
+from nicegui import ui
+
+
+@ui.page('/')
+def page():
+    number_checker = NumberChecker()
+    number = ui.number(value=42.0)
+
+    async def check():
+        even = await number_checker.is_even(number.value)
+        ui.notify(f'{number.value} is {"even" if even else "odd"}')
+
+    ui.button('Check', on_click=check)
+
+
+ui.run()

+ 13 - 0
examples/node_module_integration/number_checker.js

@@ -0,0 +1,13 @@
+export default {
+  async mounted() {
+    await import("is-odd");
+  },
+  methods: {
+    isOdd(number) {
+      return isOdd(number);
+    },
+    isEven(number) {
+      return !isOdd(number);
+    },
+  },
+};

+ 19 - 0
examples/node_module_integration/number_checker.py

@@ -0,0 +1,19 @@
+from nicegui import ui
+
+
+class NumberChecker(ui.element, component='number_checker.js', dependencies=['dist/is-odd.js']):
+
+    def __init__(self) -> None:
+        """NumberChecker
+
+        A number checker based on the `is-odd <https://www.npmjs.com/package/is-odd>`_ NPM package.
+        """
+        super().__init__()
+
+    async def is_odd(self, number: int) -> bool:
+        """Check if a number is odd."""
+        return await self.run_method('isOdd', number)
+
+    async def is_even(self, number: int) -> bool:
+        """Check if a number is even."""
+        return await self.run_method('isEven', number)

+ 15 - 0
examples/node_module_integration/package.json

@@ -0,0 +1,15 @@
+{
+  "scripts": {
+    "build": "webpack --config webpack.config.js"
+  },
+  "dependencies": {
+    "is-odd": "^3.0.1"
+  },
+  "devDependencies": {
+    "@babel/core": "^7.24.5",
+    "@babel/preset-env": "^7.24.5",
+    "babel-loader": "^9.1.3",
+    "webpack": "^5.91.0",
+    "webpack-cli": "^5.1.4"
+  }
+}

+ 26 - 0
examples/node_module_integration/webpack.config.js

@@ -0,0 +1,26 @@
+const path = require("path");
+
+module.exports = {
+  entry: "is-odd/index.js",
+  mode: "development",
+  output: {
+    path: path.resolve(__dirname, "dist"),
+    filename: "is-odd.js",
+    library: "isOdd",
+    libraryTarget: "umd",
+  },
+  module: {
+    rules: [
+      {
+        test: /\.js$/,
+        exclude: /node_modules/,
+        use: {
+          loader: "babel-loader",
+          options: {
+            presets: ["@babel/preset-env"],
+          },
+        },
+      },
+    ],
+  },
+};

+ 2 - 0
examples/signature_pad/.gitignore

@@ -0,0 +1,2 @@
+node_modules/
+package-lock.json

+ 20 - 0
examples/signature_pad/README.md

@@ -0,0 +1,20 @@
+# Use Node Modules as Third-party Dependencies
+
+This example demonstrates how to use third-party node modules as dependencies in a NiceGUI app.
+The app uses the [signature_pad](https://www.npmjs.com/package/signature_pad) node module to create a signature pad.
+In package.json, the signature_pad module is listed as a dependency,
+while signature_pad.js and signature_pad.py define the new UI element which can be used in main.py.
+
+1. First, install the third-party node modules (assuming you have NPM installed):
+
+   ```bash
+   npm install
+   ```
+
+   This will create a node_modules directory containing the signature_pad module.
+
+2. Now you can run the app as usual:
+
+   ```bash
+   python3 main.py
+   ```

+ 9 - 0
examples/signature_pad/main.py

@@ -0,0 +1,9 @@
+#!/usr/bin/env python3
+from signature_pad import SignaturePad
+
+from nicegui import ui
+
+pad = SignaturePad().classes('border')
+ui.button('Clear', on_click=pad.clear)
+
+ui.run()

+ 5 - 0
examples/signature_pad/package.json

@@ -0,0 +1,5 @@
+{
+  "dependencies": {
+    "signature_pad": "^4.1.7"
+  }
+}

+ 16 - 0
examples/signature_pad/signature_pad.js

@@ -0,0 +1,16 @@
+import SignaturePad from "signature_pad";
+
+export default {
+  template: "<canvas></canvas>",
+  props: {
+    options: Array,
+  },
+  mounted() {
+    this.pad = new SignaturePad(this.$el, this.options);
+  },
+  methods: {
+    clear() {
+      this.pad.clear();
+    },
+  },
+};

+ 20 - 0
examples/signature_pad/signature_pad.py

@@ -0,0 +1,20 @@
+from typing import Dict, Optional
+
+from nicegui import ui
+
+
+class SignaturePad(ui.element,
+                   component='signature_pad.js',
+                   dependencies=['node_modules/signature_pad/dist/signature_pad.min.js']):
+
+    def __init__(self, options: Optional[Dict] = None) -> None:
+        """SignaturePad
+
+        An element that integrates the `Signature Pad library <https://szimek.github.io/signature_pad/>`_.
+        """
+        super().__init__()
+        self._props['options'] = options or {}
+
+    def clear(self):
+        """Clear the signature."""
+        self.run_method('clear')

+ 15 - 4
nicegui/element.py

@@ -107,9 +107,10 @@ class Element(Visibility):
 
     def __init_subclass__(cls, *,
                           component: Union[str, Path, None] = None,
-                          libraries: List[Union[str, Path]] = [],  # noqa: B006
-                          exposed_libraries: List[Union[str, Path]] = [],  # noqa: B006
-                          extra_libraries: List[Union[str, Path]] = [],  # noqa: B006
+                          dependencies: List[Union[str, Path]] = [],  # noqa: B006
+                          libraries: List[Union[str, Path]] = [],  # noqa: B006  # DEPRECATED
+                          exposed_libraries: List[Union[str, Path]] = [],  # noqa: B006  # DEPRECATED
+                          extra_libraries: List[Union[str, Path]] = [],  # noqa: B006  # DEPRECATED
                           ) -> None:
         super().__init_subclass__()
         base = Path(inspect.getfile(cls)).parent
@@ -120,6 +121,16 @@ class Element(Visibility):
                 path = base / path
             return sorted(path.parent.glob(path.name), key=lambda p: p.stem)
 
+        if libraries:
+            helpers.warn_once('The `libraries` parameter is deprecated. Use `dependencies` instead.',
+                              stack_info=True)
+        if exposed_libraries:
+            helpers.warn_once('The `exposed_libraries` parameter is deprecated. Use `dependencies` instead.',
+                              stack_info=True)
+        if extra_libraries:
+            helpers.warn_once('The `extra_libraries` parameter is deprecated. Use `dependencies` instead.',
+                              stack_info=True)
+
         cls.component = copy(cls.component)
         cls.libraries = copy(cls.libraries)
         cls.extra_libraries = copy(cls.extra_libraries)
@@ -133,7 +144,7 @@ class Element(Visibility):
         for library in extra_libraries:
             for path in glob_absolute_paths(library):
                 cls.extra_libraries.append(register_library(path))
-        for library in exposed_libraries:
+        for library in exposed_libraries + dependencies:
             for path in glob_absolute_paths(library):
                 cls.exposed_libraries.append(register_library(path, expose=True))
 

+ 1 - 0
nicegui/elements/aggrid.js

@@ -1,3 +1,4 @@
+import "ag-grid-community";
 import { convertDynamicProperties } from "../../static/utils/dynamic_properties.js";
 
 export default {

+ 1 - 1
nicegui/elements/aggrid.py

@@ -13,7 +13,7 @@ except ImportError:
     pass
 
 
-class AgGrid(Element, component='aggrid.js', libraries=['lib/aggrid/ag-grid-community.min.js']):
+class AgGrid(Element, component='aggrid.js', dependencies=['lib/aggrid/ag-grid-community.min.js']):
 
     def __init__(self,
                  options: Dict, *,

+ 5 - 0
nicegui/elements/echart.js

@@ -1,9 +1,13 @@
+import "echarts";
 import { convertDynamicProperties } from "../../static/utils/dynamic_properties.js";
 
 export default {
   template: "<div></div>",
   async mounted() {
     await this.$nextTick(); // wait for Tailwind classes to be applied
+    if (this.enable_3d) {
+      await import("echarts-gl");
+    }
 
     this.chart = echarts.init(this.$el);
     this.chart.on("click", (e) => this.$emit("pointClick", e));
@@ -81,5 +85,6 @@ export default {
   },
   props: {
     options: Object,
+    enable_3d: Boolean,
   },
 };

+ 2 - 5
nicegui/elements/echart.py

@@ -17,7 +17,7 @@ except ImportError:
     pass
 
 
-class EChart(Element, component='echart.js', libraries=['lib/echarts/echarts.min.js'], extra_libraries=['lib/echarts-gl/echarts-gl.min.js']):
+class EChart(Element, component='echart.js', dependencies=['lib/echarts/echarts.min.js', 'lib/echarts-gl/echarts-gl.min.js']):
 
     def __init__(self, options: Dict, on_point_click: Optional[Callable] = None, *, enable_3d: bool = False) -> None:
         """Apache EChart
@@ -32,11 +32,8 @@ class EChart(Element, component='echart.js', libraries=['lib/echarts/echarts.min
         """
         super().__init__()
         self._props['options'] = options
+        self._props['enable_3d'] = enable_3d or any('3D' in key for key in options)
         self._classes.append('nicegui-echart')
-        for key in options:
-            if '3D' in key or enable_3d:
-                self.libraries.extend(library for library in self.extra_libraries if library.name == 'echarts-gl')
-                break
 
         if on_point_click:
             self.on_point_click(on_point_click)

+ 1 - 1
nicegui/elements/joystick.py

@@ -6,7 +6,7 @@ from ..element import Element
 from ..events import GenericEventArguments, JoystickEventArguments, handle_event
 
 
-class Joystick(Element, component='joystick.vue', libraries=['lib/nipplejs/nipplejs.js']):
+class Joystick(Element, component='joystick.vue', dependencies=['lib/nipplejs/nipplejs.js']):
 
     def __init__(self, *,
                  on_start: Optional[Callable[..., Any]] = None,

+ 2 - 1
nicegui/elements/joystick.vue

@@ -4,7 +4,8 @@
 
 <script>
 export default {
-  mounted() {
+  async mounted() {
+    await import("nipplejs");
     const joystick = nipplejs.create({
       zone: this.$el.children[0],
       position: { left: "50%", top: "50%" },

+ 1 - 1
nicegui/elements/json_editor.py

@@ -7,7 +7,7 @@ from ..element import Element
 from ..events import GenericEventArguments, JsonEditorChangeEventArguments, JsonEditorSelectEventArguments, handle_event
 
 
-class JsonEditor(Element, component='json_editor.js', exposed_libraries=['lib/vanilla-jsoneditor/index.js']):
+class JsonEditor(Element, component='json_editor.js', dependencies=['lib/vanilla-jsoneditor/index.js']):
 
     def __init__(self,
                  properties: Dict, *,

+ 4 - 2
nicegui/elements/mermaid.py

@@ -5,8 +5,10 @@ from .mixins.content_element import ContentElement
 
 class Mermaid(ContentElement,
               component='mermaid.js',
-              exposed_libraries=['lib/mermaid/mermaid.esm.min.mjs'],
-              extra_libraries=['lib/mermaid/*.js']):
+              dependencies=[
+                  'lib/mermaid/*.js',
+                  'lib/mermaid/mermaid.esm.min.mjs',
+              ]):
     CONTENT_PROP = 'content'
 
     def __init__(self, content: str, config: Optional[Dict] = None) -> None:

+ 1 - 1
nicegui/elements/plotly.py

@@ -12,7 +12,7 @@ except ImportError:
     pass
 
 
-class Plotly(Element, component='plotly.vue', libraries=['lib/plotly/plotly.min.js']):
+class Plotly(Element, component='plotly.vue', dependencies=['lib/plotly/plotly.min.js']):
 
     def __init__(self, figure: Union[Dict, go.Figure]) -> None:
         """Plotly Element

+ 1 - 1
nicegui/elements/plotly.vue

@@ -5,7 +5,7 @@
 <script>
 export default {
   async mounted() {
-    await this.$nextTick();
+    await import("plotly");
     this.update();
   },
   updated() {

+ 2 - 1
nicegui/elements/scene.js

@@ -2,9 +2,10 @@ import * as THREE from "three";
 import { CSS2DRenderer, CSS2DObject } from "CSS2DRenderer";
 import { CSS3DRenderer, CSS3DObject } from "CSS3DRenderer";
 import { DragControls } from "DragControls";
+import { GLTFLoader } from "GLTFLoader";
 import { OrbitControls } from "OrbitControls";
 import { STLLoader } from "STLLoader";
-import { GLTFLoader } from "GLTFLoader";
+import "tween";
 
 function texture_geometry(coords) {
   const geometry = new THREE.BufferGeometry();

+ 4 - 4
nicegui/elements/scene.py

@@ -40,16 +40,16 @@ class SceneObject:
 
 class Scene(Element,
             component='scene.js',
-            libraries=['lib/tween/tween.umd.js'],
-            exposed_libraries=[
+            dependencies=[
                 'lib/three/three.module.js',
+                'lib/three/modules/BufferGeometryUtils.js',
                 'lib/three/modules/CSS2DRenderer.js',
                 'lib/three/modules/CSS3DRenderer.js',
                 'lib/three/modules/DragControls.js',
+                'lib/three/modules/GLTFLoader.js',
                 'lib/three/modules/OrbitControls.js',
                 'lib/three/modules/STLLoader.js',
-                'lib/three/modules/GLTFLoader.js',
-                'lib/three/modules/BufferGeometryUtils.js',
+                'lib/tween/tween.umd.js',
             ]):
     # pylint: disable=import-outside-toplevel
     from .scene_objects import Box as box

+ 4 - 2
nicegui/elements/scene_view.py

@@ -11,8 +11,10 @@ from .scene import Scene, SceneCamera
 
 class SceneView(Element,
                 component='scene_view.js',
-                libraries=['lib/tween/tween.umd.js'],
-                exposed_libraries=['lib/three/three.module.js']):
+                dependencies=[
+                    'lib/tween/tween.umd.js',
+                    'lib/three/three.module.js',
+                ]):
 
     def __init__(self,
                  scene: Scene,

+ 13 - 0
website/documentation/content/section_configuration_deployment.py

@@ -79,6 +79,19 @@ def env_var_demo():
     ui.label(f'Markdown content cache size is {markdown.prepare_content.cache_info().maxsize}')
 
 
+doc.text('Custom Vue Components', '''
+    You can create custom components by subclassing `ui.element` and implementing a corresponding Vue component.
+    The ["Custom Vue components" example](https://github.com/zauberzeug/nicegui/tree/main/examples/custom_vue_component)
+    demonstrates how to create a custom counter component which emits events and receives updates from the server.
+
+    The ["Signature pad" example](https://github.com/zauberzeug/nicegui/blob/main/examples/signature_pad)
+    shows how to define dependencies for a custom component using a `package.json` file.
+    This allows you to use third-party libraries via NPM in your component.
+
+    Last but not least, the ["Node module integration" example](https://github.com/zauberzeug/nicegui/blob/main/examples/node_module_integration)
+    demonstrates how to create a package.json file and a webpack.config.js file to bundle a custom Vue component with its dependencies.
+''')
+
 doc.text('Server Hosting', '''
     To deploy your NiceGUI app on a server, you will need to execute your `main.py` (or whichever file contains your `ui.run(...)`) on your cloud infrastructure.
     You can, for example, just install the [NiceGUI python package via pip](https://pypi.org/project/nicegui/) and use systemd or similar service to start the main script.

+ 2 - 0
website/examples.py

@@ -68,4 +68,6 @@ examples: List[Example] = [
     Example('Audio Recorder', 'Record audio, play it back or download it'),
     Example('ZeroMQ', 'Simple ZeroMQ PUSH/PULL server and client'),
     Example('NGINX HTTPS', 'Use NGINX to serve a NiceGUI app with HTTPS'),
+    Example('Node Module Integration', 'Use NPM to add dependencies to a NiceGUI app'),
+    Example('Signature Pad', 'A custom element based on [signature_pad](https://www.npmjs.com/package/signature_pad'),
 ]