How to open and close Unpoly layer
- Why Unpoly?
- Prerequisities
- Installing Unpoly
- Opening a modal layer with Unpoly
- Markup of a modal layer
- Flask application
- Submitting form when a field changes
- Markup of modal layer with an automatically submitted form
- Telling the server that the form is automatically submitted
- Flask route for an automatically submitted form
- Index of used functions and methods
This post presents an 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. Also, it allows you to open page fragments in an overlay layer.
By the end of this tutorial, you'll know:
- How to create modal layers using the Unpoly library
- How to open a modal layer
- How to emit a server response to close a modal layer
- How to create a modal layer with an automatically submitted form
Why Unpoly?
Unpoly was designed to enhance server-rendered application
Not every web application needs the complexity of the React JavaScript library. Frameworks like Django, FastAPI, and Flask are capable of generating HTML pages using fast template engines. And the core web stack — HTML, CSS, and JavaScript — allows one to create efficient and accessible multi-page web applications.
Unpoly was written to complement the Ruby on Rails framework and follows the same principle: batteries are included. This means that it provides a large set of features and prefers convention over explicit configuration. For these reasons, Unpoly isn’t the most lightweight or the fastest among JavaScript libraries.
Unpoly doesn’t require package manager and has no dependencies
If your web application is running on a server, then you’re already managing a stack of sophisticated frameworks, libraries, and tools. Adding another platform on top, namely Node.js, just to use npm could be excessive.
Unpoly follows the principle of progressive enhancement
The progressive enhancement technique addresses the problems of people who are not able, or do not want, to use JavaScript on the web. According to this principle, the basic functions of a web application should work without any JavaScript. More functions and affordances, enhanced with JavaScript, are provided for browsers supporting it.
Unpoly is well-documented and battle-tested
It has more then 10 years of development. Its documentation was written by human for humans.
Prerequisities
Start by creating a directory for the Flask application.
Shell
$ mkdir flask-app
$ cd flask-app
Next, create a new virtual environment using the venv module, activate it, and install Flask with pip.
Shell
$ python -m venv venv
$ source venv/bin/activate
$ pip install flask
Installing Unpoly
The simplest way to add Unpoly to our application is to copy unpoly.js and unpoly.css from Unpoly site to static directory of our flask-app.
Shell
$ mkdir static
$ wget https://cdn.jsdelivr.net/npm/unpoly@3.14.1/unpoly.js
$ wget https://cdn.jsdelivr.net/npm/unpoly@3.14.1/unpoly.css
Then add <link> and <script> elements to these files before your own style and scripts files. The commands in double curly braces are executed by Jinja2 template engine. url_for() is a method that Flask provides in the template context. block and endblock are Jinja2 commands for rendering a page using child templates. You can read more about Jinja2 templates here.
base.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="{{ url_for('static', filename='unpoly.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<script src="{{ url_for('static', filename='unpoly.js') }}"></script>
<script src="{{ url_for('static', filename='scripts.js') }}"></script>
<title>{% block title %}My Flask app{% endblock %}</title>
</head>
<body>
<main up-main="root">{% block content %}{% endblock %}</main>
</body>
</html>
Opening a modal layer with Unpoly
Unpoly uses HTML attributes that enable additional behavior on HTML elements.
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 a JavaScript snippet that is called, when the overlay is accepted. Here's the up.navigate() function, which navigates to a URL built by Flask's url_for() function.
up-history="false" sets that changes to fragments 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 Jinja2 expressions that call Python functions.
Markup of a modal layer
Now, let's create our new modal layer in html file named layer_modal.html.
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 built 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 the variable error passed to the template by Flask's route function.
Flask application
Finally, we create Flask application.
hello.py
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.
Run our application with command like this:
Shell
$ flask --app hello run
Submitting form when a field changes
Now, we address a 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 the 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.
Markup of modal layer with an automatically submitted form
The form element attributes are exactly the same as in the first example. Note the use of the up-autosubmit attribute on the input element.
layer_autosubmit.html
<main up-main="modal">
<form method="post" action="{{ url_for('new_with_autosubmit') }}"
up-submit="" up-fail-target="form" novalidate="">
<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 delay (in milliseconds) before the form is submitted after a change.
Telling the server that the form is automatically submitted
The trick here is to pass a parameter to the form before it is submitted. This can be done using Unpoly up.on() function that will be called on up:form:submit event. We check that event.target.name is another_field and set the parameter to pass. (If the user hits the submit button, then event.target is the submit button.)
hello.js
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 hello.js along with other static files. It is important to load hello.js after unpoly.js.
Flask route for an automatically submitted form
hello.py
@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.