Merge pull request #4636 from pallets/docs-javascript
This commit is contained in:
commit
9398630a8f
10 changed files with 285 additions and 170 deletions
|
|
@ -29,7 +29,7 @@ collected in the following pages.
|
|||
wtforms
|
||||
templateinheritance
|
||||
flashing
|
||||
jquery
|
||||
javascript
|
||||
lazyloading
|
||||
mongoengine
|
||||
favicon
|
||||
|
|
|
|||
259
docs/patterns/javascript.rst
Normal file
259
docs/patterns/javascript.rst
Normal file
|
|
@ -0,0 +1,259 @@
|
|||
JavaScript, ``fetch``, and JSON
|
||||
===============================
|
||||
|
||||
You may want to make your HTML page dynamic, by changing data without
|
||||
reloading the entire page. Instead of submitting an HTML ``<form>`` and
|
||||
performing a redirect to re-render the template, you can add
|
||||
`JavaScript`_ that calls |fetch|_ and replaces content on the page.
|
||||
|
||||
|fetch|_ is the modern, built-in JavaScript solution to making
|
||||
requests from a page. You may have heard of other "AJAX" methods and
|
||||
libraries, such as |XHR|_ or `jQuery`_. These are no longer needed in
|
||||
modern browsers, although you may choose to use them or another library
|
||||
depending on your application's requirements. These docs will only focus
|
||||
on built-in JavaScript features.
|
||||
|
||||
.. _JavaScript: https://developer.mozilla.org/Web/JavaScript
|
||||
.. |fetch| replace:: ``fetch()``
|
||||
.. _fetch: https://developer.mozilla.org/Web/API/Fetch_API
|
||||
.. |XHR| replace:: ``XMLHttpRequest()``
|
||||
.. _XHR: https://developer.mozilla.org/Web/API/XMLHttpRequest
|
||||
.. _jQuery: https://jquery.com/
|
||||
|
||||
|
||||
Rendering Templates
|
||||
-------------------
|
||||
|
||||
It is important to understand the difference between templates and
|
||||
JavaScript. Templates are rendered on the server, before the response is
|
||||
sent to the user's browser. JavaScript runs in the user's browser, after
|
||||
the template is rendered and sent. Therefore, it is impossible to use
|
||||
JavaScript to affect how the Jinja template is rendered, but is is
|
||||
possible to render data into the JavaScript that will run.
|
||||
|
||||
To provide data to JavaScript when rendering the template, use the
|
||||
:func:`~jinja-filters.tojson` filter in a ``<script>`` block. This will
|
||||
convert the data to a valid JavaScript object, and ensure that any
|
||||
unsafe HTML characters are rendered safely. If you do not use the
|
||||
``tojson`` filter, you will get a ``SyntaxError`` in the browser
|
||||
console.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
data = generate_report()
|
||||
return render_template("report.html", chart_data=data)
|
||||
|
||||
.. code-block:: jinja
|
||||
|
||||
<script>
|
||||
const chart_data = {{ chart_data|tojson }}
|
||||
chartLib.makeChart(chart_data)
|
||||
</script>
|
||||
|
||||
A less common pattern is to add the data to a ``data-`` attribute on an
|
||||
HTML tag. In this case, you must use single quotes around the value, not
|
||||
double quotes, otherwise you will produce invalid or unsafe HTML.
|
||||
|
||||
.. code-block:: jinja
|
||||
|
||||
<div data-chart='{{ chart_data|tojson }}'></div>
|
||||
|
||||
|
||||
Generating URLs
|
||||
---------------
|
||||
|
||||
The other way to get data from the server to JavaScript is to make a
|
||||
request for it. First, you need to know the URL to request.
|
||||
|
||||
The simplest way to generate URLs is to continue to use
|
||||
:func:`~flask.url_for` when rendering the template. For example:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
const user_url = {{ url_for("user", id=current_user.id)|tojson }}
|
||||
fetch(user_url).then(...)
|
||||
|
||||
However, you might need to generate a URL based on information you only
|
||||
know in JavaScript. As discussed above, JavaScript runs in the user's
|
||||
browser, not as part of the template rendering, so you can't use
|
||||
``url_for`` at that point.
|
||||
|
||||
In this case, you need to know the "root URL" under which your
|
||||
application is served. In simple setups, this is ``/``, but it might
|
||||
also be something else, like ``https://example.com/myapp/``.
|
||||
|
||||
A simple way to tell your JavaScript code about this root is to set it
|
||||
as a global variable when rendering the template. Then you can use it
|
||||
when generating URLs from JavaScript.
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
const SCRIPT_ROOT = {{ request.script_root|tojson }}
|
||||
let user_id = ... // do something to get a user id from the page
|
||||
let user_url = `${SCRIPT_ROOT}/user/${user_id}`
|
||||
fetch(user_url).then(...)
|
||||
|
||||
|
||||
Making a Request with ``fetch``
|
||||
-------------------------------
|
||||
|
||||
|fetch|_ takes two arguments, a URL and an object with other options,
|
||||
and returns a |Promise|_. We won't cover all the available options, and
|
||||
will only use ``then()`` on the promise, not other callbacks or
|
||||
``await`` syntax. Read the linked MDN docs for more information about
|
||||
those features.
|
||||
|
||||
By default, the GET method is used. If the response contains JSON, it
|
||||
can be used with a ``then()`` callback chain.
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
const room_url = {{ url_for("room_detail", id=room.id)|tojson }}
|
||||
fetch(room_url)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
// data is a parsed JSON object
|
||||
})
|
||||
|
||||
To send data, use a data method such as POST, and pass the ``body``
|
||||
option. The most common types for data are form data or JSON data.
|
||||
|
||||
To send form data, pass a populated |FormData|_ object. This uses the
|
||||
same format as an HTML form, and would be accessed with ``request.form``
|
||||
in a Flask view.
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
let data = new FormData()
|
||||
data.append("name": "Flask Room")
|
||||
data.append("description": "Talk about Flask here.")
|
||||
fetch(room_url, {
|
||||
"method": "POST",
|
||||
"body": data,
|
||||
}).then(...)
|
||||
|
||||
In general, prefer sending request data as form data, as would be used
|
||||
when submitting an HTML form. JSON can represent more complex data, but
|
||||
unless you need that it's better to stick with the simpler format. When
|
||||
sending JSON data, the ``Content-Type: application/json`` header must be
|
||||
sent as well, otherwise Flask will return a 400 error.
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
let data = {
|
||||
"name": "Flask Room",
|
||||
"description": "Talk about Flask here.",
|
||||
}
|
||||
fetch(room_url, {
|
||||
"method": "POST",
|
||||
"headers": {"Content-Type": "application/json"},
|
||||
"body": JSON.stringify(data),
|
||||
}).then(...)
|
||||
|
||||
.. |Promise| replace:: ``Promise``
|
||||
.. _Promise: https://developer.mozilla.org/Web/JavaScript/Reference/Global_Objects/Promise
|
||||
.. |FormData| replace:: ``FormData``
|
||||
.. _FormData: https://developer.mozilla.org/en-US/docs/Web/API/FormData
|
||||
|
||||
|
||||
Following Redirects
|
||||
-------------------
|
||||
|
||||
A response might be a redirect, for example if you logged in with
|
||||
JavaScript instead of a traditional HTML form, and your view returned
|
||||
a redirect instead of JSON. JavaScript requests do follow redirects, but
|
||||
they don't change the page. If you want to make the page change you can
|
||||
inspect the response and apply the redirect manually.
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
fetch("/login", {"body": ...}).then(
|
||||
response => {
|
||||
if (response.redirected) {
|
||||
window.location = response.url
|
||||
} else {
|
||||
showLoginError()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
Replacing Content
|
||||
-----------------
|
||||
|
||||
A response might be new HTML, either a new section of the page to add or
|
||||
replace, or an entirely new page. In general, if you're returning the
|
||||
entire page, it would be better to handle that with a redirect as shown
|
||||
in the previous section. The following example shows how to a ``<div>``
|
||||
with the HTML returned by a request.
|
||||
|
||||
.. code-block:: html
|
||||
|
||||
<div id="geology-fact">
|
||||
{{ include "geology_fact.html" }}
|
||||
</div>
|
||||
<script>
|
||||
const geology_url = {{ url_for("geology_fact")|tojson }}
|
||||
const geology_div = getElementById("geology-fact")
|
||||
fetch(geology_url)
|
||||
.then(response => response.text)
|
||||
.then(text => geology_div.innerHtml = text)
|
||||
</script>
|
||||
|
||||
|
||||
Return JSON from Views
|
||||
----------------------
|
||||
|
||||
To return a JSON object from your API view, you can directly return a
|
||||
dict from the view. It will be serialized to JSON automatically.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@app.route("/user/<int:id>")
|
||||
def user_detail(id):
|
||||
user = User.query.get_or_404(id)
|
||||
return {
|
||||
"username": User.username,
|
||||
"email": User.email,
|
||||
"picture": url_for("static", filename=f"users/{id}/profile.png"),
|
||||
}
|
||||
|
||||
If you want to return another JSON type, use the
|
||||
:func:`~flask.json.jsonify` function, which creates a response object
|
||||
with the given data serialized to JSON.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from flask import jsonify
|
||||
|
||||
@app.route("/users")
|
||||
def user_list():
|
||||
users = User.query.order_by(User.name).all()
|
||||
return jsonify([u.to_json() for u in users])
|
||||
|
||||
It is usually not a good idea to return file data in a JSON response.
|
||||
JSON cannot represent binary data directly, so it must be base64
|
||||
encoded, which can be slow, takes more bandwidth to send, and is not as
|
||||
easy to cache. Instead, serve files using one view, and generate a URL
|
||||
to the desired file to include in the JSON. Then the client can make a
|
||||
separate request to get the linked resource after getting the JSON.
|
||||
|
||||
|
||||
Receiving JSON in Views
|
||||
-----------------------
|
||||
|
||||
Use the :attr:`~flask.Request.json` property of the
|
||||
:data:`~flask.request` object to decode the request's body as JSON. If
|
||||
the body is not valid JSON, or the ``Content-Type`` header is not set to
|
||||
``application/json``, a 400 Bad Request error will be raised.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from flask import request
|
||||
|
||||
@app.post("/user/<int:id>")
|
||||
def user_update(id):
|
||||
user = User.query.get_or_404(id)
|
||||
user.update_from_json(request.json)
|
||||
db.session.commit()
|
||||
return user.to_json()
|
||||
|
|
@ -1,148 +1,6 @@
|
|||
:orphan:
|
||||
|
||||
AJAX with jQuery
|
||||
================
|
||||
|
||||
`jQuery`_ is a small JavaScript library commonly used to simplify working
|
||||
with the DOM and JavaScript in general. It is the perfect tool to make
|
||||
web applications more dynamic by exchanging JSON between server and
|
||||
client.
|
||||
|
||||
JSON itself is a very lightweight transport format, very similar to how
|
||||
Python primitives (numbers, strings, dicts and lists) look like which is
|
||||
widely supported and very easy to parse. It became popular a few years
|
||||
ago and quickly replaced XML as transport format in web applications.
|
||||
|
||||
.. _jQuery: https://jquery.com/
|
||||
|
||||
Loading jQuery
|
||||
--------------
|
||||
|
||||
In order to use jQuery, you have to download it first and place it in the
|
||||
static folder of your application and then ensure it's loaded. Ideally
|
||||
you have a layout template that is used for all pages where you just have
|
||||
to add a script statement to the bottom of your ``<body>`` to load jQuery:
|
||||
|
||||
.. sourcecode:: html
|
||||
|
||||
<script src="{{ url_for('static', filename='jquery.js') }}"></script>
|
||||
|
||||
Another method is using Google's `AJAX Libraries API
|
||||
<https://developers.google.com/speed/libraries/>`_ to load jQuery:
|
||||
|
||||
.. sourcecode:: html
|
||||
|
||||
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
|
||||
<script>window.jQuery || document.write('<script src="{{
|
||||
url_for('static', filename='jquery.js') }}">\x3C/script>')</script>
|
||||
|
||||
In this case you have to put jQuery into your static folder as a fallback, but it will
|
||||
first try to load it directly from Google. This has the advantage that your
|
||||
website will probably load faster for users if they went to at least one
|
||||
other website before using the same jQuery version from Google because it
|
||||
will already be in the browser cache.
|
||||
|
||||
Where is My Site?
|
||||
-----------------
|
||||
|
||||
Do you know where your application is? If you are developing the answer
|
||||
is quite simple: it's on localhost port something and directly on the root
|
||||
of that server. But what if you later decide to move your application to
|
||||
a different location? For example to ``http://example.com/myapp``? On
|
||||
the server side this never was a problem because we were using the handy
|
||||
:func:`~flask.url_for` function that could answer that question for
|
||||
us, but if we are using jQuery we should not hardcode the path to
|
||||
the application but make that dynamic, so how can we do that?
|
||||
|
||||
A simple method would be to add a script tag to our page that sets a
|
||||
global variable to the prefix to the root of the application. Something
|
||||
like this:
|
||||
|
||||
.. sourcecode:: html+jinja
|
||||
|
||||
<script>
|
||||
$SCRIPT_ROOT = {{ request.script_root|tojson }};
|
||||
</script>
|
||||
|
||||
|
||||
JSON View Functions
|
||||
-------------------
|
||||
|
||||
Now let's create a server side function that accepts two URL arguments of
|
||||
numbers which should be added together and then sent back to the
|
||||
application in a JSON object. This is a really ridiculous example and is
|
||||
something you usually would do on the client side alone, but a simple
|
||||
example that shows how you would use jQuery and Flask nonetheless::
|
||||
|
||||
from flask import Flask, jsonify, render_template, request
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.route('/_add_numbers')
|
||||
def add_numbers():
|
||||
a = request.args.get('a', 0, type=int)
|
||||
b = request.args.get('b', 0, type=int)
|
||||
return jsonify(result=a + b)
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
return render_template('index.html')
|
||||
|
||||
As you can see I also added an `index` method here that renders a
|
||||
template. This template will load jQuery as above and have a little form where
|
||||
we can add two numbers and a link to trigger the function on the server
|
||||
side.
|
||||
|
||||
Note that we are using the :meth:`~werkzeug.datastructures.MultiDict.get` method here
|
||||
which will never fail. If the key is missing a default value (here ``0``)
|
||||
is returned. Furthermore it can convert values to a specific type (like
|
||||
in our case `int`). This is especially handy for code that is
|
||||
triggered by a script (APIs, JavaScript etc.) because you don't need
|
||||
special error reporting in that case.
|
||||
|
||||
The HTML
|
||||
--------
|
||||
|
||||
Your index.html template either has to extend a :file:`layout.html` template with
|
||||
jQuery loaded and the `$SCRIPT_ROOT` variable set, or do that on the top.
|
||||
Here's the HTML code needed for our little application (:file:`index.html`).
|
||||
Notice that we also drop the script directly into the HTML here. It is
|
||||
usually a better idea to have that in a separate script file:
|
||||
|
||||
.. sourcecode:: html
|
||||
|
||||
<script>
|
||||
$(function() {
|
||||
$('a#calculate').bind('click', function() {
|
||||
$.getJSON($SCRIPT_ROOT + '/_add_numbers', {
|
||||
a: $('input[name="a"]').val(),
|
||||
b: $('input[name="b"]').val()
|
||||
}, function(data) {
|
||||
$("#result").text(data.result);
|
||||
});
|
||||
return false;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<h1>jQuery Example</h1>
|
||||
<p><input type=text size=5 name=a> +
|
||||
<input type=text size=5 name=b> =
|
||||
<span id=result>?</span>
|
||||
<p><a href=# id=calculate>calculate server side</a>
|
||||
|
||||
I won't go into detail here about how jQuery works, just a very quick
|
||||
explanation of the little bit of code above:
|
||||
|
||||
1. ``$(function() { ... })`` specifies code that should run once the
|
||||
browser is done loading the basic parts of the page.
|
||||
2. ``$('selector')`` selects an element and lets you operate on it.
|
||||
3. ``element.bind('event', func)`` specifies a function that should run
|
||||
when the user clicked on the element. If that function returns
|
||||
`false`, the default behavior will not kick in (in this case, navigate
|
||||
to the `#` URL).
|
||||
4. ``$.getJSON(url, data, func)`` sends a ``GET`` request to `url` and will
|
||||
send the contents of the `data` object as query parameters. Once the
|
||||
data arrived, it will call the given function with the return value as
|
||||
argument. Note that we can use the `$SCRIPT_ROOT` variable here that
|
||||
we set earlier.
|
||||
|
||||
Check out the :gh:`example source <examples/javascript>` for a full
|
||||
application demonstrating the code on this page, as well as the same
|
||||
thing using ``XMLHttpRequest`` and ``fetch``.
|
||||
Obsolete, see :doc:`/patterns/javascript` instead.
|
||||
|
|
|
|||
|
|
@ -3,15 +3,15 @@ JavaScript Ajax Example
|
|||
|
||||
Demonstrates how to post form data and process a JSON response using
|
||||
JavaScript. This allows making requests without navigating away from the
|
||||
page. Demonstrates using |XMLHttpRequest|_, |fetch|_, and
|
||||
|jQuery.ajax|_. See the `Flask docs`_ about jQuery and Ajax.
|
||||
|
||||
.. |XMLHttpRequest| replace:: ``XMLHttpRequest``
|
||||
.. _XMLHttpRequest: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest
|
||||
page. Demonstrates using |fetch|_, |XMLHttpRequest|_, and
|
||||
|jQuery.ajax|_. See the `Flask docs`_ about JavaScript and Ajax.
|
||||
|
||||
.. |fetch| replace:: ``fetch``
|
||||
.. _fetch: https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch
|
||||
|
||||
.. |XMLHttpRequest| replace:: ``XMLHttpRequest``
|
||||
.. _XMLHttpRequest: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest
|
||||
|
||||
.. |jQuery.ajax| replace:: ``jQuery.ajax``
|
||||
.. _jQuery.ajax: https://api.jquery.com/jQuery.ajax/
|
||||
|
||||
|
|
@ -21,7 +21,7 @@ page. Demonstrates using |XMLHttpRequest|_, |fetch|_, and
|
|||
Install
|
||||
-------
|
||||
|
||||
::
|
||||
.. code-block:: text
|
||||
|
||||
$ python3 -m venv venv
|
||||
$ . venv/bin/activate
|
||||
|
|
@ -31,7 +31,7 @@ Install
|
|||
Run
|
||||
---
|
||||
|
||||
::
|
||||
.. code-block:: text
|
||||
|
||||
$ export FLASK_APP=js_example
|
||||
$ flask run
|
||||
|
|
@ -42,7 +42,7 @@ Open http://127.0.0.1:5000 in a browser.
|
|||
Test
|
||||
----
|
||||
|
||||
::
|
||||
.. code-block:: text
|
||||
|
||||
$ pip install -e '.[test]'
|
||||
$ coverage run -m pytest
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<!doctype html>
|
||||
<title>JavaScript Example</title>
|
||||
<link rel="stylesheet" href="https://unpkg.com/sakura.css@1.0.0/css/normalize.css">
|
||||
<link rel="stylesheet" href="https://unpkg.com/sakura.css@1.0.0/css/sakura-earthly.css">
|
||||
<link rel="stylesheet" href="https://unpkg.com/normalize.css@8.0.1/normalize.css">
|
||||
<link rel="stylesheet" href="https://unpkg.com/sakura.css@1.3.1/css/sakura.css">
|
||||
<style>
|
||||
ul { margin: 0; padding: 0; display: flex; list-style-type: none; }
|
||||
li > * { padding: 1em; }
|
||||
|
|
@ -13,10 +13,10 @@
|
|||
</style>
|
||||
<ul>
|
||||
<li><span>Type:</span>
|
||||
<li class="{% if js == 'plain' %}active{% endif %}">
|
||||
<a href="{{ url_for('index', js='plain') }}">Plain</a>
|
||||
<li class="{% if js == 'fetch' %}active{% endif %}">
|
||||
<a href="{{ url_for('index', js='fetch') }}">Fetch</a>
|
||||
<li class="{% if js == 'xhr' %}active{% endif %}">
|
||||
<a href="{{ url_for('index', js='xhr') }}">XHR</a>
|
||||
<li class="{% if js == 'jquery' %}active{% endif %}">
|
||||
<a href="{{ url_for('index', js='jquery') }}">jQuery</a>
|
||||
</ul>
|
||||
|
|
|
|||
|
|
@ -2,14 +2,11 @@
|
|||
|
||||
{% block intro %}
|
||||
<a href="https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch"><code>fetch</code></a>
|
||||
is the <em>new</em> plain JavaScript way to make requests. It's
|
||||
supported in all modern browsers except IE, which requires a
|
||||
<a href="https://github.com/github/fetch">polyfill</a>.
|
||||
is the <em>modern</em> plain JavaScript way to make requests. It's
|
||||
supported in all modern browsers.
|
||||
{% endblock %}
|
||||
|
||||
{% block script %}
|
||||
<script src="https://unpkg.com/promise-polyfill@7.1.2/dist/polyfill.min.js"></script>
|
||||
<script src="https://unpkg.com/whatwg-fetch@2.0.4/fetch.js"></script>
|
||||
<script>
|
||||
function addSubmit(ev) {
|
||||
ev.preventDefault();
|
||||
|
|
|
|||
|
|
@ -2,8 +2,9 @@
|
|||
|
||||
{% block intro %}
|
||||
<a href="https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest"><code>XMLHttpRequest</code></a>
|
||||
is the plain JavaScript way to make requests. It's natively supported
|
||||
by all browsers.
|
||||
is the original JavaScript way to make requests. It's natively supported
|
||||
by all browsers, but has been superseded by
|
||||
<a href="{{ url_for("index", js="fetch") }}"><code>fetch</code></a>.
|
||||
{% endblock %}
|
||||
|
||||
{% block script %}
|
||||
|
|
@ -5,8 +5,8 @@ from flask import request
|
|||
from js_example import app
|
||||
|
||||
|
||||
@app.route("/", defaults={"js": "plain"})
|
||||
@app.route("/<any(plain, jquery, fetch):js>")
|
||||
@app.route("/", defaults={"js": "fetch"})
|
||||
@app.route("/<any(xhr, jquery, fetch):js>")
|
||||
def index(js):
|
||||
return render_template(f"{js}.html", js=js)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[metadata]
|
||||
name = js_example
|
||||
version = 1.0.0
|
||||
version = 1.1.0
|
||||
url = https://flask.palletsprojects.com/patterns/jquery/
|
||||
license = BSD-3-Clause
|
||||
maintainer = Pallets
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ from flask import template_rendered
|
|||
@pytest.mark.parametrize(
|
||||
("path", "template_name"),
|
||||
(
|
||||
("/", "plain.html"),
|
||||
("/plain", "plain.html"),
|
||||
("/", "xhr.html"),
|
||||
("/plain", "xhr.html"),
|
||||
("/fetch", "fetch.html"),
|
||||
("/jquery", "jquery.html"),
|
||||
),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue