Browse Source

Page Builder API for extension library elements... and a bit more (#2438)

- Added taipy.gui.extention entry point for API generation.
- Code reorganization.
- Hide inner properties.
- Store extension libraries element constructors in their library module.
- Added PropertyType.any and dynamic_any.
- Added optional documentation on extension library elements and their properties.
- Added test on tgb api generation for extension libraries.
Fabien Lelaquais 3 tháng trước cách đây
mục cha
commit
a932f8cc14

+ 4 - 4
frontend/taipy-gui/src/components/Taipy/tableUtils.tsx

@@ -44,10 +44,6 @@ import { FormatConfig } from "../../context/taipyReducers";
 import { dateToString, getDateTime, getDateTimeString, getNumberString, getTimeZonedDate } from "../../utils/index";
 import { TaipyActiveProps, TaipyMultiSelectProps, getSuffixedClassNames } from "./utils";
 
-/**
- * A column description as received by the backend.
- */
-
 /**
  * Generates a  CSS class name for a table header.
  * @param columnName - The name of the column.
@@ -63,6 +59,10 @@ export const generateHeaderClassName = (columnName: string | undefined): string
     return "-" + columnName.replace(/\W+/g, "-").replace(/-+/g, "-").toLowerCase();
 };
 
+/**
+ * A column description as received by the backend.
+ */
+
 export interface ColumnDesc {
     /** The unique column identifier. */
     dfid: string;

+ 18 - 18
frontend/taipy-gui/src/components/Taipy/utils.ts

@@ -14,9 +14,19 @@
 import { MouseEvent, ReactNode } from "react";
 import { SxProps } from "@mui/material";
 
-export interface TaipyActiveProps extends TaipyDynamicProps, TaipyHoverProps {
-    defaultActive?: boolean;
-    active?: boolean;
+export interface TaipyBaseProps {
+    id?: string;
+    libClassName?: string;
+    className?: string;
+    dynamicClassName?: string;
+    privateClassName?: string;
+    children?: ReactNode;
+}
+
+interface TaipyDynamicProps extends TaipyBaseProps {
+    updateVarName?: string;
+    propagate?: boolean;
+    updateVars?: string;
 }
 
 export interface TaipyHoverProps {
@@ -24,19 +34,13 @@ export interface TaipyHoverProps {
     defaultHoverText?: string;
 }
 
-interface TaipyDynamicProps extends TaipyBaseProps {
-    updateVarName?: string;
-    propagate?: boolean;
-    updateVars?: string;
+export interface TaipyActiveProps extends TaipyDynamicProps, TaipyHoverProps {
+    defaultActive?: boolean;
+    active?: boolean;
 }
 
-export interface TaipyBaseProps {
-    id?: string;
-    libClassName?: string;
-    className?: string;
-    dynamicClassName?: string;
-    privateClassName?: string;
-    children?: ReactNode;
+export interface TaipyLabelProps {
+    label?: string;
 }
 
 export interface TaipyMultiSelectProps {
@@ -68,10 +72,6 @@ export interface TaipyInputProps extends TaipyActiveProps, TaipyChangeProps, Tai
     width?: string | number;
 }
 
-export interface TaipyLabelProps {
-    label?: string;
-}
-
 export interface DateProps {
     maxDate?: unknown;
     maxDateTime?: unknown;

+ 283 - 256
taipy/gui/_renderers/builder.py

@@ -85,7 +85,7 @@ class _Builder:
         gui: "Gui",
         control_type: str,
         element_name: str,
-        attributes: t.Optional[t.Dict[str, t.Any]],
+        prop_values: t.Optional[t.Dict[str, t.Any]],
         hash_names: t.Optional[t.Dict[str, str]] = None,
         default_value: t.Optional[t.Any] = "<Empty>",
         lib_name: str = "taipy",
@@ -101,20 +101,20 @@ class _Builder:
         self.__control_type = control_type
         self.__element_name = element_name
         self.__lib_name = lib_name
-        self.__attributes = attributes or {}
+        self.__prop_values = prop_values or {}
         self.__hashes = hash_names.copy()
         self.__update_vars: t.List[str] = []
         self.__gui: Gui = gui
 
         self.__default_property_name = _Factory.get_default_property_name(control_type) or ""
-        default_property_value = self.__attributes.get(self.__default_property_name, None)
+        default_property_value = self.__prop_values.get(self.__default_property_name, None)
         if default_property_value is None and default_value is not None:
-            self.__attributes[self.__default_property_name] = default_value
+            self.__prop_values[self.__default_property_name] = default_value
 
         # Bind properties dictionary to attributes if condition is matched (will
         # leave the binding for function at the builder )
-        if "properties" in self.__attributes:
-            (prop_dict, prop_hash) = _Builder.__parse_attribute_value(gui, self.__attributes["properties"])
+        if "properties" in self.__prop_values:
+            (prop_dict, prop_hash) = _Builder.__parse_attribute_value(gui, self.__prop_values["properties"])
             if prop_hash is None:
                 prop_hash = prop_dict
                 prop_hash = self.__gui._bind_var(prop_hash)
@@ -125,14 +125,14 @@ class _Builder:
                 var_name, _ = gui._get_real_var_name(prop_hash)
                 for k, v in prop_dict.items():
                     (val, key_hash) = _Builder.__parse_attribute_value(gui, v)
-                    self.__attributes[k] = (
+                    self.__prop_values[k] = (
                         f"{{None if ({var_name}) is None else ({var_name}).get('{k}')}}" if key_hash is None else v
                     )
             else:
                 _warn(f"{self.__control_type}.properties ({prop_hash}) must be a dict.")
 
         # Bind potential function and expressions in self.attributes
-        self.__hashes.update(_Builder._get_variable_hash_names(gui, self.__attributes, hash_names))
+        self.__hashes.update(_Builder._get_variable_hash_names(gui, self.__prop_values, hash_names))
 
         # set classname
         self.__set_class_names()
@@ -206,78 +206,63 @@ class _Builder:
 
             name (str): The property name.
         """
-        return _get_name_indexed_property(self.__attributes, name)
+        return _get_name_indexed_property(self.__prop_values, name)
 
-    def __get_boolean_attribute(self, name: str, default_value=False):
-        bool_attr = self.__attributes.get(name, default_value)
-        return _is_true(bool_attr) if isinstance(bool_attr, str) else bool(bool_attr)
-
-    def set_boolean_attribute(self, name: str, value: bool):
-        """
-        TODO-undocumented
-        Defines a React Boolean attribute (attr={true|false}).
+    def __set_json_attribute(self, name, value):
+        return self.set_attribute(name, json.dumps(value, cls=_TaipyJsonEncoder))
 
-        Arguments:
-            name (str): The property name.
-            value (bool): the boolean value.
-        """
-        return self.__set_react_attribute(_to_camel_case(name), value)
+    def __set_any_attribute(self, name: str, default_value: t.Optional[str] = None):
+        value = self.__prop_values.get(name, default_value)
+        return self.__set_json_attribute(_to_camel_case(name), value)
 
-    def set_dict_attribute(self, name: str, default_value: t.Optional[t.Dict[str, t.Any]] = None):
-        """
-        TODO-undocumented
-        Defines a React attribute as a stringified json dict.
-        The original property can be a dict or a string formed as <key 1>:<value 1>;<key 2>:<value 2>.
+    def __set_dynamic_any_attribute(self, name: str, default_value: t.Optional[str] = None):
+        value = self.__prop_values.get(name, default_value)
+        self.__set_json_attribute(_to_camel_case(f"default_{name}"), value)
 
-        Arguments:
-            name (str): The property name.
-            default value (dict): used if no value is specified.
-        """
-        dict_attr = self.__attributes.get(name)
-        if dict_attr is None:
-            dict_attr = default_value
-        if dict_attr is not None:
-            if isinstance(dict_attr, str):
-                vals = [x.strip().split(":") for x in dict_attr.split(";")]
-                dict_attr = {val[0].strip(): val[1].strip() for val in vals if len(val) > 1}
-            if isinstance(dict_attr, (dict, _MapDict)):
-                self.__set_json_attribute(_to_camel_case(name), dict_attr)
+        if hash := self.__hashes.get(name):
+            if isinstance(value, (dict, _MapDict)):
+                hash = self.__get_typed_hash_name(hash, PropertyType.dynamic_dict)
+                react_name = _to_camel_case(name)
+                self.__update_vars.append(f"{react_name}={hash}")
+                self.__set_react_attribute(react_name, hash)
             else:
-                _warn(f"{self.__element_name}: {name} should be a dict: '{str(dict_attr)}'.")
+                self.__update_vars.append(f"{name}={hash}")
+                self.__set_react_attribute(name, hash)
         return self
 
-    def set_dynamic_dict_attribute(self, name: str, default_value: t.Optional[t.Dict[str, t.Any]] = None):
+    def __get_boolean_attribute(self, name: str, default_value=False):
+        bool_attr = self.__prop_values.get(name, default_value)
+        return _is_true(bool_attr) if isinstance(bool_attr, str) else bool(bool_attr)
+
+    def __set_boolean_attribute(self, name: str, value: bool):
         """
         TODO-undocumented
-        Defines a React attribute as a stringified json dict.
-        The original property can be a dict or a string formed as <key 1>:<value 1>;<key 2>:<value 2>.
+        Defines a React Boolean attribute (attr={true|false}).
 
         Arguments:
             name (str): The property name.
-            default value (dict): used if no value is specified.
+            value (bool): the boolean value.
         """
-        dict_attr = self.__attributes.get(name)
-        if dict_attr is None:
-            dict_attr = default_value
-        if dict_attr is not None:
-            if isinstance(dict_attr, str):
-                vals = [x.strip().split(":") for x in dict_attr.split(";")]
-                dict_attr = {val[0].strip(): val[1].strip() for val in vals if len(val) > 1}
-            if isinstance(dict_attr, (dict, _MapDict)):
-                self.__set_json_attribute(_to_camel_case("default_" + name), dict_attr)
-            else:
-                _warn(f"{self.__element_name}: {name} should be a dict: '{str(dict_attr)}'.")
-        if dict_hash := self.__hashes.get(name):
-            dict_hash = self.__get_typed_hash_name(dict_hash, PropertyType.dynamic_dict)
-            prop_name = _to_camel_case(name)
-            self.__update_vars.append(f"{prop_name}={dict_hash}")
-            self.__set_react_attribute(prop_name, dict_hash)
-        return self
+        return self.__set_react_attribute(_to_camel_case(name), value)
 
-    def __set_json_attribute(self, name, value):
-        return self.set_attribute(name, json.dumps(value, cls=_TaipyJsonEncoder))
+    def __set_dynamic_bool_attribute(self, name: str, def_val: t.Any, with_update: bool, update_main=True):
+        value = self.__get_boolean_attribute(name, def_val)
+        hash = self.__hashes.get(name)
+        default_name = f"default_{name}" if hash is not None else name
+        if value != def_val:
+            self.__set_boolean_attribute(default_name, value)
+        if hash is not None:
+            hash = self.__get_typed_hash_name(hash, PropertyType.dynamic_boolean)
+            self.__set_react_attribute(_to_camel_case(name), _get_client_var_name(hash))
+            if with_update:
+                if update_main:
+                    self.__set_update_var_name(hash)
+                else:
+                    self.__update_vars.append(f"{_to_camel_case(name)}={hash}")
 
-    def set_number_attribute(self, name: str, default_value: t.Optional[str] = None, optional: t.Optional[bool] = True):
+    def __set_number_attribute(
+        self, name: str, default_value: t.Optional[str] = None, optional: t.Optional[bool] = True
+    ):
         """
         TODO-undocumented
         Defines a React number attribute (attr={<number>}).
@@ -288,7 +273,7 @@ class _Builder:
             default_value (optional(str)): the default value as a string.
             optional (bool): Default to True, the property is required if False.
         """
-        value = self.__attributes.get(name, default_value)
+        value = self.__prop_values.get(name, default_value)
         if value is None:
             if not optional:
                 _warn(f"Property {name} is required for control {self.__control_type}.")
@@ -306,24 +291,35 @@ class _Builder:
             )
         return self.__set_react_attribute(_to_camel_case(name), val)
 
+    def __set_dynamic_number_attribute(self, var_name: str, default_value: t.Any):
+        value = self.__prop_values.get(var_name)
+        if value is None:
+            value = default_value
+        if isinstance(value, str):
+            try:
+                value = float(value)
+            except Exception as e:
+                _warn(f"{self.__element_name}: {var_name} cannot be transformed into a number", e)
+                value = 0
+        if isinstance(value, numbers.Number):
+            self.__set_react_attribute(_to_camel_case(f"default_{var_name}"), value)
+        elif value is not None:
+            _warn(f"{self.__element_name}: {var_name} value is not valid ({value}).")
+        if hash := self.__hashes.get(var_name):
+            hash = self.__get_typed_hash_name(hash, PropertyType.number)
+            self.__update_vars.append(f"{var_name}={hash}")
+            self.__set_react_attribute(var_name, hash)
+        return self
+
     def __set_string_attribute(
         self, name: str, default_value: t.Optional[str] = None, optional: t.Optional[bool] = True
     ):
-        str_attr = self.__attributes.get(name, default_value)
-        if str_attr is None:
+        value = self.__prop_values.get(name, default_value)
+        if value is None:
             if not optional:
                 _warn(f"Property {name} is required for control {self.__control_type}.")
             return self
-        return self.set_attribute(_to_camel_case(name), str(str_attr))
-
-    def __set_dynamic_date_attribute(self, var_name: str, default_value: t.Optional[str] = None):
-        date_attr = self.__attributes.get(var_name, default_value)
-        if date_attr is None:
-            date_attr = default_value
-        if isinstance(date_attr, (datetime, date, time)):
-            value = _date_to_string(date_attr)
-            self.set_attribute(_to_camel_case(var_name), value)
-        return self
+        return self.set_attribute(_to_camel_case(name), str(value))
 
     def __set_dynamic_string_attribute(
         self,
@@ -332,47 +328,142 @@ class _Builder:
         with_update: t.Optional[bool] = False,
         dynamic_property_name: t.Optional[str] = None,
     ):
-        str_val = self.__attributes.get(name, default_value)
-        if str_val is not None:
+        value = self.__prop_values.get(name, default_value)
+        if value is not None:
             self.set_attribute(
-                _to_camel_case(f"default_{name}" if dynamic_property_name is None else name), str(str_val)
+                _to_camel_case(f"default_{name}" if dynamic_property_name is None else name), str(value)
             )
-        if hash_name := self.__hashes.get(name):
+        if hash := self.__hashes.get(name):
             prop_name = _to_camel_case(name if dynamic_property_name is None else dynamic_property_name)
             if with_update:
-                self.__update_vars.append(f"{prop_name}={hash_name}")
-            self.__set_react_attribute(prop_name, hash_name)
+                self.__update_vars.append(f"{prop_name}={hash}")
+            self.__set_react_attribute(prop_name, hash)
+        return self
+
+    def __set_string_or_number_attribute(self, name: str, default_value: t.Optional[t.Any] = None):
+        value = self.__prop_values.get(name, default_value)
+        if value is None:
+            return self
+        if isinstance(value, numbers.Number):
+            return self.__set_react_attribute(_to_camel_case(name), value)
+        else:
+            return self.set_attribute(_to_camel_case(name), value)
+
+    def __set_dynamic_string_list(self, name: str, default_value: t.Any):
+        value = self.__prop_values.get(name)
+        if value is None:
+            value = default_value
+        if isinstance(value, str):
+            value = [s.strip() for s in value.split(";") if s.strip()]
+        if isinstance(value, list):
+            self.__set_json_attribute(_to_camel_case(f"default_{name}"), value)
+        if hash := self.__hashes.get(name):
+            self.__update_vars.append(f"{name}={hash}")
+            self.__set_react_attribute(name, hash)
+        return self
+
+    def __set_list_attribute(
+        self,
+        name: str,
+        hash: t.Optional[str],
+        value: t.Any,
+        elt_type: t.Type,
+        dynamic=True,
+        default_val: t.Optional[t.Any] = None,
+    ) -> t.List[str]:
+        value = default_val if value is None else value
+        if not hash and isinstance(value, str):
+            value = [elt_type(t.strip()) for t in value.split(";")]
+        if isinstance(value, list):
+            if hash and dynamic:
+                self.__set_react_attribute(name, hash)
+                return [f"{name}={hash}"]
+            else:
+                self.__set_json_attribute(name, value)
+        elif value is not None:
+            _warn(f"{self.__element_name}: {name} should be a list of {elt_type}.")
+        return []
+
+    def __set_dict_attribute(self, name: str, default_value: t.Optional[t.Dict[str, t.Any]] = None):
+        """
+        TODO-undocumented
+        Defines a React attribute as a stringified json dict.
+        The original property can be a dict or a string formed as <key 1>:<value 1>;<key 2>:<value 2>.
+
+        Arguments:
+            name (str): The property name.
+            default value (dict): used if no value is specified.
+        """
+        value = self.__prop_values.get(name)
+        if value is None:
+            value = default_value
+        if value is not None:
+            if isinstance(value, str):
+                vals = [x.strip().split(":") for x in value.split(";")]
+                value = {val[0].strip(): val[1].strip() for val in vals if len(val) > 1}
+            if isinstance(value, (dict, _MapDict)):
+                self.__set_json_attribute(_to_camel_case(name), value)
+            else:
+                _warn(f"{self.__element_name}: {name} should be a dict: '{str(value)}'.")
+        return self
+
+    def __set_dynamic_dict_attribute(self, name: str, default_value: t.Optional[t.Dict[str, t.Any]] = None):
+        """
+        TODO-undocumented
+        Defines a React attribute as a stringified json dict.
+        The original property can be a dict or a string formed as <key 1>:<value 1>;<key 2>:<value 2>.
+
+        Arguments:
+            name (str): The property name.
+            default value (dict): used if no value is specified.
+        """
+        value = self.__prop_values.get(name)
+        if value is None:
+            value = default_value
+        if value is not None:
+            if isinstance(value, str):
+                vals = [x.strip().split(":") for x in value.split(";")]
+                value = {val[0].strip(): val[1].strip() for val in vals if len(val) > 1}
+            if isinstance(value, (dict, _MapDict)):
+                self.__set_json_attribute(_to_camel_case("default_" + name), value)
+            else:
+                _warn(f"{self.__element_name}: {name} should be a dict: '{str(value)}'.")
+        if hash := self.__hashes.get(name):
+            hash = self.__get_typed_hash_name(hash, PropertyType.dynamic_dict)
+            prop_name = _to_camel_case(name)
+            self.__update_vars.append(f"{prop_name}={hash}")
+            self.__set_react_attribute(prop_name, hash)
+        return self
+
+    def __set_dynamic_date_attribute(self, var_name: str, default_value: t.Optional[str] = None):
+        value = self.__prop_values.get(var_name, default_value)
+        if value is None:
+            value = default_value
+        if isinstance(value, (datetime, date, time)):
+            value = _date_to_string(value)
+            self.set_attribute(_to_camel_case(var_name), value)
         return self
 
     def __set_function_attribute(
         self, name: str, default_value: t.Optional[str] = None, optional: t.Optional[bool] = True
     ):
-        str_attr = self.__attributes.get(name, default_value)
-        if str_attr is None:
+        value = self.__prop_values.get(name, default_value)
+        if value is None:
             if not optional:
                 _warn(f"Property {name} is required for control {self.__control_type}.")
             return self
-        elif _is_function(str_attr):
-            str_attr = self.__hashes.get(name)
-            if str_attr is None:
+        elif _is_function(value):
+            value = self.__hashes.get(name)
+            if value is None:
                 return self
-        elif _is_boolean(str_attr) and not _is_true(t.cast(str, str_attr)):
+        elif _is_boolean(value) and not _is_true(t.cast(str, value)):
             return self.__set_react_attribute(_to_camel_case(name), False)
-        elif str_attr:
-            str_attr = str(str_attr)
-            func = self.__gui._get_user_function(str_attr)
-            if func == str_attr:
-                _warn(f"{self.__control_type}.{name}: {str_attr} is not a function.")
-        return self.set_attribute(_to_camel_case(name), str_attr) if str_attr else self
-
-    def __set_string_or_number_attribute(self, name: str, default_value: t.Optional[t.Any] = None):
-        attr = self.__attributes.get(name, default_value)
-        if attr is None:
-            return self
-        if isinstance(attr, numbers.Number):
-            return self.__set_react_attribute(_to_camel_case(name), attr)
-        else:
-            return self.set_attribute(_to_camel_case(name), attr)
+        elif value:
+            value = str(value)
+            func = self.__gui._get_user_function(value)
+            if func == value:
+                _warn(f"{self.__control_type}.{name}: {value} is not a function.")
+        return self.set_attribute(_to_camel_case(name), value) if value else self
 
     def __set_react_attribute(self, name: str, value: t.Any):
         return self.set_attribute(name, "{!" + (str(value).lower() if isinstance(value, bool) else str(value)) + "!}")
@@ -387,7 +478,7 @@ class _Builder:
         property_name = var_name if property_name is None else property_name
         lov_name = self.__hashes.get(var_name)
         real_var_name = self.__gui._get_real_var_name(lov_name)[0] if lov_name else None
-        lov = self.__attributes.get(var_name)
+        lov = self.__prop_values.get(var_name)
         adapter: t.Any = None
         var_type: t.Optional[str] = None
         if isinstance(lov, str):
@@ -405,13 +496,13 @@ class _Builder:
 
         default_lov: t.Optional[t.List[t.Any]] = [] if with_default or not lov_name else None
 
-        adapter = self.__attributes.get("adapter", adapter)
+        adapter = self.__prop_values.get("adapter", adapter)
         if adapter and isinstance(adapter, str):
             adapter = self.__gui._get_user_function(adapter)
         if adapter and not _is_function(adapter):
             _warn(f"{self.__element_name}: adapter property value is invalid.")
             adapter = None
-        var_type = self.__attributes.get("type", var_type)
+        var_type = self.__prop_values.get("type", var_type)
         if isclass(var_type):
             var_type = var_type.__name__
 
@@ -421,7 +512,7 @@ class _Builder:
                 if lov:
                     elt = lov[0]
                 else:
-                    value = self.__attributes.get("value")
+                    value = self.__prop_values.get("value")
                     if isinstance(value, list):
                         if len(value) > 0:
                             elt = value[0]
@@ -460,7 +551,7 @@ class _Builder:
                         default_lov.append(ret)
 
             ret_list = []
-            value = self.__attributes.get("value")
+            value = self.__prop_values.get("value")
             val_list = value if isinstance(value, list) else [value]
             for val in val_list:
                 ret = self.__gui._run_adapter(
@@ -475,8 +566,8 @@ class _Builder:
                 self.__set_default_value("value", ret_list)
             else:
                 ret_val = ret_list[0] if len(ret_list) else ""
-                if ret_val == "-1" and self.__attributes.get("unselected_value") is not None:
-                    ret_val = str(self.__attributes.get("unselected_value", ""))
+                if ret_val == "-1" and self.__prop_values.get("unselected_value") is not None:
+                    ret_val = str(self.__prop_values.get("unselected_value", ""))
                 self.__set_default_value("value", ret_val)
 
         # LoV default value
@@ -500,12 +591,12 @@ class _Builder:
         return self
 
     def __filter_attribute_names(self, names: t.Iterable[str]):
-        return [k for k in self.__attributes if k in names or any(k.startswith(n + "[") for n in names)]
+        return [k for k in self.__prop_values if k in names or any(k.startswith(n + "[") for n in names)]
 
     def __get_held_name(self, key: str):
         name = self.__hashes.get(key)
         if name:
-            v = self.__attributes.get(key)
+            v = self.__prop_values.get(key)
             if isinstance(v, _TaipyBase):
                 return name[: len(v.get_hash()) + 1]
         return name
@@ -514,12 +605,12 @@ class _Builder:
         hash_names = [k for k in self.__hashes if k in keys]
         attr_names = [k for k in keys if k not in hash_names]
         return (
-            {k: v for k, v in self.__attributes.items() if k in attr_names},
+            {k: v for k, v in self.__prop_values.items() if k in attr_names},
             {k: self.__get_held_name(k) for k in self.__hashes if k in hash_names},
         )
 
     def __build_rebuild_fn(self, fn_name: str, attribute_names: t.Iterable[str]):
-        rebuild = self.__attributes.get("rebuild", False)
+        rebuild = self.__prop_values.get("rebuild", False)
         rebuild_hash = self.__hashes.get("rebuild")
         if rebuild_hash or _is_true(rebuild):
             attributes, hashes = self.__filter_attributes_hashes(self.__filter_attribute_names(attribute_names))
@@ -536,8 +627,8 @@ class _Builder:
         return None
 
     def _get_dataframe_attributes(self) -> "_Builder":
-        date_format = _add_to_dict_and_get(self.__attributes, "date_format", "MM/dd/yyyy")
-        data = self.__attributes.get("data")
+        date_format = _add_to_dict_and_get(self.__prop_values, "date_format", "MM/dd/yyyy")
+        data = self.__prop_values.get("data")
         data_hash = self.__hashes.get("data", "")
         cmp_hash = ""
         if data_hash:
@@ -552,16 +643,16 @@ class _Builder:
                 cmp_hash = self.__gui._evaluate_expr(
                     "{"
                     + f"{self.__gui._get_call_method_name('_compare_data')}"
-                    + f'({self.__gui._get_real_var_name(data_hash)[0]},{",".join(cmp_datas)})'
+                    + f"({self.__gui._get_real_var_name(data_hash)[0]},{','.join(cmp_datas)})"
                     + "}"
                 )
                 self.__update_vars.append(f"comparedatas={','.join(cmp_datas_hash)}")
         cols_description = self.__gui._get_accessor().get_cols_description(data_hash, _TaipyData(data, data_hash))
         col_dict = _get_columns_dict(
-            self.__attributes.get("columns", {}),
+            self.__prop_values.get("columns", {}),
             cols_description,
             date_format,
-            self.__attributes.get("number_format"),
+            self.__prop_values.get("number_format"),
         )
 
         rebuild_fn_hash = self.__build_rebuild_fn(
@@ -570,7 +661,7 @@ class _Builder:
         if rebuild_fn_hash:
             self.__set_react_attribute("columns", rebuild_fn_hash)
         if col_dict is not None:
-            _enhance_columns(self.__attributes, self.__hashes, col_dict, self.__element_name)
+            _enhance_columns(self.__prop_values, self.__hashes, col_dict, self.__element_name)
             self.__set_json_attribute("defaultColumns", col_dict)
         if cmp_hash:
             hash_name = self.__get_typed_hash_name(cmp_hash, PropertyType.data)
@@ -579,12 +670,12 @@ class _Builder:
                 _get_client_var_name(hash_name),
             )
             self.__set_update_var_name(hash_name)
-            self.set_boolean_attribute("compare", True)
+            self.__set_boolean_attribute("compare", True)
             self.__set_string_attribute("on_compare")
 
-        if not isinstance(self.__attributes.get("style"), (type(None), dict, _MapDict)):
+        if not isinstance(self.__prop_values.get("style"), (type(None), dict, _MapDict)):
             _warn("Table: property 'style' has been renamed to 'row_class_name'.")
-        if row_class_name := self.__attributes.get("row_class_name"):
+        if row_class_name := self.__prop_values.get("row_class_name"):
             if _is_function(row_class_name):
                 value = self.__hashes.get("row_class_name")
             elif isinstance(row_class_name, str):
@@ -595,7 +686,7 @@ class _Builder:
                 _warn(f"{self.__element_name}: row_class_name={value} must not be a column name.")
             elif value:
                 self.set_attribute("rowClassName", value)
-        if tooltip := self.__attributes.get("tooltip"):
+        if tooltip := self.__prop_values.get("tooltip"):
             if _is_function(tooltip):
                 value = self.__hashes.get("tooltip")
             elif isinstance(tooltip, str):
@@ -609,8 +700,8 @@ class _Builder:
         return self
 
     def _get_chart_config(self, default_type: str, default_mode: str):
-        self.__attributes["_default_type"] = default_type
-        self.__attributes["_default_mode"] = default_mode
+        self.__prop_values["_default_type"] = default_type
+        self.__prop_values["_default_mode"] = default_mode
         rebuild_fn_hash = self.__build_rebuild_fn(
             self.__gui._get_call_method_name("_chart_conf"),
             _CHART_NAMES + ("_default_type", "_default_mode"),
@@ -619,7 +710,7 @@ class _Builder:
             self.__set_react_attribute("config", rebuild_fn_hash)
 
         # read column definitions
-        data = self.__attributes.get("data")
+        data = self.__prop_values.get("data")
         data_hash = self.__hashes.get("data", "")
         cols_description = [self.__gui._get_accessor().get_cols_description(data_hash, _TaipyData(data, data_hash))]
 
@@ -631,7 +722,7 @@ class _Builder:
                 typed_hash = self.__get_typed_hash_name(add_data_hash, _TaipyData)
                 data_updates.append(typed_hash)
                 self.__set_react_attribute(f"data{data_idx}", _get_client_var_name(typed_hash))
-                add_data = self.__attributes.get(name_idx)
+                add_data = self.__prop_values.get(name_idx)
                 data_idx += 1
                 name_idx = f"data[{data_idx}]"
                 cols_description.append(
@@ -639,7 +730,7 @@ class _Builder:
                 )
             self.set_attribute("dataVarNames", ";".join(data_updates))
 
-        config = _build_chart_config(self.__gui, self.__attributes, cols_description)
+        config = _build_chart_config(self.__gui, self.__prop_values, cols_description)
 
         self.__set_json_attribute("defaultConfig", config)
         self._set_chart_selected(max=len(config.get("traces", [])))
@@ -647,42 +738,20 @@ class _Builder:
         return self
 
     def _set_string_with_check(self, var_name: str, values: t.List[str], default_value: t.Optional[str] = None):
-        value = self.__attributes.get(var_name, default_value)
+        value = self.__prop_values.get(var_name, default_value)
         if value is not None:
             value = str(value).lower()
-            self.__attributes[var_name] = value
+            self.__prop_values[var_name] = value
             if value not in values:
                 _warn(f"{self.__element_name}: {var_name}={value} should be in {values}.")
             else:
                 self.__set_string_attribute(var_name, default_value)
         return self
 
-    def __set_list_attribute(
-        self,
-        name: str,
-        hash_name: t.Optional[str],
-        val: t.Any,
-        elt_type: t.Type,
-        dynamic=True,
-        default_val: t.Optional[t.Any] = None,
-    ) -> t.List[str]:
-        val = default_val if val is None else val
-        if not hash_name and isinstance(val, str):
-            val = [elt_type(t.strip()) for t in val.split(";")]
-        if isinstance(val, list):
-            if hash_name and dynamic:
-                self.__set_react_attribute(name, hash_name)
-                return [f"{name}={hash_name}"]
-            else:
-                self.__set_json_attribute(name, val)
-        elif val is not None:
-            _warn(f"{self.__element_name}: {name} should be a list of {elt_type}.")
-        return []
-
     def _set_chart_selected(self, max=0):
         name = "selected"
-        default_sel = self.__attributes.get(name)
-        if not isinstance(default_sel, list) and name in self.__attributes:
+        default_sel = self.__prop_values.get(name)
+        if not isinstance(default_sel, list) and name in self.__prop_values:
             default_sel = []
         if max == 0:
             self.__update_vars.extend(
@@ -696,10 +765,10 @@ class _Builder:
             return
         idx = 1
         name_idx = f"{name}[{idx}]"
-        sel = self.__attributes.get(name_idx)
-        if not isinstance(sel, list) and name_idx in self.__attributes:
+        sel = self.__prop_values.get(name_idx)
+        if not isinstance(sel, list) and name_idx in self.__prop_values:
             sel = []
-        while idx <= max or name_idx in self.__attributes:
+        while idx <= max or name_idx in self.__prop_values:
             if sel is not None or default_sel is not None:
                 self.__update_vars.extend(
                     self.__set_list_attribute(
@@ -711,14 +780,14 @@ class _Builder:
                 )
             idx += 1
             name_idx = f"{name}[{idx}]"
-            sel = self.__attributes.get(name_idx)
-            if not isinstance(sel, list) and name_idx in self.__attributes:
+            sel = self.__prop_values.get(name_idx)
+            if not isinstance(sel, list) and name_idx in self.__prop_values:
                 sel = []
 
     def _get_list_attribute(self, name: str, list_type: PropertyType):
         hash_name = self.__hashes.get(name)
         if hash_name is None:
-            list_val = self.__attributes.get(name)
+            list_val = self.__prop_values.get(name)
             if isinstance(list_val, str):
                 list_val = list(list_val.split(";"))
             if isinstance(list_val, list):
@@ -739,7 +808,7 @@ class _Builder:
 
     def __set_class_names(self):
         self.set_attribute("libClassName", self.__lib_name + "-" + self.__control_type.replace("_", "-"))
-        if (private_css := self.__attributes.get("style")) and isinstance(private_css, (dict, _MapDict)):
+        if (private_css := self.__prop_values.get("style")) and isinstance(private_css, (dict, _MapDict)):
             taipy_style = etree.Element("TaipyStyle")
             taipy_style.set("className", f"tpcss-{id(private_css)}")
             taipy_style.set(
@@ -751,7 +820,7 @@ class _Builder:
         return self.__set_dynamic_string_attribute("class_name", dynamic_property_name="dynamic_class_name")
 
     def _set_dataType(self):
-        value = self.__attributes.get("value")
+        value = self.__prop_values.get("value")
         return self.set_attribute("dataType", _get_data_type(value))
 
     def _set_file_content(self, var_name: str = "content"):
@@ -762,7 +831,7 @@ class _Builder:
         return self
 
     def _set_content(self, var_name: str = "content", image=True):
-        content = self.__attributes.get(var_name)
+        content = self.__prop_values.get(var_name)
         hash_name = self.__hashes.get(var_name)
         if content is None and hash_name is None:
             return self
@@ -776,41 +845,6 @@ class _Builder:
             )
         return self.set_attribute(_to_camel_case(f"default_{var_name}"), value)
 
-    def __set_dynamic_string_list(self, var_name: str, default_value: t.Any):
-        hash_name = self.__hashes.get(var_name)
-        loi = self.__attributes.get(var_name)
-        if loi is None:
-            loi = default_value
-        if isinstance(loi, str):
-            loi = [s.strip() for s in loi.split(";") if s.strip()]
-        if isinstance(loi, list):
-            self.__set_json_attribute(_to_camel_case(f"default_{var_name}"), loi)
-        if hash_name:
-            self.__update_vars.append(f"{var_name}={hash_name}")
-            self.__set_react_attribute(var_name, hash_name)
-        return self
-
-    def __set_dynamic_number_attribute(self, var_name: str, default_value: t.Any):
-        hash_name = self.__hashes.get(var_name)
-        numVal = self.__attributes.get(var_name)
-        if numVal is None:
-            numVal = default_value
-        if isinstance(numVal, str):
-            try:
-                numVal = float(numVal)
-            except Exception as e:
-                _warn(f"{self.__element_name}: {var_name} cannot be transformed into a number", e)
-                numVal = 0
-        if isinstance(numVal, numbers.Number):
-            self.__set_react_attribute(_to_camel_case(f"default_{var_name}"), numVal)
-        elif numVal is not None:
-            _warn(f"{self.__element_name}: {var_name} value is not valid ({numVal}).")
-        if hash_name:
-            hash_name = self.__get_typed_hash_name(hash_name, PropertyType.number)
-            self.__update_vars.append(f"{var_name}={hash_name}")
-            self.__set_react_attribute(var_name, hash_name)
-        return self
-
     def __set_default_value(
         self,
         var_name: str,
@@ -819,7 +853,7 @@ class _Builder:
         var_type: t.Optional[t.Union[PropertyType, t.Type[_TaipyBase]]] = None,
     ):
         if value is None:
-            value = self.__attributes.get(var_name)
+            value = self.__prop_values.get(var_name)
         default_var_name = _to_camel_case(f"default_{var_name}")
         if isinstance(value, (datetime, date, time)):
             return self.set_attribute(default_var_name, _date_to_string(value))
@@ -864,7 +898,7 @@ class _Builder:
         """
         var_name = self.__default_property_name if var_name is None else var_name
         if var_type == PropertyType.slider_value or var_type == PropertyType.toggle_value:
-            if self.__attributes.get("lov"):
+            if self.__prop_values.get("lov"):
                 var_type = PropertyType.lov_value
                 native_type = False
             elif var_type == PropertyType.toggle_value:
@@ -874,7 +908,7 @@ class _Builder:
             else:
                 var_type = (
                     PropertyType.dynamic_lo_numbers
-                    if isinstance(self.__attributes.get("value"), list)
+                    if isinstance(self.__prop_values.get("value"), list)
                     else PropertyType.dynamic_number
                 )
                 native_type = True
@@ -890,7 +924,7 @@ class _Builder:
                 self.__set_update_var_name(hash_name)
             if with_default:
                 if native_type:
-                    val = self.__attributes.get(var_name)
+                    val = self.__prop_values.get(var_name)
                     if native_type and isinstance(val, str):
                         with contextlib.suppress(Exception):
                             val = float(val)
@@ -898,9 +932,9 @@ class _Builder:
                 else:
                     self.__set_default_value(var_name, var_type=var_type)
         else:
-            if var_type == PropertyType.data and (self.__control_type != "chart" or "figure" not in self.__attributes):
+            if var_type == PropertyType.data and (self.__control_type != "chart" or "figure" not in self.__prop_values):
                 _warn(f"{self.__control_type}.{var_name} property should be bound.")
-            value = self.__attributes.get(var_name)
+            value = self.__prop_values.get(var_name)
             if value is not None:
                 if native_type:
                     if isinstance(value, str):
@@ -914,28 +948,28 @@ class _Builder:
         return self
 
     def _set_labels(self, var_name: str = "labels"):
-        if value := self.__attributes.get(var_name):
+        if value := self.__prop_values.get(var_name):
             if _is_true(value):
                 return self.__set_react_attribute(_to_camel_case(var_name), True)
             elif isinstance(value, (dict, _MapDict)):
-                return self.set_dict_attribute(var_name)
+                return self.__set_dict_attribute(var_name)
         return self
 
     def _set_partial(self):
         if self.__control_type not in _Builder.__BLOCK_CONTROLS:
             return self
-        if partial := self.__attributes.get("partial"):
-            if self.__attributes.get("page"):
+        if partial := self.__prop_values.get("partial"):
+            if self.__prop_values.get("page"):
                 _warn(f"{self.__element_name} control: page and partial should not be both defined.")
             if isinstance(partial, Partial):
-                self.__attributes["page"] = partial._route
+                self.__prop_values["page"] = partial._route
                 self.__set_react_attribute("partial", partial._route)
                 self.__set_react_attribute("defaultPartial", True)
         return self
 
     def _set_propagate(self):
         val = self.__get_boolean_attribute("propagate", t.cast(bool, self.__gui._config.config.get("propagate")))
-        return self if val else self.set_boolean_attribute("propagate", False)
+        return self if val else self.__set_boolean_attribute("propagate", False)
 
     def __set_refresh_on_update(self):
         if self.__update_vars:
@@ -945,7 +979,7 @@ class _Builder:
     def _set_table_pagesize_options(self, default_size=None):
         if default_size is None:
             default_size = [50, 100, 500]
-        page_size_options = self.__attributes.get("page_size_options", default_size)
+        page_size_options = self.__prop_values.get("page_size_options", default_size)
         if isinstance(page_size_options, str):
             try:
                 page_size_options = [int(s.strip()) for s in page_size_options.split(";")]
@@ -960,10 +994,10 @@ class _Builder:
     def _set_input_type(self, type_name: str, allow_password=False):
         if allow_password and self.__get_boolean_attribute("password", False):
             return self.set_attribute("type", "password")
-        return self.set_attribute("type", self.__attributes.get("type", type_name))
+        return self.set_attribute("type", self.__prop_values.get("type", type_name))
 
     def _set_kind(self):
-        if self.__attributes.get("theme", False):
+        if self.__prop_values.get("theme", False):
             self.set_attribute("mode", "theme")
         return self
 
@@ -975,21 +1009,6 @@ class _Builder:
             hash_name = self.__gui._evaluate_bind_holder(t.cast(t.Type[_TaipyBase], taipy_type), expr)
         return hash_name
 
-    def __set_dynamic_bool_attribute(self, name: str, def_val: t.Any, with_update: bool, update_main=True):
-        hash_name = self.__hashes.get(name)
-        val = self.__get_boolean_attribute(name, def_val)
-        default_name = f"default_{name}" if hash_name is not None else name
-        if val != def_val:
-            self.set_boolean_attribute(default_name, val)
-        if hash_name is not None:
-            hash_name = self.__get_typed_hash_name(hash_name, PropertyType.dynamic_boolean)
-            self.__set_react_attribute(_to_camel_case(name), _get_client_var_name(hash_name))
-            if with_update:
-                if update_main:
-                    self.__set_update_var_name(hash_name)
-                else:
-                    self.__update_vars.append(f"{_to_camel_case(name)}={hash_name}")
-
     def __set_dynamic_property_without_default(
         self, name: str, property_type: PropertyType, optional: t.Optional[bool] = False
     ):
@@ -1021,12 +1040,12 @@ class _Builder:
         return self.__set_react_attribute(_to_camel_case(property_name), _get_client_var_name(front_var))
 
     def _set_indexed_icons(self, name="use_icon"):
-        global_icon = self.__attributes.get(name)
+        global_icon = self.__prop_values.get(name)
         indexed = self.get_name_indexed_property(name)
         global_bool = _is_true(global_icon) if global_icon is not None and _is_boolean(global_icon) else None
         if global_icon is not None and not indexed:
             if global_bool is not None:
-                self.set_boolean_attribute(name, global_bool)
+                self.__set_boolean_attribute(name, global_bool)
             else:
                 self.__set_json_attribute(_to_camel_case(name), {"__default": str(global_icon)})
         elif indexed:
@@ -1047,19 +1066,25 @@ class _Builder:
 
             attributes (list(tuple)): The list of attributes as (property name, property type, default value).
         """
-        attributes.append(("id",))  # Every element should have an id attribute
+        # Every element must have an id attribute
+        if not any(attr[0] == "id" for attr in attributes):
+            attributes.append(("id",))
         for attr in attributes:
             if not isinstance(attr, tuple):
                 attr = (attr,)
             var_type = _get_tuple_val(attr, 1, PropertyType.string)
             if var_type == PropertyType.to_json:
                 var_type = _TaipyToJson
-            if var_type == PropertyType.boolean:
+            if var_type == PropertyType.any:
+                self.__set_any_attribute(attr[0], _get_tuple_val(attr, 2, None))
+            elif var_type == PropertyType.dynamic_any:
+                self.__set_dynamic_any_attribute(attr[0], _get_tuple_val(attr, 2, None))
+            elif var_type == PropertyType.boolean:
                 def_val = _get_tuple_val(attr, 2, False)
-                if isinstance(def_val, bool) or self.__attributes.get(attr[0], None) is not None:
+                if isinstance(def_val, bool) or self.__prop_values.get(attr[0], None) is not None:
                     val = self.__get_boolean_attribute(attr[0], def_val)
                     if val != def_val:
-                        self.set_boolean_attribute(attr[0], val)
+                        self.__set_boolean_attribute(attr[0], val)
             elif var_type == PropertyType.dynamic_boolean:
                 self.__set_dynamic_bool_attribute(
                     attr[0],
@@ -1068,7 +1093,7 @@ class _Builder:
                     _get_tuple_val(attr, 4, True),
                 )
             elif var_type == PropertyType.number:
-                self.set_number_attribute(attr[0], _get_tuple_val(attr, 2, None))
+                self.__set_number_attribute(attr[0], _get_tuple_val(attr, 2, None))
             elif var_type == PropertyType.dynamic_number:
                 self.__set_dynamic_number_attribute(attr[0], _get_tuple_val(attr, 2, None))
             elif var_type == PropertyType.string:
@@ -1082,12 +1107,27 @@ class _Builder:
                     self.__set_list_attribute(
                         attr[0],
                         self.__hashes.get(attr[0]),
-                        self.__attributes.get(attr[0]),
+                        self.__prop_values.get(attr[0]),
                         str,
                         False,
                         _get_tuple_val(attr, 2, None),
                     )
                 )
+            elif var_type == PropertyType.string_or_number:
+                self.__set_string_or_number_attribute(attr[0], _get_tuple_val(attr, 2, None))
+            elif var_type == PropertyType.dynamic_list:
+                self.__set_dynamic_string_list(attr[0], _get_tuple_val(attr, 2, None))
+            elif var_type == PropertyType.dict:
+                self.__set_dict_attribute(attr[0], _get_tuple_val(attr, 2, None))
+            elif var_type == PropertyType.dynamic_dict:
+                self.__set_dynamic_dict_attribute(attr[0], _get_tuple_val(attr, 2, None))
+            elif var_type == PropertyType.boolean_or_list:
+                if _is_boolean(self.__prop_values.get(attr[0])):
+                    self.__set_dynamic_bool_attribute(attr[0], _get_tuple_val(attr, 2, False), True, update_main=False)
+                else:
+                    self.__set_dynamic_string_list(attr[0], _get_tuple_val(attr, 2, None))
+            elif var_type == PropertyType.dynamic_date:
+                self.__set_dynamic_date_attribute(attr[0], _get_tuple_val(attr, 2, None))
             elif var_type == PropertyType.function:
                 self.__set_function_attribute(attr[0], _get_tuple_val(attr, 2, None), _get_tuple_val(attr, 3, True))
             elif var_type == PropertyType.react:
@@ -1096,26 +1136,13 @@ class _Builder:
                     self.__update_vars.append(f"{prop_name}={hash_name}")
                     self.__set_react_attribute(prop_name, hash_name)
                 else:
-                    self.__set_react_attribute(prop_name, self.__attributes.get(attr[0], _get_tuple_val(attr, 2, None)))
+                    self.__set_react_attribute(
+                        prop_name, self.__prop_values.get(attr[0], _get_tuple_val(attr, 2, None))
+                    )
             elif var_type == PropertyType.broadcast:
                 self.__set_react_attribute(
                     _to_camel_case(attr[0]), _get_broadcast_var_name(_get_tuple_val(attr, 2, None))
                 )
-            elif var_type == PropertyType.string_or_number:
-                self.__set_string_or_number_attribute(attr[0], _get_tuple_val(attr, 2, None))
-            elif var_type == PropertyType.dict:
-                self.set_dict_attribute(attr[0], _get_tuple_val(attr, 2, None))
-            elif var_type == PropertyType.dynamic_dict:
-                self.set_dynamic_dict_attribute(attr[0], _get_tuple_val(attr, 2, None))
-            elif var_type == PropertyType.dynamic_list:
-                self.__set_dynamic_string_list(attr[0], _get_tuple_val(attr, 2, None))
-            elif var_type == PropertyType.boolean_or_list:
-                if _is_boolean(self.__attributes.get(attr[0])):
-                    self.__set_dynamic_bool_attribute(attr[0], _get_tuple_val(attr, 2, False), True, update_main=False)
-                else:
-                    self.__set_dynamic_string_list(attr[0], _get_tuple_val(attr, 2, None))
-            elif var_type == PropertyType.dynamic_date:
-                self.__set_dynamic_date_attribute(attr[0], _get_tuple_val(attr, 2, None))
             elif var_type == PropertyType.data:
                 self.__set_dynamic_property_without_default(attr[0], t.cast(PropertyType, var_type))
             elif (
@@ -1142,7 +1169,7 @@ class _Builder:
                     self.__update_vars.append(f"{prop_name}={hash_name}")
                     self.__set_react_attribute(prop_name, hash_name)
                 else:
-                    val = self.__attributes.get(attr[0])
+                    val = self.__prop_values.get(attr[0])
                     self.set_attribute(
                         prop_name, var_type(_get_tuple_val(attr, 2, None) if val is None else val, "").get()
                     )

+ 31 - 31
taipy/gui/_renderers/factory.py

@@ -75,7 +75,7 @@ class _Factory:
             gui=gui,
             control_type=control_type,
             element_name="Alert",
-            attributes=attrs,
+            prop_values=attrs,
         )
         .set_value_and_default(var_type=PropertyType.dynamic_string)
         .set_attributes(
@@ -89,7 +89,7 @@ class _Factory:
             gui=gui,
             control_type=control_type,
             element_name="Button",
-            attributes=attrs,
+            prop_values=attrs,
         )
         .set_value_and_default(with_update=False)
         .set_attributes(
@@ -103,7 +103,7 @@ class _Factory:
             ]
         ),
         "chat": lambda gui, control_type, attrs: _Builder(
-            gui=gui, control_type=control_type, element_name="Chat", attributes=attrs, default_value=None
+            gui=gui, control_type=control_type, element_name="Chat", prop_values=attrs, default_value=None
         )
         .set_value_and_default(with_update=True, with_default=False, var_type=PropertyType.data)
         .set_attributes(
@@ -123,7 +123,7 @@ class _Factory:
             ]
         ),
         "chart": lambda gui, control_type, attrs: _Builder(
-            gui=gui, control_type=control_type, element_name="Chart", attributes=attrs, default_value=None
+            gui=gui, control_type=control_type, element_name="Chart", prop_values=attrs, default_value=None
         )
         .set_value_and_default(with_default=False, var_type=PropertyType.data)
         .set_attributes(
@@ -148,13 +148,13 @@ class _Factory:
         ._get_chart_config("scatter", "lines+markers")
         ._set_propagate(),
         "content": lambda gui, control_type, attrs: _Builder(
-            gui=gui, control_type=control_type, element_name="PageContent", attributes=attrs
+            gui=gui, control_type=control_type, element_name="PageContent", prop_values=attrs
         ),
         "date": lambda gui, control_type, attrs: _Builder(
             gui=gui,
             control_type=control_type,
             element_name="DateSelector",
-            attributes=attrs,
+            prop_values=attrs,
             default_value=datetime.fromtimestamp(0),
         )
         .set_value_and_default(var_type=PropertyType.date)
@@ -178,7 +178,7 @@ class _Factory:
             gui=gui,
             control_type=control_type,
             element_name="DateRange",
-            attributes=attrs,
+            prop_values=attrs,
         )
         .set_value_and_default(var_type=PropertyType.date_range)
         .set_attributes(
@@ -200,7 +200,7 @@ class _Factory:
             gui=gui,
             control_type=control_type,
             element_name="Dialog",
-            attributes=attrs,
+            prop_values=attrs,
         )
         .set_value_and_default(var_type=PropertyType.dynamic_boolean)
         ._set_partial()  # partial should be set before page
@@ -221,7 +221,7 @@ class _Factory:
         )
         ._set_propagate(),
         "expandable": lambda gui, control_type, attrs: _Builder(
-            gui=gui, control_type=control_type, element_name="Expandable", attributes=attrs, default_value=None
+            gui=gui, control_type=control_type, element_name="Expandable", prop_values=attrs, default_value=None
         )
         .set_value_and_default()
         ._set_partial()  # partial should be set before page
@@ -237,7 +237,7 @@ class _Factory:
             gui=gui,
             control_type=control_type,
             element_name="FileDownload",
-            attributes=attrs,
+            prop_values=attrs,
         )
         .set_value_and_default(var_name="label", with_update=False)
         ._set_content("content", image=False)
@@ -257,7 +257,7 @@ class _Factory:
             gui=gui,
             control_type=control_type,
             element_name="FileSelector",
-            attributes=attrs,
+            prop_values=attrs,
         )
         .set_value_and_default(var_name="label", with_update=False)
         ._set_file_content()
@@ -278,7 +278,7 @@ class _Factory:
             gui=gui,
             control_type=control_type,
             element_name="Image",
-            attributes=attrs,
+            prop_values=attrs,
         )
         .set_value_and_default(var_name="label", with_update=False)
         ._set_content("content")
@@ -295,7 +295,7 @@ class _Factory:
             gui=gui,
             control_type=control_type,
             element_name="Indicator",
-            attributes=attrs,
+            prop_values=attrs,
         )
         .set_value_and_default(with_update=False, native_type=True)
         .set_attributes(
@@ -313,7 +313,7 @@ class _Factory:
             gui=gui,
             control_type=control_type,
             element_name="Input",
-            attributes=attrs,
+            prop_values=attrs,
         )
         ._set_input_type("text", True)
         .set_value_and_default()
@@ -334,7 +334,7 @@ class _Factory:
             ]
         ),
         "layout": lambda gui, control_type, attrs: _Builder(
-            gui=gui, control_type=control_type, element_name="Layout", attributes=attrs, default_value=None
+            gui=gui, control_type=control_type, element_name="Layout", prop_values=attrs, default_value=None
         )
         .set_value_and_default(with_default=False)
         .set_attributes(
@@ -344,7 +344,7 @@ class _Factory:
             ]
         ),
         "login": lambda gui, control_type, attrs: _Builder(
-            gui=gui, control_type=control_type, element_name="Login", attributes=attrs, default_value=None
+            gui=gui, control_type=control_type, element_name="Login", prop_values=attrs, default_value=None
         )
         .set_value_and_default(default_val="Log-in")
         .set_attributes(
@@ -358,7 +358,7 @@ class _Factory:
             gui=gui,
             control_type=control_type,
             element_name="MenuCtl",
-            attributes=attrs,
+            prop_values=attrs,
         )
         .set_attributes(
             [
@@ -378,7 +378,7 @@ class _Factory:
             gui=gui,
             control_type=control_type,
             element_name="Metric",
-            attributes=attrs,
+            prop_values=attrs,
         )
         .set_value_and_default(var_type=PropertyType.dynamic_number, native_type=True)
         .set_attributes(
@@ -407,7 +407,7 @@ class _Factory:
             ]
         ),
         "navbar": lambda gui, control_type, attrs: _Builder(
-            gui=gui, control_type=control_type, element_name="NavBar", attributes=attrs, default_value=None
+            gui=gui, control_type=control_type, element_name="NavBar", prop_values=attrs, default_value=None
         ).set_attributes(
             [
                 ("active", PropertyType.dynamic_boolean, True),
@@ -419,7 +419,7 @@ class _Factory:
             gui=gui,
             control_type=control_type,
             element_name="Input",
-            attributes=attrs,
+            prop_values=attrs,
             default_value=0,
         )
         ._set_input_type("number")
@@ -442,7 +442,7 @@ class _Factory:
             ]
         ),
         "pane": lambda gui, control_type, attrs: _Builder(
-            gui=gui, control_type=control_type, element_name="Pane", attributes=attrs, default_value=None
+            gui=gui, control_type=control_type, element_name="Pane", prop_values=attrs, default_value=None
         )
         .set_value_and_default(var_type=PropertyType.dynamic_boolean)
         ._set_partial()  # partial should be set before page
@@ -462,7 +462,7 @@ class _Factory:
         )
         ._set_propagate(),
         "part": lambda gui, control_type, attrs: _Builder(
-            gui=gui, control_type=control_type, element_name="Part", attributes=attrs, default_value=None
+            gui=gui, control_type=control_type, element_name="Part", prop_values=attrs, default_value=None
         )
         ._set_partial()  # partial should be set before page
         .set_attributes(
@@ -478,7 +478,7 @@ class _Factory:
             gui=gui,
             control_type=control_type,
             element_name="Progress",
-            attributes=attrs,
+            prop_values=attrs,
         )
         .set_value_and_default(var_type=PropertyType.dynamic_number, native_type=True)
         .set_attributes(
@@ -492,7 +492,7 @@ class _Factory:
             ]
         ),
         "selector": lambda gui, control_type, attrs: _Builder(
-            gui=gui, control_type=control_type, element_name="Selector", attributes=attrs, default_value=None
+            gui=gui, control_type=control_type, element_name="Selector", prop_values=attrs, default_value=None
         )
         .set_value_and_default(with_default=False, var_type=PropertyType.lov_value)
         .set_attributes(
@@ -518,7 +518,7 @@ class _Factory:
             gui=gui,
             control_type=control_type,
             element_name="Slider",
-            attributes=attrs,
+            prop_values=attrs,
             default_value=0,
         )
         .set_value_and_default(native_type=True, var_type=PropertyType.slider_value)
@@ -546,7 +546,7 @@ class _Factory:
             gui=gui,
             control_type=control_type,
             element_name="Status",
-            attributes=attrs,
+            prop_values=attrs,
         )
         .set_value_and_default(with_update=False)
         .set_attributes(
@@ -560,7 +560,7 @@ class _Factory:
             gui=gui,
             control_type=control_type,
             element_name="Table",
-            attributes=attrs,
+            prop_values=attrs,
         )
         .set_value_and_default(with_default=False, var_type=PropertyType.data)
         ._get_dataframe_attributes()
@@ -594,7 +594,7 @@ class _Factory:
             gui=gui,
             control_type=control_type,
             element_name="Field",
-            attributes=attrs,
+            prop_values=attrs,
         )
         .set_value_and_default(with_update=False)
         ._set_dataType()
@@ -611,7 +611,7 @@ class _Factory:
             gui=gui,
             control_type=control_type,
             element_name="TimeSelector",
-            attributes=attrs,
+            prop_values=attrs,
             default_value=datetime.today().time(),
         )
         .set_value_and_default(var_type=PropertyType.time)
@@ -628,7 +628,7 @@ class _Factory:
         )
         ._set_propagate(),
         "toggle": lambda gui, control_type, attrs: _Builder(
-            gui=gui, control_type=control_type, element_name="Toggle", attributes=attrs, default_value=None
+            gui=gui, control_type=control_type, element_name="Toggle", prop_values=attrs, default_value=None
         )
         .set_value_and_default(with_default=False, var_type=PropertyType.toggle_value)
         .set_attributes(
@@ -650,7 +650,7 @@ class _Factory:
             gui=gui,
             control_type=control_type,
             element_name="TreeView",
-            attributes=attrs,
+            prop_values=attrs,
         )
         .set_value_and_default(with_default=False, var_type=PropertyType.lov_value)
         .set_attributes(

+ 4 - 7
taipy/gui/builder/_api_generator.py

@@ -89,10 +89,7 @@ class _ElementApiGenerator(object, metaclass=_Singleton):
                 f"Python API for extension library '{library_name}' is not available. To fix this, import 'taipy.gui.builder' before importing the extension library."  # noqa: E501
             )
             return
-        library_module = getattr(self.__module, library_name, None)
-        if library_module is None:
-            library_module = types.ModuleType(library_name)
-            setattr(self.__module, library_name, library_module)
+        library_module = sys.modules[library.__module__]
         for element_name, element in library.get_elements().items():
             setattr(
                 library_module,
@@ -104,13 +101,13 @@ class _ElementApiGenerator(object, metaclass=_Singleton):
                     {name: str(prop.property_type) for name, prop in element.attributes.items()},
                 ),
             )
-            # Allow element to be accessed from the root module
+            # Allow element to be accessed from this module (taipy.gui.builder)
             if hasattr(self.__module, element_name):
                 _TaipyLogger._get_logger().info(
                     f"Can't add element `{element_name}` of library `{library_name}` to the root of Builder API as another element with the same name already exists."  # noqa: E501
                 )
-                continue
-            setattr(self.__module, element_name, getattr(library_module, element_name))
+            else:
+                setattr(self.__module, element_name, getattr(library_module, element_name))
 
     @staticmethod
     def create_block_api(

+ 138 - 0
taipy/gui/extension/__main__.py

@@ -0,0 +1,138 @@
+# © 2021-2025, Avaiga Pte Ltd. All Rights Reserved. The use of the Taipy software and any part thereof is governed by
+# Avaiga Pte Ltd's Software License and Maintenance Agreement. Unauthorised use, reproduction and modification is
+# strictly not allowed.
+
+import argparse
+import os
+import typing as t
+from io import StringIO
+
+from taipy.gui.extension import ElementLibrary, PropertyType
+
+
+def error(message):
+    print(message)  # noqa: T201
+    exit(1)
+
+
+def generate_doc(library: ElementLibrary) -> str:  # noqa: C901F
+    stream = StringIO()
+
+    def clean_doc_string(doc_string) -> t.Optional[str]:
+        if not doc_string:
+            return None
+        lines = doc_string.splitlines()
+        min_indent = min((len(line) - len(line.lstrip())) for line in lines if line.strip())
+        lines = [line[min_indent:] if line.strip() else "" for line in lines]
+        while lines and not lines[0].strip():
+            lines.pop(0)
+        while lines and not lines[-1].strip():
+            lines.pop()
+        return "\n".join(lines) if lines else None
+
+    print("# ----------------------------------------------------------------------", file=stream)
+    print("# Generated by taipy.gui.extension module", file=stream)
+    print("# ----------------------------------------------------------------------", file=stream)
+    for element_name, element in library.get_elements().items():
+        properties: list[str] = []
+        property_doc = {}
+        default_property_found = False
+        for property_name, property in element.attributes.items():
+            desc = property_name
+            # Could use 'match' with Python >= 3.10
+            if property.property_type in [PropertyType.boolean, PropertyType.dynamic_boolean]:
+                desc = desc + ": bool"
+            elif property.property_type in [PropertyType.string, PropertyType.dynamic_string]:
+                desc = desc + ": str"
+            elif property.property_type in [PropertyType.dict, PropertyType.dynamic_dict]:
+                desc = desc + ": dict"
+            if property_name == element.default_attribute:
+                properties.insert(0, desc)
+                default_property_found = True
+            else:
+                properties.append(desc)
+            if doc_string := clean_doc_string(property.doc_string):
+                property_doc[property_name] = doc_string
+        if default_property_found and len(properties) > 1:
+            properties.insert(1, "*")
+        doc_string = clean_doc_string(element.doc_string)
+        documentation = ""
+        if doc_string:
+            lines = doc_string.splitlines()
+            documentation = f'    """{lines.pop(0)}\n'
+            while lines:
+                line = lines.pop(0)
+                documentation += f"    {line}\n" if line else "\n"
+        if property_doc:
+            documentation += "\n    Arguments:\n"
+            for property_name, doc_string in property_doc.items():
+                lines = doc_string.splitlines()
+                documentation += f"        {property_name}: {lines.pop(0)}\n"
+                while lines:
+                    line = lines.pop(0)
+                    if line:
+                        documentation += f"        {line}\n"
+        if documentation:
+            documentation += '    """\n'
+        print(f"def {element_name}({', '.join(properties)}):\n{documentation}    ...\n\n", file=stream)
+
+    return stream.getvalue()
+
+
+def generate_tgb(args) -> None:
+    from importlib import import_module
+    from inspect import getmembers, isclass
+
+    package_root_dir = args.package_root_dir[0]
+    # Remove potential directory separator at the end of the package root dir
+    if package_root_dir[-1] == "/" or package_root_dir[-1] == "\\":
+        package_root_dir = package_root_dir[:-1]
+    module = None
+    try:
+        module = import_module(package_root_dir)
+    except Exception as e:
+        error(f"Couldn't open module '{package_root_dir}' ({e})")
+    library: t.Optional[ElementLibrary] = None
+    for _, member in getmembers(module, lambda o: isclass(o) and issubclass(o, ElementLibrary)):
+        if library:
+            error("Extension contains more than one ElementLibrary")
+        library = member()
+    if library is None:
+        error("Extension does not contain any ElementLibrary")
+        return  # To avoid having to deal with this case in the following code
+    pyi_path = os.path.join(package_root_dir, "__init__.pyi")
+    pyi_file = None
+    try:
+        pyi_file = open(pyi_path, "w")
+    except Exception as e:
+        error(f"Couldn't open Python Interface Definition file '{pyi_file}' ({e})")
+
+    print(f"Inspecting extension library '{library.get_name()}'")  # noqa: T201
+    content = generate_doc(library)
+
+    if pyi_file:
+        print(content, file=pyi_file)
+        pyi_file.close()
+    print(f"File '{pyi_path}' was updated.")  # noqa: T201
+
+
+def main(arg_strings=None) -> None:
+    parser = argparse.ArgumentParser(description="taipy.gui.extensions entry point.")
+    sub_parser = parser.add_subparsers(dest="command", help="Commands to run", required=True)
+
+    tgb_generation = sub_parser.add_parser(
+        "generate_tgb", aliases=["api"], help="Generate Page Builder API for a Taipy GUI extension package."
+    )
+    tgb_generation.add_argument(
+        dest="package_root_dir",
+        nargs=1,
+        help="The root dir of the extension package." + " This directory must contain a __init__.py file.",
+    )
+    tgb_generation.set_defaults(func=generate_tgb)
+
+    args = parser.parse_args(arg_strings)
+    args.func(args)
+
+
+if __name__ == "__main__":
+    main()

+ 93 - 50
taipy/gui/extension/library.py

@@ -43,6 +43,8 @@ class ElementProperty:
         default_value: t.Optional[t.Any] = None,
         js_name: t.Optional[str] = None,
         with_update: t.Optional[bool] = None,
+        *,
+        doc_string: t.Optional[str] = None,
     ) -> None:
         """Initializes a new custom property declaration for an `Element^`.
 
@@ -53,6 +55,8 @@ class ElementProperty:
                 If unspecified, a camel case version of `name` is generated: for example, if `name` is
                 "my_property_name", then this property is referred to as "myPropertyName" in the
                 JavaScript code.
+            doc_string: An optional string that holds documentation for that property.<br/>
+                This is used when generating the stub classes for extension libraries.
         """
         self.default_value = default_value
         self.property_type: t.Union[PropertyType, t.Type[_TaipyBase]]
@@ -66,6 +70,7 @@ class ElementProperty:
             self.property_type = property_type
         self._js_name = js_name
         self.with_update = with_update
+        self.doc_string = doc_string
         super().__init__()
 
     def check(self, element_name: str, prop_name: str):
@@ -95,16 +100,14 @@ class Element:
     what the default property name is.
     """
 
-    __RE_PROP_VAR = re.compile(r"<tp:prop:(\w+)>")
-    __RE_UNIQUE_VAR = re.compile(r"<tp:uniq:(\w+)>")
-
     def __init__(
         self,
         default_property: str,
         properties: t.Dict[str, ElementProperty],
         react_component: t.Optional[str] = None,
+        *,
         render_xhtml: t.Optional[t.Callable[[t.Dict[str, t.Any]], str]] = None,
-        inner_properties: t.Optional[t.Dict[str, ElementProperty]] = None,
+        doc_string: t.Optional[str] = None,
     ) -> None:
         """Initializes a new custom element declaration.
 
@@ -112,21 +115,24 @@ class Element:
         *react_component* is ignored.
 
         Arguments:
-            default_property (str): The name of the default property for this element.
-            properties (Dict[str, ElementProperty]): The dictionary containing the properties of this element, where the keys are the property names and the values are instances of ElementProperty.
-            inner_properties (Optional[List[ElementProperty]]): The optional list of inner properties for this element.<br/>
-                Default values are set/bound automatically.
-            react_component (Optional[str]): The name of the component to be created on the front-end.<br/>
+            default_property: The name of the default property for this element.
+            properties: The dictionary containing the properties of this element, where
+                the keys are the property names and the values are instances of ElementProperty.
+            react_component: The name of the component to be created on the front-end.<br/>
                 If not specified, it is set to a camel case version of the element's name
                 ("one_name" is transformed to "OneName").
-            render_xhtml (Optional[callable[[dict[str, Any]], str]]): A function that receives a
-                dictionary containing the element's properties and their values
-                and that must return a valid XHTML string.
+            render_xhtml: A function that receives a dictionary containing the element's properties and their values
+                and that must return a valid XHTML string.<br/>
+                This is used to implement static elements.
+            doc_string: The documentation text for this element or None if there is none, which is
+                the default.<br/>
+                This string is used when generating stub functions so elements of extension libraries
+                can be used with the Page Builder API.
         """  # noqa: E501
         self.default_attribute = default_property
         self.attributes = properties
-        self.inner_properties = inner_properties
         self.js_name = react_component
+        self.doc_string = doc_string
         if callable(render_xhtml):
             self._render_xhtml = render_xhtml
         super().__init__()
@@ -152,6 +158,9 @@ class Element:
     def _is_server_only(self):
         return hasattr(self, "_render_xhtml") and callable(self._render_xhtml)
 
+    def _process_inner_properties(self, _gui: "Gui", _attributes: t.Dict[str, t.Any], _counter: int):
+        pass
+
     def _call_builder(
         self,
         name,
@@ -162,40 +171,7 @@ class Element:
         counter: int = 0,
     ) -> t.Union[t.Any, t.Tuple[str, str]]:
         attributes = properties if isinstance(properties, dict) else {}
-        if self.inner_properties:
-            uniques: t.Dict[str, int] = {}
-            self.attributes.update(
-                {
-                    prop: ElementProperty(attr.property_type, None, attr._js_name, attr.with_update)
-                    for prop, attr in self.inner_properties.items()
-                }
-            )
-            for prop, attr in self.inner_properties.items():
-                val = attr.default_value
-                if val:
-                    # handling property replacement in inner properties <tp:prop:...>
-                    while m := Element.__RE_PROP_VAR.search(val):
-                        var = attributes.get(m.group(1))
-                        hash_value = None if var is None else gui._evaluate_expr(var)
-                        if hash_value:
-                            names = gui._get_real_var_name(hash_value)
-                            hash_value = names[0] if isinstance(names, tuple) else names
-                        else:
-                            hash_value = "None"
-                        val = val[: m.start()] + hash_value + val[m.end() :]
-                    # handling unique id replacement in inner properties <tp:uniq:...>
-                    has_uniq = False
-                    while m := Element.__RE_UNIQUE_VAR.search(val):
-                        has_uniq = True
-                        id = uniques.get(m.group(1))
-                        if id is None:
-                            id = len(uniques) + 1
-                            uniques[m.group(1)] = id
-                        val = f"{val[: m.start()]}{counter}{id}{val[m.end() :]}"
-                    if has_uniq and gui._is_expression(val):
-                        gui._evaluate_expr(val, True)
-
-                attributes[prop] = val
+        self._process_inner_properties(gui, attributes, counter)
         # this modifies attributes
         hash_names = _Builder._get_variable_hash_names(gui, attributes)  # variable replacement
         # call user render if any
@@ -226,7 +202,7 @@ class Element:
                 gui=gui,
                 control_type=name,
                 element_name=f"{lib.get_js_module_name()}_{self._get_js_name(name)}",
-                attributes=properties,
+                prop_values=properties,
                 hash_names=hash_names,
                 lib_name=lib.get_name(),
                 default_value=default_value,
@@ -265,7 +241,7 @@ class ElementLibrary(ABC):
         The default implementation returns an empty dictionary, indicating that this library
         contains no custom visual elements.
         """
-        return {}
+        pass
 
     @abstractmethod
     def get_name(self) -> str:
@@ -295,7 +271,7 @@ class ElementLibrary(ABC):
             because each JavaScript module will have to have a unique name.
 
         """
-        raise NotImplementedError
+        pass
 
     def get_js_module_name(self) -> str:
         """
@@ -464,3 +440,70 @@ class ElementLibrary(ABC):
             This version will be appended to the resource URL as a query arg (?v=<version>)
         """
         return None
+
+
+class _ElementWithInnerProps(Element):
+    __RE_PROP_VAR = re.compile(r"<tp:prop:(\w+)>")
+    __RE_UNIQUE_VAR = re.compile(r"<tp:uniq:(\w+)>")
+
+    def __init__(
+        self,
+        default_property: str,
+        properties: t.Dict[str, ElementProperty],
+        react_component: t.Optional[str] = None,
+        render_xhtml: t.Optional[t.Callable[[t.Dict[str, t.Any]], str]] = None,
+        doc_string: t.Optional[str] = None,
+        *,
+        inner_properties: t.Optional[t.Dict[str, ElementProperty]] = None,
+    ) -> None:
+        """NOT DOCUMENTED
+
+        Arguments:
+            inner_properties (Optional[List[ElementProperty]]): The optional list of inner properties
+                for this element.<br/>
+                Default values are set/bound automatically.
+        """
+        super().__init__(
+            default_property=default_property,
+            properties=properties,
+            react_component=react_component,
+            render_xhtml=render_xhtml,
+            doc_string=doc_string,
+        )
+        self.inner_properties = inner_properties
+
+    def _process_inner_properties(self, gui: "Gui", attributes: t.Dict[str, t.Any], counter: int):
+        if self.inner_properties:
+            uniques: t.Dict[str, int] = {}
+            self.attributes.update(
+                {
+                    prop: ElementProperty(attr.property_type, None, attr._js_name, attr.with_update)
+                    for prop, attr in self.inner_properties.items()
+                }
+            )
+            for prop, attr in self.inner_properties.items():
+                val = attr.default_value
+                if val:
+                    # handling property replacement in inner properties <tp:prop:...>
+                    while m := _ElementWithInnerProps.__RE_PROP_VAR.search(val):
+                        var = attributes.get(m.group(1))
+                        hash_value = None if var is None else gui._evaluate_expr(var)
+                        if hash_value:
+                            names = gui._get_real_var_name(hash_value)
+                            hash_value = names[0] if isinstance(names, tuple) else names
+                        else:
+                            hash_value = "None"
+                        val = val[: m.start()] + hash_value + val[m.end() :]
+                    # handling unique id replacement in inner properties <tp:uniq:...>
+                    has_uniq = False
+                    while m := _ElementWithInnerProps.__RE_UNIQUE_VAR.search(val):
+                        has_uniq = True
+                        id = uniques.get(m.group(1))
+                        if id is None:
+                            id = len(uniques) + 1
+                            uniques[m.group(1)] = id
+                        val = f"{val[: m.start()]}{counter}{id}{val[m.end() :]}"
+                    if has_uniq and gui._is_expression(val):
+                        gui._evaluate_expr(val, True)
+
+                attributes[prop] = val

+ 12 - 0
taipy/gui/types.py

@@ -72,6 +72,14 @@ class PropertyType(Enum):
     See `ElementProperty^` for more details.
     """
 
+    any = "any"
+    """
+    The property holds a value of any serializable type.
+    """
+    dynamic_any = "dynamicany"
+    """
+    The property is dynamic and holds a value of any serializable type.
+    """
     boolean = "boolean"
     """
     The property holds a Boolean value.
@@ -109,6 +117,10 @@ class PropertyType(Enum):
     dynamic_list = "dynamiclist"
     """
     The property is dynamic and holds a list.
+
+    The React component must have two parameters: "<propertyName>" that must be a list of object, and
+    "default<PropertyName>" that must be a string, set to the JSON representation of the initial value
+    of the property.
     """
     dynamic_string = "dynamicstring"
     """

+ 8 - 7
taipy/gui_core/_GuiCoreLib.py

@@ -15,6 +15,7 @@ from datetime import datetime
 from taipy.core import Cycle, DataNode, Job, Scenario, Sequence, Task
 from taipy.gui import Gui, State
 from taipy.gui.extension import Element, ElementLibrary, ElementProperty, PropertyType
+from taipy.gui.extension.library import _ElementWithInnerProps
 
 from ..version import _get_version
 from ._adapters import (
@@ -65,8 +66,8 @@ class _GuiCore(ElementLibrary):
     __DATANODE_SELECTOR_SORT_VAR = "__tpgc_dn_sort"
     __DATANODE_SELECTOR_ERROR_VAR = "__tpgc_dn_error"
 
-    __elements = {
-        "scenario_selector": Element(
+    __elements: dict[str, Element] = {
+        "scenario_selector": _ElementWithInnerProps(
             "value",
             {
                 "id": ElementProperty(PropertyType.string),
@@ -113,7 +114,7 @@ class _GuiCore(ElementLibrary):
                 ),
             },
         ),
-        "scenario": Element(
+        "scenario": _ElementWithInnerProps(
             "scenario",
             {
                 "id": ElementProperty(PropertyType.string),
@@ -144,7 +145,7 @@ class _GuiCore(ElementLibrary):
                 ),
             },
         ),
-        "scenario_dag": Element(
+        "scenario_dag": _ElementWithInnerProps(
             "scenario",
             {
                 "id": ElementProperty(PropertyType.string),
@@ -161,7 +162,7 @@ class _GuiCore(ElementLibrary):
                 "on_select": ElementProperty(PropertyType.function, f"{{{__CTX_VAR_NAME}.on_dag_select}}"),
             },
         ),
-        "data_node_selector": Element(
+        "data_node_selector": _ElementWithInnerProps(
             "value",
             {
                 "id": ElementProperty(PropertyType.string),
@@ -198,7 +199,7 @@ class _GuiCore(ElementLibrary):
                 ),
             },
         ),
-        "data_node": Element(
+        "data_node": _ElementWithInnerProps(
             __DATANODE_VIZ_DATA_NODE_PROP,
             {
                 "id": ElementProperty(PropertyType.string),
@@ -274,7 +275,7 @@ class _GuiCore(ElementLibrary):
                 ),
             },
         ),
-        "job_selector": Element(
+        "job_selector": _ElementWithInnerProps(
             "value",
             {
                 "id": ElementProperty(PropertyType.string),

+ 2 - 1
tests/gui/extension/test_library.py

@@ -16,6 +16,7 @@ import pytest
 
 from taipy.gui import Gui
 from taipy.gui.extension import Element, ElementLibrary, ElementProperty, PropertyType
+from taipy.gui.extension.library import _ElementWithInnerProps
 
 
 def render_xhtml_4_my_library(properties: t.Dict[str, t.Any]) -> str:
@@ -53,7 +54,7 @@ class MyLibrary(ElementLibrary):
             "h1",
             render_xhtml=render_xhtml_4_my_library_fail,
         ),
-        "inner": Element(
+        "inner": _ElementWithInnerProps(
             "value",
             {"value": ElementProperty(PropertyType.string, "")},
             inner_properties={

+ 64 - 0
tests/gui/extension/test_tgb.py

@@ -0,0 +1,64 @@
+# Copyright 2021-2025 Avaiga Private Limited
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations under the License.
+import typing as t
+
+from taipy.gui import Gui
+from taipy.gui.extension import Element, ElementLibrary, ElementProperty, PropertyType
+
+
+class TgbLibrary(ElementLibrary):
+    elements = {
+        "e1": Element(
+            "s1",
+            {
+                "b1": ElementProperty(PropertyType.boolean, doc_string="e1.b1 doc"),
+                "b2": ElementProperty(PropertyType.dynamic_boolean),
+                "s1": ElementProperty(PropertyType.string),
+                "s2": ElementProperty(PropertyType.dynamic_string),
+                "d1": ElementProperty(PropertyType.dict),
+                "d2": ElementProperty(PropertyType.dynamic_dict),
+            },
+            "E1", doc_string="e1 doc",
+        ),
+        "e2": Element(
+            "x",
+            {
+                "p1": ElementProperty(PropertyType.any),
+                "p2": ElementProperty(PropertyType.any),
+            },
+            "E2",
+        ),
+    }
+
+    def get_name(self) -> str:
+        return "test_ext_tgb"
+
+    def get_elements(self) -> t.Dict[str, Element]:
+        return TgbLibrary.elements
+
+
+def test_tgb_generation(gui: Gui, test_client, helpers):
+    from taipy.gui.extension.__main__ import generate_doc
+
+    library = TgbLibrary()
+    api = generate_doc(library)
+    assert "def e1(" in api, "Missing element e1"
+    assert "s1" in api, "Missing property s1"
+    assert "s1: str" in api, "Incorrect property type for s1"
+    assert "(s1: str, *" in api, "Property s1 should be the default property"
+    assert "b1: bool" in api, "Missing or incorrect property type for b1"
+    assert "b2: bool" in api, "Missing or incorrect property type for b2"
+    assert "s2: str" in api, "Missing or incorrect property type for s2"
+    assert "d1: dict" in api, "Missing or incorrect property type for d2"
+    assert "d2: dict" in api, "Missing or incorrect property type for d2"
+    assert "e1 doc" in api, "Missing doc for e1"
+    assert "def e2(" in api, "Missing element e2"
+    assert "e2(p1, p2)" in api, "Wrong default property in e2"