Form structure

The Hello world! example of the previous chapter follows the general structure of the form initialization code:

  • Create an empty form: form = simian.gui.Form()
  • Add components to it from top to bottom:
    • Create the component with default property values.
    • Change property values of the component where necessary.
  • Return a dict/struct with a form field that contains the form.
    • Optionally specify a navbar field to set the logo and title of the application.

There are three ways of adding components to the form:

Adding components from constructor

Components are created by calling the Component constructor:

comp = component.<name>(<key>, <parent>)

In this call:

  • name is the name of any class in component that is not the Component class. The Component is an abstract superclass of all implemented components, so it cannot be used directly. For a full list of available components, see Components and subsections.
  • key is a unique string by which the component can be recognized. In MATLAB it must be a valid structure field name, which can be checked using isvarname(<key>) (Python does not have this restriction). In order to prevent unexpected behavior, it is advised to use globally unique component keys within the form.
  • parent is an optional parent to which the new component must be added. You can add the new component directly to the form just like in the Hello world! example. Alternatively, you could add the component to a parent component such as a panel or table. More information on nesting of components can be found in Component nesting.

You can choose to import the component package to prevent having to use it for the creation of every single component. The syntax then becomes:

from simian.gui.component import *
comp = <name>(<key>, <parent>)
import simian.gui_v3_0_1.component.*
comp = <name>(<key>, <parent>);

There are over 35 different components that follow the same pattern. Each of these is described in the Components section and subsections.

Adding components from table

When each component is constructed individually, the code can quickly become very lengthy. In order to keep the overview, components can be specified in a Pandas DataFrame or list of lists (in Python) or MATLAB table by using the utils.addComponentsFromTable function. It takes two input arguments:

  • parent: a Form or Component that can have subcomponents (i.e. it has a components property),
  • table: a Pandas DataFrame, list of lists, or MATLAB table.

It returns the created components in a dict/struct.

The table or DataFrame may have the following columns :

  • key: Component key (mandatory). Must be a valid variable name and unique per level.
  • type/class: Component type or class for creating the component (mandatory).
  • level: Nesting level. Top level components (relative to parent) have level 1. Nested components, e.g. in a Panel, increment with 1. If the column is not present, all levels will be set to 1.
  • options: Dictionary/struct with options, may contain any property that can be set to the component. Use None/missing to leave unspecified. If the column is not present, all options will be set to None/missing. Keys/fields 'defaultValue', 'label', 'tooltip', 'type', and 'key' are ignored, as these are columns of the table.
  • defaultValue: The default value for the component, must be of a valid data type for the component type. Use None/missing to leave unspecified. If the column is not present, all default values will be set to None/missing.
  • label: Label for the component. Use None/missing to leave unspecified. If the column is not present, all labels will be set to None/missing.
  • tooltip: Tooltip for the component. Use None/missing to leave unspecified. If the column is not present, all tooltips will be set to None/missing.

In case of a Python list of lists input these columns are assumed to be the fields of the inner lists, unless specified differently in the column_names input.

Although in general only components can be added to the table, there are some other type/class values that can be added:

  • column for adding the actual columns to a Columns component. In the options, a width field with an integer value must be specified. The total of the column widths must add up to 12.
  • tab for adding the actual tabs to a Tabs component. The label will be shown on the tab.
  • tablerow for adding rows to a Table component. Can only be added to a Table.
  • tablecell for adding a TableCell to a tablerow. The TableCell can contain other components.

Validation, conditionals and logic can be added to the returned components as usual (see here), or they can be added via the options dict/struct that is put in the table.

The dict/struct that is returned uses the keys specified in the table definition as keys/fields. If keys in the table are not unique, then only the last component with the non-unique key will be in the output.

Example

This example shows a simple form built using a DataFrame/table. It consists of a panel that is added to the form and four components that are added to the panel, using the level column. The options dict/struct is used for multiple components, further reducing the amount of code required for building the form.

Form

def gui_init(_meta_data: dict) -> dict:
    form = Form()

    component_dict = _create_layout(form)

    _fill_column(component_dict["left"], "From")
    _fill_column(component_dict["right"], "To")

    return {"form": form}


def _create_layout(form: Form) -> dict:
    column_options = {"width": 6}

    component_specs = [
        #   key             class       level   options
        [   "locations",    "Columns",  1,      None            ],
        [   "left",         "Column",   2,      column_options  ],
        [   "right",        "Column",   2,      column_options  ],
    ]

    return utils.addComponentsFromTable(form, component_specs, ["key", "class", "level", "options"])


def _fill_column(column: component.Columns, label: str) -> None:
    key = label.lower()

    shared_options = {"labelPosition": "left-left"}

    component_specs = [
        #   key             class           level   options             defaultValue    label
        [   key + "Panel",  "Panel",        1,      None,               None,           label       ],
        [   key,            "Container",    2,      None,               None,           None        ],
        [   "name",         "TextField",    3,      shared_options,     "Breda",        "Name"      ],
        [   "latitude",     "Number",       3,      shared_options,     51.5883621,     "Latitude"  ],
        [   "longitude",    "Number",       3,      shared_options,     4.7760251,      "Longitude" ],
    ]

    column_names = ["key", "class", "level", "options", "defaultValue", "label"],

    utils.addComponentsFromTable(column, component_specs, column_names)
function payload = guiInit(metaData)
    form = Form();

    componentStruct = createLayout(form);

    fillColumn(componentStruct.left, "From");
    fillColumn(componentStruct.right, "To");

    payload.form = form;
end

function componentStruct = createLayout(form)
    columnOptions.width = 6;

    componentSpecs = {
        % key           class       level   options
        "locations",    "Columns",  1,      missing
        "left",         "Column",   2,      columnOptions
        "right",        "Column",   2,      columnOptions
        };

    componentTable = cell2table(componentSpecs, 'VariableNames', ["key", "class", "level", "options"]);

    componentStruct = utils.addComponentsFromTable(form, componentTable);
end

function fillColumn(column, label)
    key = lower(label);

    sharedOptions.labelPosition = "left-left";

    componentSpecs = {
        % key           class           level   options             defaultValue    label
        key + "Panel",  "Panel",        1,      missing,            missing,        label
        key,            "Container",    2,      missing,            missing,        missing
        "name",         "TextField",    3,      sharedOptions,      "Breda",        "Name"
        "latitude",     "Number",       3,      sharedOptions,      51.5883621,     "Latitude"
        "longitude",    "Number",       3,      sharedOptions,      4.7760251,      "Longitude"
        };

    componentTable = cell2table(componentSpecs, 'VariableNames', ["key", "class", "level", "options", "defaultValue", "label"]);

    utils.addComponentsFromTable(column, componentTable);
end

Adding components from JSON

With the introduction of the Simian Form Builder, it is possible to generate components from a JSON form definition file by providing a named argument to the Form constructor.

form = Form(from_file="/path/to/my/form.json")
form = Form(from_file=__file__)  # The extension is replaced with ".json" for convenience.
form = Form(FromFile="/path/to/my/form.json");
form = Form(FromFile=mfilename("fullpath")); % The ".json" extension is added for convenience.

Additionally components that are able to hold other components, can be populated using the addFromJson method.

comp = component.Container("myContainer")
comp.addFromJson("/path/to/my/form.json")
comp = component.Container("myContainer");
comp.addFromJson("/path/to/my/form.json");

Initialization code

When components are generated using a JSON form, the component objects are added to the form tree. To modify a component programmatically without searching for it in the form, register a component initializer function.

This can be done by calling the static method Form.componentInitializer before building the form. The input consists of named arguments, where the keyword/name must match the key of the component and the value is an initialization function handle. The initialization function will be called with one input argument: the component object. Only one function can be registered per component.

Note that the initialization function must be registered before the component is created. Otherwise, the function is not applied to the component. As a result, you cannot register an initialization function from another initialization function.

def gui_init(meta_data: dict) -> dict:
    Form.componentInitializer(name=initialize_name)
    form = Form(from_file=__file__)
    return {"form": form}

def initialize_name(comp: component.TextField):
    comp.defaultValue = os.getlogin()
function payload = guiInit(metaData)
    Form.componentInitializer('name', @initializeName);
    form = Form(FromFile='form.json');
    payload = struct("form", form);
end

function initializeName(comp)
    comp.defaultValue = getenv('USERNAME');
end

Note that you can configure the behaviour of registered initializer functions by adding inputs to a function that wraps the actual function. In the example below a standard set_default function is used to set the default value of multiple components.

def gui_init(meta_data: dict) -> dict:
    Form.componentInitializer(
        name=set_default(os.getlogin()),
        folder=set_default(os.getcwd()),
    )
    form = Form(from_file=__file__)
    return {"form": form}

def set_default(value) -> Callable:
    def inner(comp: component.TextField):
        comp.defaultValue = value
    return inner
function payload = guiInit(metaData)
    Form.componentInitializer( ...
        'name', set_default(getenv('USERNAME')), ...
        'folder', set_default(cd()) ...
        );
    form = Form(FromFile='form.json');
    payload = struct("form", form);
end

function func = set_default(value)
    func = @(comp) inner(comp, value);
end

function payload = inner(comp, value)
    comp.defaultValue = value;
end

In a wrapped initialization function it is possible to register other initialization functions. In the example below the initialization function for the folder TextField is added in the function wrapping the name TextField initializer.

A construction like this is useful when your web app is created from a JSON form definition and incorporates another JSON form definition with initializer functions that must be executed.

def gui_init(meta_data: dict) -> dict:
    Form.componentInitializer(name=initialize_name())  # Function is executed!
    form = Form(from_file=__file__)
    return {"form": form}

def initialize_name() -> Callable:
    # Add an initialize function for the 'folder' component.
    Form.componentInitializer(folder=initialize_folder)
    def inner(comp: component.TextField):
        comp.defaultValue = os.getlogin()
    return inner

def initialize_folder(comp: component.TextField):
    comp.defaultValue = os.getcwd()
function payload = guiInit(metaData)
    Form.componentInitializer('name', initializeName());  % Function is executed!
    form = Form(FromFile='form.json');
    payload = struct("form", form);
end

function func = initializeName()
    % Add an initialize function for the 'folder' component.
    Form.componentInitializer(folder=@initialize_folder)
    func = @(comp) inner(comp);
end

function inner(comp)
    comp.defaultValue = getenv('USERNAME');
end

function initialize_folder(comp)
    comp.defaultValue = cd();
end