Working with forms

Basic usage

Usually almost each site require to interact with a user, so you need different types of forms for working with data. Tabbli supports a system for automatic form rendering, validation and submission. In most cases you can use method get_record_form(...) from DB interface. Please see Working with the database section for understand principles for working with data. The basic scenario for forms is like this:

  • Create a form object for the collection

  • Check the request method and try to validate the form if it is POST

  • If validation is passed then save a data. Do not forget to execute scenarios for a proper trigger with execute_scenario(...) method.

  • Render the form if you need it (for GET request or if validation has not been passed)

  • Make a redirect to another page after successful form submission

Look at this example:

{% from "BASE:macroses/forms.html" import form_fields with context %}
{% set form = DB(request)
    .get_collection('message')
    .get_record_form() %}

{% if request.method == 'POST' %}
    {% if form.is_valid() %}
        {% if form.is_valid() %}
            {% set record = form.save() %}
            {% do record.execute_scenario('added') %}
            {% do redirect_to('/') %}
        {% endif %}
    {% endif %}
{% endif %}

<form method="post">
    {{ form_fields(form) }}
    <button type="submit" class="btn btn-primary">Submit</button>
</form>

If you need to edit existing record you need to specify record attribute for get_record_form:

{% set job = DB().get_collection('jobs').get_record(key=key) %}
{% set form = DB(request)
    .get_collection('jobs')
    .get_record_form(record=job) %}

For cases when you use File properties do not forge to use enctype="multipart/form-data" attribute for the form tag:

<form method="post" enctype="multipart/form-data">
    {{ form_fields(form) }}
    <div class="form-group mt-4 pt-4">
        <button type="submit" class="btn btn-success">Submit</button>
    </div>
</form>

Modify submitted data before saving to the database

If a form returns only a part of required property values you need for successful saving a record to the database, you can use form.save(commit=False) for creating a record object but without saving to the database. Modify the record and then use its save() method for update the database.

{% set record = form.save(commit=False) %}
{% do record.properties.update({'user': request.user.email}) %}
{% do record.save() %}
{% do record.execute_scenario('added') %}

As you see you should use update(...) method for change the properties, because {% set ... %} tag allows to modify variables only inside the template context.

If you need to change some system properties, like name for example, you should use setattr function:

{% do setattr(record, 'name', {'en':'Lorem ipsum'}) %}

Hiding "Name" field

If you want to generate a value for Name field automatically or using computation options for it, you can use attribute ignore_name:

{% set form = DB(request)
    .get_collection('profile')
    .get_record_form(record=profile, ignore_name=True) %}

Show only specified fields

If you want to create a form with only specified fields (for example, if you want to fill the rest of properties automatically) you should specify property_keys attribute:

{% set form = DB(request)
    .get_collection('profile')
    .get_record_form(
        record=profile, ignore_name=True,
        property_keys=['bio', 'avatar', 'gender', 'website']) %}

Widgets overriding

Some types of properties (like relations) support alternate widgets. You can such code for change it:

{% set form = DB(request)
    .get_collection('profile')
    .get_record_form(
        record=profile, ignore_name=True,
        property_keys=['bio', 'avatar', 'gender', 'website'],
        override_widgets={'gender': 'radio_select'}) %}

The next alternate widgets are available:

  • For single item selection:

    • default - autocomplete widget;

    • select - just a regular select box;

    • radio_select - radio buttons.

  • For multiple items selection:

    • default - autocomplete widget;

    • select_multiple - regular selectbox with multiple selection support;

    • checkbox_select - a list of checkboxes.

Submitting forms via AJAX

In some cases it might sense to work with forms via AJAX, without reloading a whole page in the browser. Tabbli provides a simple JS framework for work with such kind of behavior which is called TabbliJS which is included to a standard site template. First of all, you need to add a container to any page where you need to show the form:

<div class="__action--ajax-load" 
    id="form-container" 
    data-src="/submit-contact/" 
    style="height:300px;">Loading...</div>

Here the class __actiom--ajax-load is using to tell the Tabbli to load the page with URL specified in the attribute data-src. After that you should create additional controller and a template which renders the form itself.

{% from "BASE:macroses/forms.html" import form_fields with context %}
{% set form = DB(request)
    .get_collection('contacts').get_record_form() %}
    
{% if request.method == "POST" %}
    {% if form.is_valid() %}
        {% set record = form.save() %}
        {% do record.execute_scenario('added') %}
        {% set form = None %}
    {% endif %}
{% endif %}

{% if form %}
<form class="__action--ajax-submit" method="post" 
        action="/submit-contact/" data-target="#form-container">
    {{ form_fields(form) }}
    <div class="form-group mt-4">
        <button type="submit" class="btn btn-success">Submit</button>
    </div>
</form>
{% else %}
    <div class="alert alert-success">
        Thank you for your interest!
    </div>
{% endif %}

In that code, class __action--ajax-submit means that after pressing Submit button the form data will be submitted via AJAX. When server will generate a result it will loaded into the container with id form-container (it is specified in attribute data-target). You can see that it is a container in our main page where we are loading the form into. If form validation is passed the result will contain an alert message about successful submission.

Changing standard forms look and feel

Sometimes you could be interested in overriding basic Tabbli templates for form rendering. If you use Bootstrap 4 as your CSS framework you rarely will want to do it. But, if you really need it you can override the next templates by adding your own to the directory snippets:

  • snippets/form_fields.html - renders all form widgets and block of global form errors;

  • snippets/form_field.html - render a single form widget.

If you do not override them, macros {{ form_fields(form) }} will use predefined templates. Look at the sources here:

snippets/form_fields.html
{% if form.errors.__all__ %}
    <ul class="list-unstyled">
        {% for error in form.errors.__all__ %}
            <li class="text-danger">
                <small><i class="fa fa-exclamation-circle fa-fw"></i> 
                    {{error}}
                </small>
            </li>
        {% endfor %}
    </ul>
{% endif %}
{% for field in form %}
    {% include [
        site.key+":snippets/form_field.html", 
        "BASE:snippets/form_field.html"] %}
{% endfor %}
snippets/form_field.html
{% set required_text %}<span class="text-danger">*</span>{% endset %}
{% if field.field.widget.input_type == "hidden" %}
    {{ field }}
{% else %}
<div class="{% if field.field.widget.__class__.__name__ == "CheckboxInput" %}form-check{% else %}form-group{% endif %}">
{% if field.field.widget.input_type == "checkbox" and 
        field.field.widget.allow_multiple_selected is not defined %}
    {{ field.as_widget(attrs={'class':'form-check-input'}) }}
    <label class="form-check-label" 
        for="{{ field.id_for_label }}">{{ field.label }}</label>
{% else %}
    <label for="{{ field.id_for_label }}">{{ field.label }}
        {% if field.field.required and 
            not hide_required_badges %}{{ required_text }}{% endif %}
    </label>
    {% if field.field.widget.__class__.__name__ in 
            ['SummernoteInplaceWidget', 'ClearableFileInput'] %}
        {{ field }}
    {% elif field.field.widget.__class__.__name__ == 
            "CheckboxSelectMultiple"  %}
        {% for item in field %}
            <div class="form-check">
                {{ item.tag() }}
                <label for="{{ item.id_for_label }}">
                    {{ item.choice_label }}
                </label>
            </div>
        {% endfor %}
    {% elif field.field.widget.input_type == "radio" %}
        {% for radio in field %}
            <div class="form-check">
                {{ radio.tag() }}
                <label for="{{ radio.id_for_label }}">
                    {{ radio.choice_label }}
                </label>
            </div>
        {% endfor %}
    {% else %}
        {{ field.as_widget(attrs={'class':'form-control'}) }}
    {% endif %}
{% endif %}
{% if field.help_text %}
<small class="form-text text-muted">{{field.help_text|safe}}</small>
{% endif %}
{% if field.errors %}
<ul class="list-unstyled">
    {% for error in field.errors %}
        <li class="text-danger"><small>
            <i class="fa fa-exclamation-circle fa-fw"></i> {{error}}
        </small></li>
    {% endfor %}
</ul>
{% endif %}
</div>
{% endif %}

So, if you need to change look and feel of your forms you can override these templates.

If you need to change the look of only a few forms and do not want to change it globally, you can create separate templates with other names (you can use generic templates as an example). Like snippets/my_custom_form.html and snippets/my_custom_form_field.html, for example. And than you can define your own macro for form rendering:

{% macro my_custom_form(form) %}
    {% include site.key+":snippets/my_custom_form.html" %}
{% endmacro %}

Now, you can use your new macro like generic one:

<form method="post">
    {{ my_custom_form(form) }}
    
    <button type="submit">Submit</button>
</form>

If you need to use the same macro in multiple templates you can put to the separate template and than import where you need it:

{% from site.key+":macroses/my_custom_forms.html" 
    import my_custom_form with context %}

Last updated