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 aform
field that contains the form.- Optionally specify a
navbar
field to set thelogo
andtitle
of the application.
- Optionally specify a
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 incomponent
that is not theComponent
class. TheComponent
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 usingisvarname(<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
: aForm
orComponent
that can have subcomponents (i.e. it has acomponents
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 toNone
/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 toNone
/missing
. - label: Label for the component. Use
None
/missing
to leave unspecified. If the column is not present, all labels will be set toNone
/missing
. - tooltip: Tooltip for the component. Use
None
/missing
to leave unspecified. If the column is not present, all tooltips will be set toNone
/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, awidth
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 atablerow
. TheTableCell
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.
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