diff --git a/docs/userman/gui/extension/dynamic_element/chess_game-d.png b/docs/userman/gui/extension/dynamic_element/chess_game-d.png new file mode 100644 index 000000000..6a6d44173 Binary files /dev/null and b/docs/userman/gui/extension/dynamic_element/chess_game-d.png differ diff --git a/docs/userman/gui/extension/dynamic_element/chess_game-l.png b/docs/userman/gui/extension/dynamic_element/chess_game-l.png new file mode 100644 index 000000000..46d972390 Binary files /dev/null and b/docs/userman/gui/extension/dynamic_element/chess_game-l.png differ diff --git a/docs/userman/gui/extension/dynamic_element/index.md b/docs/userman/gui/extension/dynamic_element/index.md index 8691c4325..72ee1ad73 100644 --- a/docs/userman/gui/extension/dynamic_element/index.md +++ b/docs/userman/gui/extension/dynamic_element/index.md @@ -355,4 +355,5 @@ Here are the sections that address the different use cases that your custom element may need: - [Scalar properties](scalar_props.md) +- [Tabular data properties](tabular_data_props.md) diff --git a/docs/userman/gui/extension/dynamic_element/tabular_data_props.md b/docs/userman/gui/extension/dynamic_element/tabular_data_props.md new file mode 100644 index 000000000..21690dc82 --- /dev/null +++ b/docs/userman/gui/extension/dynamic_element/tabular_data_props.md @@ -0,0 +1,219 @@ +The previous section on [Scalar properties](scalar_props.md) shows how to create a dynamic element that holds a scalar +value. However, when dealing with collections of data, such as tabular data, we need a more complex approach to support +arrays or tables that can be dynamically updated. + +In Taipy GUI, tabular data can also be bound to Python variables or expressions, allowing the user interface to instantly +reflect any changes in the underlying data. Custom elements that manage tabular data must declare their properties to +specify the type of data they support, similar to scalar properties, but now using a structure suitable for collections. +This is handled by the PropertyType class, where you can define the type as an array or table format, enabling the +binding of multidimensional data. + +For example, a table element can bind a two-dimensional array or a list of objects to its properties. Each time the data +in the array changes, the table element automatically refreshes to display the updated content in the user interface. +This approach leverages TypeScript and JavaScript code in the backend, working with React to dynamically generate HTML +for tabular displays. By using Taipy GUI’s variable binding capabilities, developers can seamlessly update and manage +tabular data within the UI. + +Even if a custom element does not need to update its tabular data dynamically, it can still be implemented as a dynamic +element to take advantage of the expressivity and flexibility offered by this approach. + +# Using tabular data + +In this section, we will expand the dynamic element library, initially created in the [Scalar properties](../scalar_props.md) +section, by adding a new dynamic custom element. + +This dynamic element will accept a property containing tabular data and display it within a table. When a Python +variable is bound to this property, updates to the variable will immediately reflect in the table content shown on +the page, ensuring real-time synchronization. + +## Declaring a dynamic element {data-source="gui:doc/extension/example_library/example_library.py#L36"} +Starting from the code mentioned above, here is how you would declare this new element: +```py title="example_library.py" +from taipy.gui.extension import Element, ElementLibrary, ElementProperty, PropertyType + +class ExampleLibrary(ElementLibrary): + def __init__(self) -> None: + # Initialize the set of visual elements for this extension library + self.elements = { + "game_table": Element( + "data", + { + "data": ElementProperty(PropertyType.data), + }, + # The name of the React component (GameTable) that implements this custom + # element, exported as GameTable in front-end/src/index.ts + # react_component="GameTable", + ), + } +``` +The declaration of this dynamic element is very similar to what we created in the [Scalar properties](scalar_props.md). +The detailed explanation of the code is as follows: + +- The *game_table* element includes a single property: *data*. +- The *data* property has the type *PropertyType.data*, meaning it holds a data value and is dynamic. +- The *get_name()* method in the *ExampleLibrary* class returns the name of the library as a string. This name is used + to identify the library within the Taipy GUI framework. +- The *get_elements()* method in the *ExampleLibrary* class returns a dictionary of all elements that are part of this + library. Each element is defined with its properties and associated React component. + +## Creating the React component {data-source="gui:doc/extension/example_library/front-end/src/GameTable.tsx"} + +The React component for the *game_table* element is defined as follows: + +```jsx title="GameTable.tsx" linenums="1" +import React, { useEffect, useMemo, useState } from "react"; +import { + createRequestDataUpdateAction, + useDispatch, + useDispatchRequestUpdateOnFirstRender, + useModule, + TaipyDynamicProps, + TableValueType, + RowType, + RowValue, +} from "taipy-gui"; + +interface GameTableProps extends TaipyDynamicProps { + data: TableValueType; +} + +const pageKey = "no-page-key"; + +const GameTable = (props: GameTableProps) => { + const { data, updateVarName = "", updateVars = "", id } = props; + const [value, setValue] = useState>>({}); + const dispatch = useDispatch(); + const module = useModule(); + const refresh = data?.__taipy_refresh !== undefined; + useDispatchRequestUpdateOnFirstRender(dispatch, id, module, updateVars); + + const colsOrder = useMemo(() => { + return Object.keys(value); + }, [value]); + + const rows = useMemo(() => { + const rows: RowType[] = []; + if (value) { + Object.entries(value).forEach(([col, colValues]) => { + colValues.forEach((val, idx) => { + rows[idx] = rows[idx] || {}; + rows[idx][col] = val; + }); + }); + } + return rows; + }, [value]); + + useEffect(() => { + if (refresh || !data || data[pageKey] === undefined) { + dispatch( + createRequestDataUpdateAction( + updateVarName, + id, + module, + colsOrder, + pageKey, + {}, + true, + "ExampleLibrary", + ), + ); + } else { + setValue(data[pageKey]); + } + }, [refresh, data, colsOrder, updateVarName, id, dispatch, module]); + + return ( +
+ + + {rows.map((row, index) => ( + + {colsOrder.map((col, cidx) => ( + + ))} + + ))} + +
{row[col]}
+
+ ); +}; + +export default GameTable; +``` + +The detailed explanation of the code is as follows: + +- We use the [*useDispatch()*](../../../../refmans/reference_guiext/functions/useDispatch.md) hook to + dispatch actions to the store and initiate backend communications. +- Additionally, the [*useModule()*](../../../../refmans/reference_guiext/functions/useModule.md) hook + retrieves the page module, enabling correct execution of backend functions. +- To request an update for every dynamic property of an element on initial render, we use the + [*useDispatchRequestUpdateOnFirstRender()*](../../../../refmans/reference_guiext/functions/useDispatchRequestUpdateOnFirstRender.md) + hook provided by the Taipy GUI Extension API. This hook takes five parameters: + - *dispatch*: The React dispatcher associated with the context. + - *id*: The identifier of the element. + - *context*: The execution context. + - *updateVars*: The content of the *updateVars* property. +- We also dispatch the + [*createRequestDataUpdateAction()*](../../../../refmans/reference_guiext/functions/createRequestDataUpdateAction.md) + hook to create a request data update action, which updates the context by invoking the + [*get_data()*](.../refmans/reference/pkg_taipy/pkg_gui/pkg_extension/ElementLibrary/#taipy.gui.extension.ElementLibrary.get_data) + method of the backend library. This invocation triggers an update of front-end elements holding the data. + +The [*createRequestDataUpdateAction()*](../../../../refmans/reference_guiext/functions/createRequestUpdateAction.md) +hook accepts eight parameters: + +- *name*: The name of the variable containing the requested data, as received in the property. +- *id*: The identifier of the visual element. +- *context*: The execution context. +- *columns*: The list of column names required by the element emitting this action. +- *pageKey*: The unique identifier for the data received from this action. +- *payload*: The payload, specific to the component type (e.g., table, chart). +- *allData*: A flag indicating if all the data is requested. +- *library*: The name of the extension library. + +## Exporting the React component {data-source="gui:doc/extension/example_library/front-end/src/index.ts"} + +When the component is entirely defined, it must be exported by the JavaScript library. +This is done by adding the export directive in the file */front-end/src/index.ts*. + +```js title="index.ts" +import GameTable from "./GameTable"; + +export { GameTable }; +``` + +## Using the element {data-source="gui:doc/extension/table_chess_game.py"} + +In the example below, we use the *game_table* element to display a chess game board. +The data is represented as a two-dimensional list of strings, where each string represents a chess piece. +The board is displayed in a table format using the *game_table* element. +We can see how the data property of the control is bound to the Python variable *data*, using the default property syntax. + +```py +data = [ + ["♖", "♘", "♗", "♕", "♔", "♗", "♘", "♖"], + ["♙", "♙", "♙", "♙", "♙", "♙", "♙", "♙"], + ["", "", "", "", "", "", "", ""], + ["", "", "", "", "", "", "", ""], + ["", "", "", "", "", "", "", ""], + ["", "", "", "", "", "", "", ""], + ["♟", "♟", "♟", "♟", "♟", "♟", "♟", "♟"], + ["♜", "♞", "♝", "♛", "♚", "♝", "♞", "♜"] +] + +page = """ +## Chess Game +<|{data}|example.game_table|> +""" +``` + +When you run this application, the page displays the element like this: + +
+ + +
Chess game
+
diff --git a/docs/userman/gui/extension/extension_data.md b/docs/userman/gui/extension/extension_data.md index d4cba5df1..5685e144a 100644 --- a/docs/userman/gui/extension/extension_data.md +++ b/docs/userman/gui/extension/extension_data.md @@ -1,10 +1,211 @@ # Using tabular data - -!!! warning "Work in Progress" - This section still requires significant work, which is in progress. - At this time, Taipy GUI provides a custom element library example - with lengthy explanations on how to build it.
- Please look into the `doc/extension` directory where Taipy GUI is - installed for more information.
- You can also look at this example directly on - [GitHub](https://github.com/Avaiga/taipy/tree/[BRANCH]/doc/gui/extension). + +In this section, we will expand the custom element library, initially created in the Static Elements section, by +adding a dynamic custom element. + +This dynamic element will accept a property containing tabular data and display it within a table. When a Python +variable is bound to this property, updates to the variable will immediately reflect in the table content shown on +the front end, ensuring real-time synchronization. + +## Declaring a dynamic element {data-source="gui:doc/extension/example_library/example_library.py#L36"} + +```py +from taipy.gui.extension import Element, ElementLibrary, ElementProperty, PropertyType + +class ExampleLibrary(ElementLibrary): + def __init__(self) -> None: + # Initialize the set of visual elements for this extension library + self.elements = { + "game_table": Element( + "data", + { + "data": ElementProperty(PropertyType.data), + }, + # The name of the React component (GameTable) that implements this custom + # element, exported as GameTable in front-end/src/index.ts + # react_component="GameTable", + ), + } + def get_name(self) -> str: + return "example" + + def get_elements(self) -> dict: + return self.elements + + def get_scripts(self) -> list[str]: + # Only one JavaScript bundle for this library. + return ["front-end/dist/exampleLibrary.js"] +``` + +The detailed explanation of the code is as follows: + +- The `game_table` element includes a single property: `data`. +- The `data` property has the type `PropertyType.data`, meaning it holds a data value and is dynamic. +- The `get_name` method in the `ExampleLibrary` class returns the name of the library as a string. This name is used + to identify the library within the Taipy GUI framework. +- The `get_elements` method in the `ExampleLibrary` class returns a dictionary of elements that are part of this + library. Each element is defined with its properties and associated React component. + +## Creating the React component {data-source="gui:doc/extension/example_library/front-end/src/GameTable.tsx"} + +The React component for the `game_table` element is defined as follows: + +```jsx +import React, { useEffect, useMemo, useState } from "react"; +import { + createRequestDataUpdateAction, + useDispatch, + useDispatchRequestUpdateOnFirstRender, + useModule, + TaipyDynamicProps, + TableValueType, + RowType, + RowValue, +} from "taipy-gui"; + +interface GameTableProps extends TaipyDynamicProps { + data: TableValueType; +} + +const pageKey = "no-page-key"; + +const GameTable = (props: GameTableProps) => { + const { data, updateVarName = "", updateVars = "", id } = props; + const [value, setValue] = useState>>({}); + const dispatch = useDispatch(); + const module = useModule(); + const refresh = data?.__taipy_refresh !== undefined; + useDispatchRequestUpdateOnFirstRender(dispatch, id, module, updateVars); + + const colsOrder = useMemo(() => { + return Object.keys(value); + }, [value]); + + const rows = useMemo(() => { + const rows: RowType[] = []; + if (value) { + Object.entries(value).forEach(([col, colValues]) => { + colValues.forEach((val, idx) => { + rows[idx] = rows[idx] || {}; + rows[idx][col] = val; + }); + }); + } + return rows; + }, [value]); + + useEffect(() => { + if (refresh || !data || data[pageKey] === undefined) { + dispatch( + createRequestDataUpdateAction( + updateVarName, + id, + module, + colsOrder, + pageKey, + {}, + true, + "ExampleLibrary", + ), + ); + } else { + setValue(data[pageKey]); + } + }, [refresh, data, colsOrder, updateVarName, id, dispatch, module]); + + return ( +
+ + + {rows.map((row, index) => ( + + {colsOrder.map((col, cidx) => ( + + ))} + + ))} + +
{row[col]}
+
+ ); +}; + +export default GameTable; +``` + +The detailed explanation of the code is as follows: + +- We use the [useDispatch](https://docs.taipy.io/en/latest/refmans/reference_guiext/functions/useDispatch/) hook to + dispatch actions to the store and initiate backend communications. +- Additionally, the [useModule](https://docs.taipy.io/en/latest/refmans/reference_guiext/functions/useModule/) hook + retrieves the page module, enabling correct execution of backend functions. +- To request an update for every dynamic property of an element on initial render, we use the + [useDispatchRequestUpdateOnFirstRender](https://docs.taipy.io/en/latest/refmans/reference_guiext/functions/useDispatchRequestUpdateOnFirstRender/#function-usedispatchrequestupdateonfirstrender) + hook provided by the Taipy GUI Extension API. This hook takes five parameters: + - `dispatch`: The React dispatcher associated with the context. + - `id`: The identifier of the element. + - `context`: The execution context. + - `updateVars`: The content of the `updateVars` property. +- We also dispatch the + [createRequestDataUpdateAction](https://docs.taipy.io/en/latest/refmans/reference_guiext/functions/createRequestUpdateAction/) + hook to create a request data update action, which updates the context by invoking the + [get_data](https://docs.taipy.io/en/latest/refmans/reference/pkg_taipy/pkg_gui/pkg_extension/ElementLibrary/#taipy.gui.extension.ElementLibrary.get_data) + method of the backend library. This invocation triggers an update of front-end elements holding the data. + +The [createRequestDataUpdateAction](https://docs.taipy.io/en/latest/refmans/reference_guiext/functions/createRequestUpdateAction/) +hook accepts eight parameters: + +- `name`: The name of the variable containing the requested data, as received in the property. +- `id`: The identifier of the visual element. +- `context`: The execution context. +- `columns`: The list of columns required by the element emitting this action. +- `pageKey`: The unique identifier for the data received from this action. +- `payload`: The payload, specific to the component type (e.g., table, chart). +- `allData`: A flag indicating if all data is requested. +- `library`: The name of the extension library. + +## Exporting the React component {data-source="gui:doc/extension/example_library/front-end/src/index.ts"} + +When the component is entirely defined, it must be exported by the JavaScript library. +This is done by adding the export directive in the file `//front-end/src/index.ts`. + +```js +import GameTable from "./GameTable"; + +export { GameTable }; +``` + +## Using the element in the application {data-source="gui:doc/extension/table_chess_game.py"} + +In the example below, we use the `game_table` element to display a chess game board. The board is represented as a +two-dimensional list of strings, where each string represents a chess piece. The board is displayed in a table format +using the `game_table` element. + +```py +from example_library import ExampleLibrary + +from taipy.gui import Gui + +data = [ + ["♖", "♘", "♗", "♕", "♔", "♗", "♘", "♖"], + ["♙", "♙", "♙", "♙", "♙", "♙", "♙", "♙"], + ["", "", "", "", "", "", "", ""], + ["", "", "", "", "", "", "", ""], + ["", "", "", "", "", "", "", ""], + ["", "", "", "", "", "", "", ""], + ["♟", "♟", "♟", "♟", "♟", "♟", "♟", "♟"], + ["♜", "♞", "♝", "♛", "♚", "♝", "♞", "♜"] +] + +page = """ +## Chess Game +<|{data}|example.game_table|> +""" + +if __name__ == "__main__": + Gui(page, libraries=[ExampleLibrary()]).run(title="Chess Game") +``` + +When you run this application, the page displays the element like this: + + diff --git a/mkdocs.yml_template b/mkdocs.yml_template index 440a98709..5776180ae 100644 --- a/mkdocs.yml_template +++ b/mkdocs.yml_template @@ -100,6 +100,7 @@ nav: - "Dynamic Elements": - userman/gui/extension/dynamic_element/index.md - "Scalar properties": userman/gui/extension/dynamic_element/scalar_props.md + - "Tabular data properties": userman/gui/extension/dynamic_element/tabular_data_props.md - "Scenario features": - "Data integration":