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.
Modal layer
layer_modal.html
<main up-main="modal"> <form method="post" action="{{ url_for('new') }}" up-submit="" up-fail-target="form"> <div> <label for="field_1"> Field 1 {% if error +%} <b>{{ error }}</b> {% endif %} </label> <input id="field_1" name="field_1" /> </div> <div> <button class="button" type="submit">Add</button> </div> </form> </main>
action={{ url_for('new') }} is an endpoint build by Flask's url_for() function.
up-submit="" submits the form via JavaScript and updates a fragment with the server response.
up-fail-target="form" sets the form element to update after a failed response.
{% if error %} is a Jinja2 conditional expression that uses variable error passed to the template by Flask's route function.
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.
Modal layer with automatically submitted form
layer_autosubmit.html
The form element attributtes are exactly the same as in the first example. Note the use of the up-autosubmit attribute on the input element.
<main up-main="modal"> <form method="post" up-submit="" up-fail-target="form" novalidate="" action="{{ url_for('new_with_autosubmit') }}"> <fieldset> <div> <label for="field_1"> Field 1 {% if error +%} <b>{{ error }}</b> {% endif %} </label> <input id="field_1" name="field_1" autofocus {% if field_1 +%} value="{{ field_1|e }}" {% endif %} /> </div> <div> <label for="another_field">Another field</label> <input id="another_field" name="another_field" up-autosubmit up-watch-delay="1000" {% if another_field +%} value="{{ another_field|e }}" {% endif %}/> </div> {% if preview +%} <div>{{ preview }}</div> {% endif %} <div> <button class="button" type="submit">Add</button> </div> </fieldset> </form> </main>
up-watch-delay="1000" sets the number of milliseconds to wait after a change.
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.