How to open and close Unpoly layer

This post presents a minimal example of how to open and close an Unpoly layer in a Flask application.

Unpoly is a lightweight JavaScript library designed to enhance server-rendered web applications. For example, it allows updating only fragments of a page in response to form submissions. This makes interactions faster and preserves client-side state in unaffected parts of the page.

Unpoly attributes for opening modal layer

Unpoly uses HTML attributes that enable additional behavior on HTML elements.

In index.html:

<a href="{{ url_for('new') }}" class="button"
   up-layer="new modal" up-size="medium" up-history="false"
   up-on-accepted="up.navigate({ url: '{{ url_for('home') }}' })">
New
</a>

up-layer="new modal" follows the link and opens the resulting page in a new modal overlay.

up-on-accepted sets JavaScript snippet that is called, when the overlay was accepted. Here's the up.navigate() function, which navigates to a URL built by Flask's url_for() function.

up-history="false" sets that fragments' changes within the overlay will never update the address bar.

up-size sets the size of overlay.

href="{{ url_for('new') }}" — Flask uses Jinja2 to render HTML files, so commands in curly braces are Python functions.

Flask application


from flask import Flask, make_response, render_template, request

app = Flask(__name__)

@app.route('/')
def home():
    return render_template('index.html')

@app.route('/new', methods=['GET', 'POST'])
def new():
    template_name = 'layer_modal.html'
    context = {
        'error': None,
        'field_1': None,
    }

    if request.method == 'POST':
        context['field_1'] = request.form.get('field_1')

        if not context['field_1']:
            context['error'] = 'Empty field'
            return render_template(
                template_name, **context
            ), 422

        response = make_response()
        response.headers['X-Up-Accept-Layer'] = 'true'
        return response

    # GET request
    return render_template(template_name, **context)

If field_1 is empty, the server returns the layer_modal.html template with error and a 422 status code. As specified by the up-fail-target attribute, Unpoly will then update the form element using this response.

The example then shows the server response Unpoly requires to accept and close the layer. Upon acceptance, the browser will navigate to url_for('home'), as directed by the up-on-accepted attribute.

Submitting form when a field changes

Now, the more complex task. We need to open a modal layer with a form that has two fields. The first, field_1, must be filled out. The second, another_field, is optional. The form should automatically submit data whenever the another_field value changes. Unpoly has up-autosubmit attribute to achieve this. For example, this could show a user a preview of an image they have just uploaded, even before they hit the submit button.

Also, if the user clicks the submit button, the server should first check if field_1 has been filled out. If it has, then the form is sent.

So, the server needs to detect whether the form was submitted automatically (due to a change in another_field) or by the user clicking the submit button.

We need to know this because field_1 only needs to be checked when the user clicks submit, not during automatic submission.

Few lines of JavaScript

The trick here is to pass a parameter to the form before it was submitted. This can be done using Unpoly up.on() function that will be called on up:form:submit event. We do check that event.target.name is another_field and set the parameter to pass. (If user hit the submit button, then event.target is this submit button.)

up.on('up:form:submit', function(event) {
    if (event.target.name === 'another_field') {
    event.params.set('autosubmit', '1');
    }
})

Where should this JavaScript code be placed in your project? I prefer separate project.js along with other static files. It is important to load project.js after unpoly.js.

Flask route for automatically submitted form


@app.route('/new-with-autosubmit', methods=['GET', 'POST'])
def new_with_autosubmit():
    template_name = 'layer_autosubmit.html'
    context = {
        'error': None,
        'field_1': None,
        'another_field': None,
        'preview': None,
    }

    if request.method == 'POST':
        context['field_1'] = request.form.get('field_1')
        context['another_field'] = request.form.get(
            'another_field'
        )
        autosubmit = request.form.get('autosubmit') == '1'

        context['preview'] = context['another_field']

        if autosubmit:
            return render_template(template_name, **context)

        if not context['field_1']:
            context['error'] = 'Empty field'
            return render_template(
                template_name, **context
            ), 422

        response = make_response()
        response.headers['X-Up-Accept-Layer'] = 'true'
        return response

    # GET request
    return render_template(template_name, **context)

autosubmit = request.form.get('autosubmit') == '1'

If autosubmit is 1, the server returns the layer_autosubmit.html template with preview.