Extension

The extension component can be used to write your own component using HTML and JavaScript.

Properties

The following properties can be specified in the form definition.

NameDescriptionDatatypeDefault
contentHTML content for the extension.string"<p>Hello, world!</p>"
initializeFnInitialize function, called once when the component is initialized.stringNone/""
updateFnUpdate function, called when the value of the component is changed from outside.stringNone/""
eventName of an event that can be emitted similar to a Button.stringNone/""

Initialize and update functions

The initialize and update functions are called with one input argument, the extension component, that has the following properties:

NameDescriptionDatatypeDefault
formioEventEvent emitter for custom eventsEventEmitter
initializeFnThe name of the initialize function provided in the form.string""
internalDictionary to store the internal state of the component.Dictionary{}
keyThe component key.string"<key>"
updateFnThe name of the update function provided in the form.string""
valueThe submission data of the component.Dictionary<string, any>{}1
valueChangeEvent emitter to notify other components the value has changed.EventEmitter<Dictionary>
1

Can be changed by setting defaultValue in the form definition.

Value change

Form.io compares values by equality when detecting changes. When changing an object's fields or an array's elements, the change may not be detected. For arrays this may be solved by slicing the array array.slice(), for objects all fields can be copied to a new object { ...object }. For complex nested values, a trick might be to serialize and deserialize JSON JSON.parse(JSON.stringify(value)).

Emitting events

When the event property is set in the form definition, the following code can be used to emit the event and call gui_event in the back-end.

component.formioEvent.emit({ "eventName": "customEvent" });

Example

In this example we create a custom number component, that has buttons to increase and decrease the value and keeps tracks of the minimum and maximum value. The example app also contains a text area that displays the current value of the custom component using calculateValue, and a button that gets and sets the submission data in the back-end.

Example app

def guiInit(_meta_data):
    # Create the form and load the json builder into it.
    form = Form()

    ## Custom extension component.
    myNumber = component.Extension("myNumber", form)

    # HTML definition of the custom component.
    myNumber.content = """<div class="number">
    <button class="number-min"><i class="fa fa-minus"></i></button>
    <input class="number-input" type="number" disabled />
    <button class="number-plus"><i class="fa fa-plus"></i></button>
    </div>"""

    # Setup the initialize and update functions that
    # will determine the component's behavior.
    myNumber.initializeFn = "initializeNumber"
    myNumber.updateFn = "updateNumber"

    # Generic properties.
    myNumber.label = "Number"
    myNumber.defaultValue = {"currentValue": 0}

    ## Display
    display = component.TextArea("display", form)
    display.setCalculateValue("value = JSON.stringify(data.myNumber)")
    display.disabled = True

    ## Button
    myButton = component.Button("myButton", form)
    myButton.label = "Reset"
    myButton.action = "event"
    myButton.event = "Reset"

    return {"form": form}


def guiEvent(meta_data, payload):
    Form.eventHandler(Reset=resetValue)
    callback = utils.getEventFunction(meta_data, payload)
    payload = callback(meta_data, payload)

    return payload


def resetValue(meta_data, payload):
    value, _ = utils.getSubmissionData(payload, "myNumber")
    print(value)

    utils.setSubmissionData(
        payload,
        "myNumber",
        {"minValue": 0, "currentValue": 0, "maxValue": 0},
    )

    return payload
function payload = guiInit(~)
    % Create the form and load the json builder into it.
    payload.form = Form();

    %% Custom extension component.
    myNumber = component.Extension("myNumber", payload.form);

    % HTML definition of the custom component.
    myNumber.content = "<div class=""number"">" ...
        + "<button class=""number-min""><i class=""fa fa-minus""></i></button>" ...
        + "<input class=""number-input"" type=""number"" disabled />" ...
        + "<button class=""number-plus""><i class=""fa fa-plus""></i></button>" ...
        + "</div>";

    % Setup the initialize and update functions that
    % will determine the component's behavior.
    myNumber.initializeFn   = "initializeNumber";
    myNumber.updateFn       = "updateNumber";

    % Generic properties.
    myNumber.label          = "Number";
    myNumber.defaultValue   = struct('currentValue', 0);

    %% Display
    display = component.TextArea("display", payload.form);
    display.setCalculateValue("value = JSON.stringify(data.myNumber)");
    display.disabled = true;

    %% Button
    myButton        = component.Button("myButton", payload.form);
    myButton.label  = "Reset";
    myButton.action = "event";
    myButton.event  = "Reset";
end

function payload = guiEvent(meta_data, payload)
    Form.eventHandler("Reset", @resetValue);
    callback = utils.getEventFunction(meta_data, payload);
    payload = callback(meta_data, payload);
end

function payload = resetValue(~, payload)
    value = utils.getSubmissionData(payload, "myNumber");
    disp(value);

    value.minValue = 0;
    value.currentValue = 0;
    value.maxValue = 0;

    payload = utils.setSubmissionData(payload, "myNumber", value);
end
// Initialize function for the custom number component.
function initializeNumber(component) {
    // Get the container element.
    let container = component.container;

    // Get the sub-components by class.
    let minMin = container.getElementsByClassName("number-min")[0];
    let input = container.getElementsByClassName("number-input")[0];
    let plusPlus = container.getElementsByClassName("number-plus")[0];

    // Decrease the value when clicking the button.
    minMin.onclick = (_) => changeValue(component, Number(input.value) - 1);

    // Increase the value when clicking the button.
    plusPlus.onclick = (_) => changeValue(component, Number(input.value) + 1);

    // Store the input element for later use.
    component.internal.input = input;
}

// Propagate the changed value to the submission data
// and emit an event to other components.
function changeValue(component, newValue) {
    let input = component.internal.input;

    // Change the value of the input element (in the user interface).
    input.value = newValue;

    // Value update works by testing equality,
    // hence we need to create a new object to trigger a change.
    value = { ...component.value };

    // Change the value of the component (and thereby the submission data).
    value.currentValue = newValue;
    value.minValue = Math.min(newValue, value.minValue ?? Infinity);
    value.maxValue = Math.max(newValue, value.maxValue ?? -Infinity);
    component.value = value;

    // Emit the value change for validation, calculateValue, etc.
    component.valueChange.emit(value);
}

// Handle updated values. Called when the value changes in the submission data.
function updateNumber(component) {
    // Get the container element and the input element.
    let input = component.internal.input;

    // Set the value in the user-interface.
    input.value = component.value.currentValue;

    component.value.minValue = Math.min(
        input.value, 
        component.value.minValue ?? Infinity
    );

    component.value.maxValue = Math.max(
        input.value,
        component.value.maxValue ?? -Infinity
    );
}
.number {
    height: 2rem;
    display: inline-block;
    overflow: hidden;
    border: 1px solid var(--gray);
    border-radius: 0.25rem;
}

.number-min {
    width: 2rem;
    height: 2rem;
    color: white;
    outline: none;
    border: 0;
    border-right: 1px solid var(--gray);
    background: var(--blue);
}

.number-input {
    padding: 0 0.5rem;
    outline: none;
    border: 0;
    background: white;
}

.number-plus {
    width: 2rem;
    height: 2rem;
    color: white;
    outline: none;
    border: 0;
    border-left: 1px solid var(--gray);
    background: var(--red);
}