Skip to main content

A JSON based markup language which translates JSON files into Flet UI

Project description

FJML

FJML is a JSON based markup language which translates JSON files into Flet UI for web, mobile and desktop applications.

Example:

{
    "Header":{
        "program_name":"Example",
        "action_import":{
            "import":"Actions",
            "from":".path.to.func.file"
        },
    },
    "Controls":[
        {
            "var_name":"msg",
            "control_type":"Text",
            "settings":{
                "value":"Hello World",
                "size":20,
                "color":{"code_refs":"colors", "attr":"green"},
                "weight":{
                    "control_type":"FontWeight",
                    "attr":"W_700"
                }
            }
        },
        {
            "var_name":"msg_display",
            "control_type":"Container",
            "settings":{
                "content":{"refs":"msg"},
                "alignment":{"control_type":"alignment.center"},
                "border_radius":10,
                "padding":{
                    "control_type":"padding.symmetric",
                    "settings":{"horizontal":10, "vertical":8}
                },
                "width":200,
                "ink":true,
                "ink_color":{"code_refs":"colors", "attr":"grey"}
            }
        }
    ],
    "UI":[
        {
            "route":"/",
            "settings":{
                "controls":[
                    {"refs":"msg_display"}
                ],
                "horizontal_alignment":{"code_refs":"cross_align"},
                "vertical_alignment":{"code_refs":"main_align"}
            }
        }
    ]
}
from fjml import data_types as dt
import flet as ft


class Colors:
    green: str = ft.colors.GREEN_600
    grey: str = ft.colors.GREY_200


class Actions(dt.EventContainer):

    def _page_setup(self):
        ...
    
    def _imports(self):
        self.colors: Colors = Colors()
        self.cross_align: str = ft.CrossAxisAlignment.CENTER
        self.main_align: str = ft.MainAxisAlignment.CENTER
from fjml import load_program, Compiler, data_types as dt
from path.to.program import Actions
import flet as ft

class Paths:
    PROGRAM: str = "path\\to\\program_folder"
    COMPILED: str = "path\\to\\compiled_program\\compiled.fjml"

class App:

    def __init__(self, compile_run: bool = False) -> None:
        if compile_run:
            compiler: Compiler = Compiler(Paths.PROGRAM, Paths.COMPILED)
            compiler.compile()
        
    async def run(self, page: ft.Page):
        page = load_program(Paths.COMPILED, Actions)
        page.go("/")
    

if __name__ == "__main__":
    app: App = App(compile_run=True)
    ft.app(target=app.run)

CLI Tooling:

FJML comes with 2 CLI commands and each have specific parameters:

  • registry :

    The registry command is a categorical command which automatically modifies the registry file which contains of all Flet controls of your current Flet installation.

    Choice Value Type Action
    delete str deletes registry file
    reset str resets registry file

    Example:

    • fjml registry update

    The reset command should be used after installation to ensure proper functioning of fjml

  • make :

    The make command generates an FJML folder containing all the needed files for running your project. make has one mandatory parameter, --name, and one optional parameter, --path. --name generates the name of your project while --path directs where that project is generated. If path is not entered it will use the current directory.

    Example:

    • fjml make --name Hello World

Python Integration

FJML allows the use of python code to perform actions such as API calls, function calls, etc. via the EventContainer Abstract Base Class.

  • The main format of the Actions class which inherits from the EventContainer looks like this:
    from fjml.data_types import EventContainer
    
    class Actions(EventContainer):
    
        def _page_setup(self):
            '''
            a custom page setup function to initialize your page object with data
            '''
        
        def _imports(self):
            '''
            an import function used to run operations outside of the page before rendering the UI
            '''
        
        #you can then add custom functions to be used throughout the FJML code
    

EventContainer also includes multiple built-in helper classes and functions to help create programs

  • EventContainer methods and classes



    • update:

      equivalent to ft.Page.update



    • dict_to_control:

      Name Attributes Return Description
      dict_to_control control: dt.ControlDict dt.ControlType Allows creating controls using FJML syntax inside the EventContainer.
      • Example Usage:

        class Actions(dt.EventContainer):
        
            def make_control(self) -> ft.Control:
                return self.dict_to_control({
                    "control_type":"Container",
                    "settings":{
                        "alignment":{"control_type":"alignment.center"},
                        "content":{
                            "control_type":"Text",
                            "settings":{
                                "value":"Hello World",
                                "size":18
                            }
                        }
                    }
                })
        

    • group_assign:

      Name Attributes Return Description
      group_assign obj: Any', 'attribute_map: Mapping[str, Any] None allows assigning multiple attributes to an object at once
      • Example Usage:

        class Data:
            f_name: str
            l_name: str
            age: int
        
        class Actions(dt.EventContainer):
        
            def fill_data(self) -> Data:
                data: Data = Data()
                self.group_assign(
                    data,
                    {
                        "f_name":"John",
                        "l_name":"Doe",
                        "age":21
                    }
                )
                return data
        

    • eval_locals:

      This class's main use is to add or delete locals from the evil statement's locals parameter.

      Methods Attributes Return Description
      add name: str, obj: Any None adds an object to the eval statement's locals
      delete name: str None deletes an object from the eval statement's locals
      mass_add data: Mapping[str, Any] None adds multiple objects to the eval statement's locals
      mass_delete data: Sequence[str] None deletes multiple objects from the eval statement's locals
      data None Mapping[str, Any] returns a copy of all preset locals in the eval statement's locals

    • object_bucket:

      Methods Attributes Return Description
      set_object name: str, obj: AnyCallable None adds any callable object to the bucket, so it can be called inside the UI code
      call_object name: str, kwargs: dict[str, Any] Any calls the object with the necessary key word arguments. (used when object is called within the UI code)
      delete_object name: str None deletes the object from the bucket

      This class's main use is to register objects for use in FJML code via the "call" designator:

      • {
            "call":"get_text",
            "settings":{
                "index":1
            }
        }
        

      If an object is not registered it can not be called but can only be referenced using a code_refs or the func designators.


    • property_bucket:

      Methods Attributes Return Description
      add name: str, obj: Any None adds a property to be used as a code_refs inside the UI code
      contains name: str bool used to check if a name is registered as property
      call name: str, operation: str, set_val: Any None Uses the property operations (set, get, del) to either set an object using the set_val parameter, get by just giving the name or deletion using the del operation.

      This class's main use is to register python functions as properties to be used as code_refs


    • setup_functions:

      This class's main use is to register functions to be called to set up what ever API, environment, etc. when the UI starts up.

      Methods Attributes Return Description
      add_func func: Callable, parameters: Sequence[Any] None adds a function to the class
      mass_add_func items: Sequence[tuple[Callable, Sequence[Any]]] None adds multiple functions to the class
      call_functions None None calls all functions added to the class

    • style_sheet:

      this class is used primarily for retrieving styles set inside the style sheet. This is mainly used in the "_unpack" designator via the {"styling":"xyz"} mapping value.

      Methods Attributes Return Description
      get_style path: str dt.JsonDict gets the style from a style sheet by name

    • view_operations:

      This class is used to generate and register Flet views. Its main use is in the Flet's Page.on_route_change event.

      Methods Attributes Return Description
      set_view route_name: str, view_settings: dt.ControlSettings None adds a UIViews to the compiled_model UI mapping attribute
      add_view view: ft.View None adds a Flet view control to the page views
      make_view view_model: UIViews ft.View generates a Flet view control from a UIViews type

UI Format

Main UI File:

{
    "Header":{
        //Used to declare certain values
    },
    "Imports":[
        // Used to import controls from other files
    ],
    "Controls":[
        // Used to assign controls to variables
    ],
    "UI":[
        // Used to define route views
    ]
}

This format separates the header data, imports, controls and display UI.

  • Header:

    Headers is primarily map based and thus requires one to use keys and values unlike the list based forms like the rest are. The keys in this block consists of:

    • import_folder:

      Value Type Example
      str "extra"

      stores the name of the folder which contains the FJML imports

    • program_name:

      Value Type Example
      str "Hello World"

      stores the name of the program

    • style_sheet_name:

      Value Type Example
      str "style_sheet"

      Stores the name of the FJML style sheet.

    • action_import:

      Value Type Example
      dt.JsonDict {"from":"...", "import":"..."}

      This dictionary imports the action class using the format:

      Key Value Type Example
      from str ".import_path.func"
      import str "Action"

      full example:

      {
          "action_import":{
              "from":".import_path.func",
              "import":"Action"
          }
      }
      

      The action_import key's value is equivalent to:

      from .import_path.func import Action
      

      The import statement is run as if it was run in your main file.

    • extensions:

      Value Type Example
      Sequence[dt.JsonDict] [{"from":"...", "import":"...", "using":"..."}, ...]

      The value of this key consists of using a sequence of dictionaries which help import multiple controls at once:

      Key Value Type Example
      from str .custom_controls
      import Union[str, Sequence[str]] "CustomBtn" or ["CustomBtn", "CustomTxt"]
      using Optional[str] "CC"

      full example:

      {
          "extensions":[
              {
                  "from":".custom_controls",
                  "import":["CustomBtn", "CustomTxt"],
                  "using":"CC"
              }
          ]
      }
      

      Using extension imports like this is equivalent to:

      • With the using key:

            from . import custom_controls as CC
            from CC import CustomBtn, CustomTxt
        

        The use of these controls in FJML code will now have to use controls like so: CC.CustomBtn

      • Without the using key:

            from .custom_controls import CustomBtn, CustomTxt
        
  • Imports:

    Imports are called using the file name of the UI file defined inside the import folder defined by the key "source". If the import folder includes different folders the use of the key "folder" can be used to indicate the specific folder inside the main import folder where you want to import from. e.g: - {"source:"container_ui"} - {source:["user_ui", "admin_ui"], "folder":"person_ui_folder"}

  • Controls:

    controls can be named using the "var_name" key and then can be called and used in the python "Action" class using "self.name_text" or in another control using a dictionary using the "refs" key and the control name as its value. e.g: {"refs":"name_text"} Format example:

    {
        "var_name":"foo",
        "control_type":"imported/registered control name",
        "settings":{
            ...
        }
    }
    
  • UI:

    The UI section is used to define the route views used by you program. These views use the format of:

    {
        "route":"/Home",
        "settings":{
            //any view settings needed. P.S. the route parameter will always be ignored if set in the settings block.
        }
    }
    

    and are contained in a Sequence

Imported UI File:

With imports the JSON structure is similar except that it only has the "Controls" container. Using controls from other files is still possible once all dependencies are also imported into the main file.

Imported file format:

{
    "Controls":[]
}

Style Sheet File:

With style sheets you are able to create styles for use later in your program. You are able to section off your styles by names and sub-names and thus call them using the format {name}.{sub_name}.{sub_sub_name}.... This format can go on forever if needed but can increase rendering time of your program if you go too deep.

Style sheet Format:

{
    "name_1":{
        "sub_name":{
            "height":200,
            "width":500
        }
    },
    "name_2":{
        "sub_name":{
            "height":200,
            "width":500
        }
    }
}

styles can be used by then adding the "_unpack" attribute inside the control's "settings" dictionary with the dictionary formats:

  • {"styles":"{name}.{sub-name}"}
  • or {"styles":"{name}.{sub-name} {name1}.{sub_name}"} if multiple styles are needed

Other FJML UI features include

  • Calling functions and objects:

    class Action(EventContainer):
    
        def _imports(self) -> None:
            #register callable object
            self.object_bucket.set_object("calc_width", self.calc_width)
        
        def calc_width(self, height: int) -> int:
            return height*2
    
    // call object
    {
        "control_type":"Container",
        "settings":{
            "width":{
                "call":"calc_width",
                "settings":{
                    "height":200
                }
            }
        }
    }
    
  • UI loops:

    {
        "control_type":"Column",
        "settings":{
            "controls":{
                "control_type":"loop",
                "depth":1,
                "iterator":[1,2,3,4,5],
                "control":{
                    "control_type":"Text",
                    "settings":{
                        "value":{
                            "control_type":"loop_index", 
                            "idx":[0]
                        }
                    }
                }
            }
        }
    }
    
  • Adding control to variables:

    {
        "var_name":"name", //<- Here
        "control_type":"Text",
        "settings":{
            "value":"John Doe",
            "size":18
        }
    }
    

    this can be accessed inside the Actions class using self.name and if control is defined as self.name inside Actions it can be called in FJML using {"refs":"name"}

  • Using variables:

    Allows the use of FJML variables to be referenced in the same file or else where without the need for constant importing

    control variables:

    {
        "var_name":"name", //<- Here
        "control_type":"Text",
        "settings":{
            "value":"John Doe",
            "size":18
        }
    }
    {
        "var_name":"text_container",
        "control_type":"Container",
        "settings":{
            "content":{"refs":"name"},
            "padding":6
        }
    }
    

    code variables:

    Allows the variables defined in python code to be accessed and used inside FJML code

    class Actions(EventContainer):
    
        def _imports(self) -> None:
            self.text_size: int = 16
    
    {
        "var_name":"name", //<- Here
        "control_type":"Text",
        "settings":{
            "value":"John Doe",
            "size":{"code_refs":"text_size"}
        }
    }
    

    Attribute and index calling

    The idx key works for both dictionaries and index based sequences.

    {
        "var_name":"name", 
        "control_type":"Text",
        "settings":{
            "value":"John Doe",
            "size":18
        }
    }
    {
        "var_name":"get_display_name",
        "control_type":"Text",
        "settings":{
            "value":{
                "refs":"name",
                "attr":"value"
            },
            "size":18
        }
    }
    
    • Group chains:

      class TextSizes:
          data: list[Union[dict[str, int], int]] = [18, {"name":16}]
      
      
      class Actions(EventContainer):
          def _imports(self) -> None:
              self.text_sizes: TextSizes = TextSizes()
      
      {
          "var_name":"name",
          "control_type":"Text",
          "settings":{
              "value":"John Doe",
              "size":{
                  "code_refs":"text_sizes",
                  "group":[
                      {"attr":"data"},
                      {"idx":1},
                      {"idx":"name"}
                  ]
              }
          }
      }
      
  • Action Class:

    In order to link your action class to FJML code you must import the in the Header container using the key, action_import.

    • Example:

      {
          "Header":{
              ...,
              "action_import":{
                  "import":"Actions",
                  "from":".ui_test_program.func"
              }
          },
          "Imports":[...],
          "Controls":[...],
          "UI":[...]
      }
      

    All action imports must exist in an importable path and be written as if it was run in the main.py file.

  • Custom Controls:

    FJML allows you multiple ways to define and add custom controls to your project. This is done by using the "extensions" key inside the "Header":

    {
        "Header":{
            ...,
            "extensions":[
                {
                    "using":"fm",
                    "import":["Buttons", "Switches"],
                    "from":"flet_material"
                }
            ]
        },
        "Imports":[...],
        "Controls":[
            {
                "var_name":"switch",
                "control_type":"fm.Switches",
                "settings":{}
            }
        ],
        "UI":[...]
    }
    

    All imports must already be installed or exist in an importable path.


Running the app

from fjml import load_program, Compiler, data_types as dt
import flet as ft

class Paths:
    PROGRAM: str = "path\\to\\program_folder"
    COMPILED: str = "path\\to\\compiled_program\\compiled.fjml"

class App:

    def __init__(self, compile_run: bool = False) -> None:
        if not compile_run:
            return
                
        compiler: Compiler = Compiler(Paths.PROGRAM, Paths.COMPILED)
        compiler.compile()
        
    async def run(self, page: ft.Page):
        page = load_program(Paths.COMPILED, page)
        page.go("/")

if __name__ == "__main__":
    app: App = App(compile_run=True)
    ft.app(target=app.run)

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

fjml-0.0.4.tar.gz (43.5 kB view hashes)

Uploaded Source

Built Distribution

fjml-0.0.4-py3-none-any.whl (44.1 kB view hashes)

Uploaded Python 3

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page