mirror of
https://github.com/bigskysoftware/hypermedia-systems.git
synced 2025-12-03 00:03:50 -05:00
853 lines
45 KiB
Plaintext
853 lines
45 KiB
Plaintext
= Hypermedia In Action
|
|
:chapter: 2
|
|
:sectnums:
|
|
:figure-caption: Figure {chapter}.
|
|
:listing-caption: Listing {chapter}.
|
|
:table-caption: Table {chapter}.
|
|
:sectnumoffset: 1
|
|
// line above: :sectnumoffset: 5 (chapter# minus 1)
|
|
:leveloffset: 1
|
|
:sourcedir: ../code/src
|
|
:source-language:
|
|
|
|
= A Simple Web Application
|
|
|
|
This chapter covers
|
|
|
|
* Picking a "web stack" to build our sample hypermedia application in
|
|
* A brief introduction to Flask & Jinja2 for Server Side Rendering (SSR)
|
|
* An overview of the functionality of our sample hypermedia application, Contact.App
|
|
* Implementing the basic CRUD (Create, Read, Update, Delete) operations and search for Contact.App
|
|
|
|
== A Simple Contact Management Web Application
|
|
|
|
To begin our journey into Hypermedia Driven Applications, we are going to create a simple contact management web
|
|
application called Contacts.app. We will start with a basic, "Web 1.0-style" multi-page application, in the grand
|
|
CRUD (Create, Read, Update, Delete) tradition. It will not be the best contact management application, but that's OK
|
|
because it will be simple (a great virtue of web 1.0 applications!) It will also be easy to incrementally improve the
|
|
application by taking advantage of hypermedia-oriented technologies like htmx.
|
|
|
|
By the time we are finished building the application, over the coming chapters, it will have some very slick features
|
|
that most developers today would assume requires the use of sophisticated client-side infrastructure. We will, instead,
|
|
implement these features entirely using the hypermedia approach, but enhanced with htmx and other libraries that stay
|
|
within that paradigm.
|
|
|
|
=== Picking A "Web Stack" To Use
|
|
|
|
In order to demonstrate how a hypermedia application works, we need to pick a server-side language and library for
|
|
handling HTTP requests. Colloquially, this is called our "Server Side" or "Web" stack, and there are literally hundreds
|
|
of options to choose from, all with passionate followers. You probably have a web framework that you prefer and, while I wish
|
|
we could write this book for every possible stack out there, in the interest of berevity we can only pick one.
|
|
|
|
For this book we are going to use the following stack:
|
|
|
|
* We will use Python as our programming language (https://www.python.org/)
|
|
* We will use Flask as our web framework, allowing us to connect HTTP requests to python logic (https://palletsprojects.com/p/flask/)
|
|
* We will use Jinja2 for our server-side templating language, allowing us to render HTML responses using a familiar
|
|
and intuitive syntax (https://palletsprojects.com/p/jinja/)
|
|
|
|
Why pick this particular stack? I am not a day-to-day Python programmer, so it's not an obvious choice for me!
|
|
But this particular stack has a number of advantages.
|
|
|
|
First off, python is the most popular programming language as of this writing, according to the TIOBE index
|
|
(https://www.tiobe.com/tiobe-index/), a popular measure of programming language popularity.
|
|
More importantly, even if you don't know or like Python, it is very easy to read. As a veteran of the programming language wars of the 90's and
|
|
early aughts, I understand how passionate people are around programming languages, and I hope python is a "least worst"
|
|
choice for readers who are not pythonistas.
|
|
|
|
I picked flask as the web framework because it is very lightweight and does not impose a lot of structure on top of the
|
|
basics of HTTP routing. This bare-bones approach isn't for everyone: in the python community, for example, many people
|
|
prefer the "Batteries Included" nature of Django, where lots of functionality is available out of the box.
|
|
|
|
I understand that perspective, but, for teaching purposes, I feel that an un-opinionated and light-weight library will
|
|
make it easier for non-Python developers to follow along by minimizing the amount of code required on the server side.
|
|
Anyone who prefers django or some other Python web framework, or some other language entirely for that matter, should be
|
|
able to easily convert the Flask examples into their native framework.
|
|
|
|
Jinja2 templates were picked because they are the default templating language for Flask. They are simple enough and
|
|
standard enough that most people who understand any server side (or client side) templating library will be able to
|
|
understand them reasonably quickly and easily.
|
|
|
|
With Jinja2, we will be rendering our HTML _on the server side_. This is the traditional approach to building web
|
|
applications, but, with the rise of SPAs, is not as widely known a technique as it once was. Today, as people are
|
|
rediscovering this approach to building web applications, the term "Server Side Rendering" or SSR is emerging as
|
|
the way that people talk about this style of templating, in contrast with "Client Side Rendering", that is, rendering
|
|
templates in the browser, as is common in SPA libraries.
|
|
|
|
In general, in Contact.app, we will intentionally keep things as simple as possible (sometimes sacrificing other
|
|
design principles to do so!) to maximize the teaching value of our code: it won't be perfectly factored code, and it
|
|
certainly won't be the most beautiful web application, but it will be easy enough to follow for readers, and easy to
|
|
translate into your preferred language and web stack.
|
|
|
|
=== The HOWL (Hypermedia On Whatever you'd Like) Stack
|
|
|
|
We picked Python and Flask for this book, but we could have picked anything. One of the wonderful things about
|
|
building a hypermedia-based application is that your backend can be... whatever you'd like! You just need to be able
|
|
to produce HTML with it.
|
|
|
|
Consider if we were instead building a web application with a large JavaScript-based SPA front end. We would almost certainly
|
|
feel pressure to adopt JavaScript on the back end. We already would have a ton of code written in JavaScript.
|
|
Why maintain two separate code bases? Why not reuse domain logic on the client-side as well as the server-side? There are now that
|
|
very good server side options for writing JavaScript code like node and deno. Why not just a single language for everything?
|
|
|
|
So, as you may have felt yourself, if you choose a JavaScript heavy front end there are many forces pushing you to adopt
|
|
JavaScript on the back end.
|
|
|
|
In contrast, by using a hypermedia-based front end you have a lot more freedom in picking the back end technology appropriate
|
|
to the problem domain you are addressing. You certainly aren't writing your server side logic in HTML! And every
|
|
major programming language has at least one good templating library that can produce HTML cleanly, often more.
|
|
|
|
If we are doing something in big data, perhaps you'd like to use Python, which has tremendous support for that domain.
|
|
If we are doing AI, perhaps you'd like to use Lisp, leaning on a language with a long history in that area of research.
|
|
Maybe you are a functional programming enthusiast and want to use OCaml or Haskell. Maybe you just really like Julia or
|
|
Nim. All perfectly valid reasons for choosing a particular server side technology! By using hypermedia as your _front end_
|
|
technology, you are freed up to adopt any of these choices. There simply isn't a large JavaScript front end code base
|
|
pressuring you to adopt JavaScript on the back end.
|
|
|
|
In the htmx community, we call this the HOWL stack: Hypermedia On Whatever you'd Like. We _like_ the idea of a multi-language,
|
|
multi-framework future in web development. To be frank, a future of total JavaScript dominance (with maybe some TypeScript
|
|
throw in) sounds pretty boring to us. We'd prefer to see many different language and web framework communities, each
|
|
with their own strengths and cultures, participating in the web development world, all through the power of hypermedia.
|
|
|
|
That sounds like a better world to us, and that's HOWL, hypermedia on whatever you'd like!
|
|
|
|
== Python
|
|
|
|
Since this book is intended to teach how hypermedia can be used effectively, we aren't going to do deep dives into
|
|
the various technologies we use _around_ that hypermedia. This has some obvious drawbacks: if you aren't comfortable
|
|
with python, for example, some of the example python code in the book may seem confusing or mysterious.
|
|
|
|
If you feel like you need a quick introduction to the language, we heartily recommend "The Quick Python Book" by
|
|
Manning Publishing. It gives you a clear and brisk introduction to the language, and will put you in a good position
|
|
to understand the relatively simple python code used in this book.
|
|
|
|
That being said, I think most web developers, even developers who are unfamiliar with python, should be able to follow
|
|
along in the code. Remember, I hadn't written very much python before writing this book, and I got the hang of it pretty
|
|
quick!
|
|
|
|
== Introducing Flask: Our First Route
|
|
|
|
Flask is a very simple but flexible web framework for Python. Just like this book isn't a Python book, it isn't a Flask book
|
|
either, so we will not go into too much detail about it. However, unlike Python, which is similar in many ways to other
|
|
programming languages, Flask might be a bit different than the web frameworks you are familiar with, so it is necessary
|
|
to do a bit more of an introduction to prepare you for the coming chapters. Thankfully, Flask is simple enough that most
|
|
web developers shouldn't have a problem following along. Let's go over the basics.
|
|
|
|
A Flask application consists of a series of _routes_ tied to functions that execute when an HTTP request to a given path is
|
|
made. I will use the term "handler" to refer to the functions associated with a route.
|
|
|
|
Let's look at the first "route" definition in our application. It will be a simple redirect, such that when a user goes to the
|
|
root of our web application, `/`, they will be redirected to the `/contacts` path instead. Redirection is an HTTP feature where, when
|
|
a user requests one URL, they are sent to another on, and is a basic piece of web functionality that is well supported
|
|
in most web frameworks.
|
|
|
|
Let's create our first route definition, a simple "Hello World" route. In the following python code you will see the
|
|
`@app` symbol. This refers to the flask application object. Don't worry too much about how it has been set up, just
|
|
understand that it is an object that encapsulates the mapping of requests to some _path_ to a python
|
|
function (i.e. handler) to be executed by the server when a request to that path is made.
|
|
|
|
Here is the code:
|
|
|
|
.A Simple "Hello World" Route
|
|
[source,python]
|
|
----
|
|
@app.route("/") <1>
|
|
def index(): <2>
|
|
return "Hello World!" <3>
|
|
----
|
|
<1> Establishes we are mapping the `/` path as a route
|
|
<2> The next method is the handler for that route
|
|
<3> Returns the string "Hello World!" to the client
|
|
|
|
The Flask pattern for doing this is to use the `route()` method on the Flask application object, and pass in the path
|
|
you wish the route to handle. In this case we pass in the root or `/` path, as a string, to the `@app.route()` method.
|
|
This establishes a path that Flask will handle.
|
|
|
|
This route declaration is then followed by a simple function definition, `index()`. In flask, the function immediately
|
|
following a route definition is the "handler" for that route, and will be executed when an HTTP request to the
|
|
given path is made. (Note that the name of the function doesn't matter, we can call it whatever
|
|
we'd like. In this case I chose `index()` because that fits with the route we are handling: the root "index" of the web
|
|
applications.) So we have the `index()` function immediately following
|
|
our route definition for the root, and this will become the handler for the root URL in our web application.
|
|
|
|
The handler in this case is dead simple, it just returns a string, "Hello World!", to the client. This isn't even
|
|
hypermedia yet, but, nonetheless, the browser renders it fine:
|
|
|
|
[#figure-1-1, reftext="Figure {chapter}.{counter:figure}"]
|
|
.Hello Flask!
|
|
image::../images/figure_2-1_hello_world.png[]
|
|
|
|
For Contact.app, rather than rendering "Hello World!" at the root path, we are going to do something a little fancy:
|
|
we are going to redirect to another path, the `/contacts` path. Redirects are a feature of HTTP that allow you to,
|
|
well, redirect a client to another location in an HTTP response. Redirecting to the `/contacts` path is a bit more
|
|
consistent with notion of resources with REST. It's a judgement call on our part, but we are going to go with it.
|
|
|
|
To change our "Hello World" route to a redirect, we only need to change one line of code:
|
|
|
|
.Changing "Hello World" to a Redirect
|
|
[source,python]
|
|
----
|
|
@app.route("/")
|
|
def index():
|
|
return redirect("/contacts") <1>
|
|
----
|
|
<1> Update to a call to `redirect()`
|
|
|
|
|
|
Now the `index()` function simply returns the result of calling a `redirect()` function with the path we with to
|
|
redirect to, in this case `/contacts`, passed in as a string. This simple handler implementation will trigger an
|
|
HTTP Redirect to that path, achieving what we desire for this route.
|
|
|
|
So, in summary, given the functionality above, when someone navigates to the root directory of our web application, Flask
|
|
will redirect them to the `/contacts` path. Pretty simple, and I hope nothing too surprising for you, regardless of what
|
|
web framework or language you are used to!
|
|
|
|
== Contact.App Functionality
|
|
|
|
OK, with that brief introduction to Flask out of the way, let's get down to specifying and implementing our application.
|
|
What will Contact.app do?
|
|
|
|
Initially, it will provide the following functionality:
|
|
|
|
* Provide a list of contacts, including first name, last name, phone and email address
|
|
* Provide the ability to search the list of contacts
|
|
* Provide the ability to add a new contact to the list
|
|
* Provide the ability to view the details of a contact on the list
|
|
* Provide the ability to edit the details of a contact on the list
|
|
* Provide the ability to delete a contact from the list
|
|
|
|
So, as you can see, this is a pretty basic CRUD application, the sort of thing that is perfect for an old-school
|
|
web 1.0 application. The source code of the application is available at https://github.com/bigskysoftware/contact-app.
|
|
|
|
=== Showing A Searchable List Of Contacts
|
|
|
|
Let's look at our first "real" bit of functionality: the ability show all the contacts in our system in a list (really,
|
|
in a table).
|
|
|
|
This functionality is going to be found at the `/contacts` path, which is the path our previous route is redirecting to.
|
|
|
|
We will use the `@app` flask instance to route the `/contacts` path and then define a handler function, `contacts()`.
|
|
This function is going to do one of two things:
|
|
|
|
* If there is a search term, it filter all contacts matching that term
|
|
* If not, it will simply return all contacts in our database.
|
|
|
|
Here is the code:
|
|
|
|
.Server Side Search
|
|
[source,python]
|
|
----
|
|
@app.route("/contacts")
|
|
def contacts():
|
|
search = request.args.get("q") <1>
|
|
if search:
|
|
contacts_set = Contact.search(search) <2>
|
|
else:
|
|
contacts_set = Contact.all() <3>
|
|
return render_template("index.html", contacts=contacts_set) <4>
|
|
----
|
|
<1> Look for the query parameter named `q`, which stands for "query"
|
|
<2> If the parameter exists, call the `Contact.search()` function with it
|
|
<3> If not, call the `Contact.all()` function
|
|
<4> pass the result to the `index.html` template to render to the client
|
|
|
|
We see the usual routing code we saw in our first example, but then we see some more elaborate code in the handler
|
|
function. First, we check to see if a search query parameter named `q` is part of the request. The "query string" is
|
|
part of the URL specification and you are probably familiar with it. Here is an example URL with a query string in it:
|
|
`https://example.com/contacts?q=joe`. The query string is everything after the `?` and is a name-value pair format. In
|
|
this case, the query parameter `q` is set to the string value `joe`.
|
|
|
|
To get back to the code, if a query parameter is found, we call out to the `search()` method on the `Contact` model to do
|
|
the actual search and return all matching contacts. If the query parameter is _not_ found, we simply get all contacts by
|
|
invoking the `all()` method on the `Contact` object.
|
|
|
|
Finally, we then render a template, `index.html` that displays the given contacts, passing in the results of whichever function
|
|
we ended up calling.
|
|
|
|
Note that we are not going to dig into the code in the `Contact` class. The implementation of the `Contact` class
|
|
is not relevant to hypermedia, we will ask you to simply accept that it is a "normal" domain model class, and the methods
|
|
on it act in the "normal" manner. We will treat `Contact` as a _resource_ and will provide hypermedia representations
|
|
of that resource to clients, in the form of HTML generated via server side templates.
|
|
|
|
==== The List & Search Template
|
|
|
|
Now we need to take a look at the template that we are going to render in our response to the client. In this
|
|
HTML response we want to have a few things:
|
|
|
|
* A list of any matching or all contacts
|
|
* A search box that a user may type a value into and submit for searches
|
|
* A bit of surrounding "chrome": a header and footer for the website that will be the same regardless of the page you
|
|
are on
|
|
|
|
Recall we are using the Jinja2 templating language here. In Jinja2 templates, we use `{{}}` to embed expression
|
|
values and we use ``{% %}`` for directives, like iteration or including other content. Jinja2 is very similar to
|
|
other templating languages, and I hope you are able to follow along easily.
|
|
|
|
Let's look at the first few lines of code in our `index.html` template:
|
|
|
|
[source, html]
|
|
----
|
|
{% extends 'layout.html' %} <1>
|
|
|
|
{% block content %} <2>
|
|
|
|
<form action="/contacts" method="get" class="tool-bar"> <3>
|
|
<label for="search">Search Term</label>
|
|
<input id="search" type="search" name="q" value="{{ request.args.get('q') or '' }}"/> <4>
|
|
<input type="submit" value="Search"/>
|
|
</form>
|
|
----
|
|
<1> Set the layout template for this template
|
|
<2> Delimit the content to be inserted into the layout
|
|
<3> Create a search form that will issue an HTTP `GET` to the `/contacts` page
|
|
<4> Create an input that a query can be typed into to search contacts
|
|
|
|
The first line of code references a base template, `layout.html`, with the `extends` directive. This layout
|
|
template provides the layout for the page (again, sometimes called "the chrome"): it imports any necessary CSS and
|
|
scripts, includes a `<head>` element, and so forth.
|
|
|
|
The next line of code declares the `content` section of this template, which is the content that will be included
|
|
within the "chrome" of the layout template.
|
|
|
|
Next we have our first bit of actual HTML, rather than just Jinja directives. We have a simple HTML form that allows
|
|
you to search contacts by issuing a `GET` request to the `/contacts` path. The form itself contains a lable and
|
|
an input with the name "q". This input's value will be submitted with the `GET` request to the `/contacts` path.
|
|
|
|
Note that the value of this input is set to the Jinja expression `{{ request.args.get('q') or '' }}`. This expression
|
|
is evaluated by Jinja and will insert the request value of "q" as the input's value, if it exists. This will "preserve"
|
|
the search value when a user does a search, so the text box contains the value that they typed in initially, and makes
|
|
for a slightly nicer user experience.
|
|
|
|
Finally, we have a submit-type input, which, when clicked on, will trigger the form to issue an HTTP request.
|
|
|
|
This search UI forms the top of our contact page, and it is followed a table of all the contacts that are are stored on
|
|
the server. (Or, if there is a search term, the contacts that match that search term. More on that in a bit.)
|
|
|
|
Here is what the template code looks like:
|
|
|
|
.The Contacts Table
|
|
[source, html]
|
|
----
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>First</th> <th>Last</th> <th>Phone</th> <th>Email</th> <th></th><1>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for contact in contacts %} <2>
|
|
<tr>
|
|
<td>{{ contact.first }}</td>
|
|
<td>{{ contact.last }}</td>
|
|
<td>{{ contact.phone }}</td>
|
|
<td>{{ contact.email }}</td> <3>
|
|
<td><a href="/contacts/{{ contact.id }}/edit">Edit</a>
|
|
<a href="/contacts/{{ contact.id }}">View</a></td> <4>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
----
|
|
<1> We output some headers for our table
|
|
<2> We iterate over the contacts that were passed in to the template
|
|
<3> We output the values of the current contact, first name, last name, etc. in columns
|
|
<4> An "operations" column that has links embedded in it to edit or view the contact details
|
|
|
|
Here we are into the core of the page: we construct a table with appropriate headers matching the data we are going
|
|
to show for each contact. We iterate over the contacts that were passed into the template by the handler method using
|
|
the `for` loop directive in Jinja2. We then construct a series of rows, one for each contact, where we render the
|
|
first and last name, phone and email of the contact as table cells in the row.
|
|
|
|
Finally, we have an additional cell that includes two links:
|
|
|
|
* A link to the "Edit" page for the contact, located at `/contacts/{{ contact.id }}/edit` (e.g. For the contact with
|
|
id 42, the edit link will point to `/contacts/42/edit`)
|
|
* A link to the "View" page for the contact `/contacts/{{ contact.id }}` (using our previous contact example, the show
|
|
page would be at `/contacts/42`)
|
|
|
|
|
|
Finally, we have a bit of end-matter: a link to add a new contact and a directive to close up the `content` block:
|
|
|
|
[source, html]
|
|
----
|
|
<p>
|
|
<a href="/contacts/new">Add Contact</a> <1>
|
|
</p>
|
|
|
|
{% endblock %} <2>
|
|
----
|
|
<1> A link to the page that allows you to create a new contact
|
|
<2> The closing element of the `content` block
|
|
|
|
And that's our template! Using this server side template, in combination with our handler method, we can respond with
|
|
an HTML _representation_ of all the contacts requested. So far, so hypermedia!
|
|
|
|
Here is what the template looks like, rendered with a bit of contact data:
|
|
|
|
[#figure-1-1, reftext="Figure {chapter}.{counter:figure}"]
|
|
.Contact.App
|
|
image::../images/figure_2-2_table_etc.png[]
|
|
|
|
|
|
It won't win any design awards at this poitn, but notice that our template, when rendered,
|
|
provides all the functionality necessary to see all the contacts and search them, and also provides links to edit them,
|
|
view details of them or even create a new one. And it does all this without the browser knowing a thing about Contacts!
|
|
The browser just knows how to issue HTTP requests and render HTML.
|
|
|
|
As simple as it is, this is a very REST-ful application!
|
|
|
|
=== Adding A New Contact
|
|
|
|
The next bit of functionality that we will add to our application is the ability to add new contacts. To do so, we
|
|
are going to need to handle that `/contacts/new` URL referenced in the "Add Contact" link above. Note that when a user
|
|
clicks on that link, the browser will issue a `GET` request to the `/contacts/new` URL. The other routes we have been
|
|
looking at were using `GET` as well, but we are actually going to use two different HTTP methods for this bit of functionality:
|
|
an HTTP `GET` and an HTTP `POST`, so we are going to be explicit when we declare this route.
|
|
|
|
Here is our code:
|
|
|
|
.The New Contact GET Route
|
|
[source,python]
|
|
----
|
|
@app.route("/contacts/new", methods=['GET']) <1>
|
|
def contacts_new_get():
|
|
return render_template("new.html", contact=Contact()) <2>
|
|
----
|
|
<1> We declare a route, explicitly handling `GET` requests to this path
|
|
<2> We render the `new.html` template, passing in a new contact object
|
|
|
|
Pretty simple! We just render a `new.html` template with, well, a new Contact, as you might expect!
|
|
(Note that `Contact()` is the python syntax for creating a new instance of the `Contact` class.)
|
|
|
|
So the handler code for this route is very simple. The `new.html` Jinja2 template, in fact, is more complex. For the
|
|
remaining templates I am not going to include the starting layout directive or the content block declaration, but you
|
|
can assume they are the same unless I say otherwise. This will let us focus on the "meat" of the template.
|
|
|
|
If you are familiar with HTML you are probably expecting a form element here, and you will not be disappointed. We are
|
|
going to use the standard form element for collecting contact information and submitting it to the server.
|
|
|
|
Here is what our HTML looks like:
|
|
|
|
.A New Contact Form
|
|
[source, html]
|
|
----
|
|
<form action="/contacts/new" method="post"> <1>
|
|
<fieldset>
|
|
<legend>Contact Values</legend>
|
|
<p>
|
|
<label for="email">Email</label> <2>
|
|
<input name="email" id="email" type="email" placeholder="Email" value="{{ contact.email or '' }}"> <3>
|
|
<span class="error">{{ contact.errors['email'] }}</span> <4>
|
|
</p>
|
|
----
|
|
<1> A form that will submit to the `/contacts/new` path, using an HTTP `POST` request
|
|
<2> A label for the first form input
|
|
<3> the first form input, of type email
|
|
<4> Any error messages associated with this field
|
|
|
|
In the first line of code we create a form that will submit back _to the same path_ that we are handling: `/contacts/new`.
|
|
Rather than issuing an HTTP `GET` to this path, however, we will issue an HTTP `POST` to it. This is the standard way
|
|
of signalling via HTTP that you wish to create a new resource, rather than simply get a representation of it.
|
|
|
|
We then have a label and input (always a good practice) that capture the email of the new contact in question. The
|
|
"name" of the input is "email" and, when this form is submitted, the value of this input will be submitted in the `POST`
|
|
request, associated with the "email" key.
|
|
|
|
Next we have inputs for the other fields for contacts:
|
|
|
|
.Inputs And Labels For The New Contact Form
|
|
[source, html]
|
|
----
|
|
<p>
|
|
<label for="first_name">First Name</label>
|
|
<input name="first_name" id="first_name" type="text" placeholder="First Name" value="{{ contact.first or '' }}">
|
|
<span class="error">{{ contact.errors['first'] }}</span>
|
|
</p>
|
|
<p>
|
|
<label for="last_name">Last Name</label>
|
|
<input name="last_name" id="last_name" type="text" placeholder="Last Name" value="{{ contact.last or '' }}">
|
|
<span class="error">{{ contact.errors['last'] }}</span>
|
|
</p>
|
|
<p>
|
|
<label for="phone">Phone</label>
|
|
<input name="phone" id="phone" type="text" placeholder="Phone" value="{{ contact.phone or '' }}">
|
|
<span class="error">{{ contact.errors['phone'] }}</span>
|
|
</p>
|
|
----
|
|
|
|
Finally, we have a button that will submit the form, the end of the form tag, and a link back to the main contacts table:
|
|
|
|
.The Submit Button For The New Contact Form
|
|
[source, html]
|
|
----
|
|
<button>Save</button>
|
|
</fieldset>
|
|
</form>
|
|
|
|
<p>
|
|
<a href="/contacts">Back</a>
|
|
</p>
|
|
----
|
|
|
|
It is worth pointing out something that is easy to miss: here we are again seeing the flexibility of hypermedia! If we
|
|
add a new field, remove a field, or change the logic around how fields are validated or work with one another, this new
|
|
state of affairs is simply reflected in the hypermedia representation given to users. A user will see the updated
|
|
new content and be able to work with it, no software update required!
|
|
|
|
==== Handling The Post to `/contacts/new`
|
|
|
|
The next step in our application is to handle the `POST` that this form makes to `/contacts/new` to create a new
|
|
Contact.
|
|
|
|
To do so, we need to add another route that uses the same path but handles the `POST` method instead of the `GET`. We
|
|
will take the submitted form values and attempt to create a Contact. If it works, we will redirect to the list of
|
|
contacts and show a success message. If it doesn't then we will show the new contact form again, rendering any
|
|
errors that occurred in the HTML so the user can correct them.
|
|
|
|
Here is our controller code:
|
|
|
|
[source, python]
|
|
----
|
|
@app.route("/contacts/new", methods=['POST'])
|
|
def contacts_new():
|
|
c = Contact(None, request.form['first_name'], request.form['last_name'], request.form['phone'],
|
|
request.form['email']) <1>
|
|
if c.save(): <2>
|
|
flash("Created New Contact!")
|
|
return redirect("/contacts") <3>
|
|
else:
|
|
return render_template("new.html", contact=c) <4>
|
|
----
|
|
<1> We construct a new contact object with the values from the form
|
|
<2> We try to save it
|
|
<3> If it succeeds we "flash" a success message and redirect back to the `/contacts` page
|
|
<4> If not, we rerender the form, showing any errors to the user
|
|
|
|
|
|
The logic here is a bit more complex than other handler methods we have seen, but not by a whole lot. The first thing
|
|
we do is create a new Contact, again using the `Contact()` syntax in python to construct the object. We pass in the values
|
|
submitted by the user in the form by using the `request.form` object, provided by flash Flask. This object allows us to
|
|
access form values in a convenient and easy to read syntax. Note that we pick out each value based on the `name` associated
|
|
with each input in the form.
|
|
|
|
We also pass in `None` as the first value to the `Contact` constructor. This is the "id" parameter, and by passing in
|
|
`None` we are signaling that it is a new contact, and needs to have an ID generated for it.
|
|
|
|
|
|
Next, we call the `save()` method on the Contact object. This returns `true` if the save is successful, and `false` if
|
|
the save is unsuccessful, for example if one of the fields has a bad value in it. (Again, we are not going to dig into
|
|
the details of how this model object is implemented, our only concern is using it to generate hypermedia responses.)
|
|
|
|
If we are able to save the contact (that is, there were no validation errors), we create a _flash_ message indicating
|
|
success and redirect the browser back to the list page. A flash is a common feature in web frameworks that allows
|
|
you to store a message that will be available on the _next_ request, typically in a cookie or in a session store.
|
|
|
|
Finally, if we are unable to save the contact, we rerender the `new.html` template with the contact. This will show the
|
|
same template as above, but the inputs will be filled in with the submitted values, and any errors associated with the
|
|
fields will be rendered to feedback to the user as to what validation failed.
|
|
|
|
Note that, in the case of a successful creation of a contact, we have implemented the Post/Redirect/Get pattern we
|
|
discussed earlier.
|
|
|
|
Believe it or not, this is about as complicated as our handler logic will get, even when we look at adding more advanced
|
|
htmx-based behavior. Simplicity is a great selling point of the hypermedia approach!
|
|
|
|
=== Viewing The Details Of A Contact
|
|
|
|
The next piece of functionality we will implement is the details page for a Contact. The user will navigate to this
|
|
page by clicking the "View" link in one of the rows in the list of contacts. This will take them to the path
|
|
`/contact/<contact id>` (e.g. `/contacts/42`). Note that this is a common pattern in web development: Contacts are being
|
|
treated as resources and the URLs around these resources are organized in a coherent manner:
|
|
|
|
* If you wish to view all contacts, you issue a `GET` to `/contacts`
|
|
* If you wish to get a hypermedia representation allowing you to create a new contact, you issue a `GET` to `/contacts/new`
|
|
* If you wish to view a specific contacts (with, say, and id of `42), you issue a `GET` to `/contacts/42`
|
|
|
|
.Path Design In Your HDA
|
|
****
|
|
It is easy to quibble about what particular path scheme you should use ("Should we `POST` to `/contacts/new` or to `/contacts`?")
|
|
and we have seen _lots_ of arguments about one approach versus another. I feel it is more important to understand
|
|
the overarching idea of resources and the hypermedia representations of them, rather than hairsplitting about path
|
|
layouts: just pick a reasonable schema you like and stay consistent.
|
|
****
|
|
|
|
Our handler logic for this route is going to be _very_ simple: we just look the Contact up by id, embedded in the path
|
|
of the URL for the route. To extract this ID we are going to need to introduce a final bit of Flack functionality: the
|
|
ability to call out pieces of a path and have them automatically extracted and then passed in to a handler function.
|
|
|
|
Here is what the code looks like, just a few lines of simple Python:
|
|
|
|
[source,python]
|
|
----
|
|
@app.route("/contacts/<contact_id>") <1>
|
|
def contacts_view(contact_id=0): <2>
|
|
contact = Contact.find(contact_id) <3>
|
|
return render_template("show.html", contact=contact) <4>
|
|
----
|
|
<1> Map the path, with a path variable named `contact_id`
|
|
<2> The handler takes the value of this path parameters
|
|
<3> Look up the corresponding contact
|
|
<4> Render the `show.html` template
|
|
|
|
You can see the syntax for extracting values from the path in the first line of code, you enclose the part of the
|
|
path you wish to extract in `<>` and give it a name. This component of the path will be extracted and then passed
|
|
into the handler function, via the parameter with the same name. So, if you were to navigate to the path `/contacts/42`
|
|
then the value `42` would be passed into the `contacts_view()` function for the value of `contact_id`.
|
|
|
|
Once we have the id of the contact we want to look up, we load it up using the `find` method on the `Contact` object. We
|
|
then pass this contact into the `show.html` template and render a response.
|
|
|
|
=== Viewing The Details Of A Contact
|
|
|
|
Our `show.html` template is relatively simple, just showing the same information as the table but in a slightly different
|
|
format (perhaps for printing.) If we add functionality like "notes" to the application later on, however, this will give
|
|
us a good place to show them.
|
|
|
|
Again, I will omit the "chrome" and focus on the meat of the template:
|
|
|
|
[source, html]
|
|
----
|
|
<h1>{{contact.first}} {{contact.last}}</h1>
|
|
|
|
<div>
|
|
<div>Phone: {{contact.phone}}</div>
|
|
<div>Email: {{contact.email}}</div>
|
|
</div>
|
|
|
|
<p>
|
|
<a href="/contacts/{{contact.id}}/edit">Edit</a>
|
|
<a href="/contacts">Back</a>
|
|
</p>
|
|
----
|
|
|
|
We simply render a nice First Name abd Last Name header with the additional contact information as well as a link to
|
|
edit it or to navigate back to the list of contacts. Simple but effective hypermedia!
|
|
|
|
=== Editing And Deleting A Contact
|
|
|
|
Editing a contact is going to look very similar to creating a new contact. As with adding a new contact, we are going
|
|
to need two routes that handle the same path, but using different HTTP methods: a `GET` to `/contacts/<contact_id>/edit`
|
|
will return a form allowing you to edit the contact with that ID and the `POST` will update it.
|
|
|
|
We will also piggyback the ability to delete a contact along with this editing functionality. To do this we will need to
|
|
handle a `POST` to `/contacts/<contact_id>/delete`.
|
|
|
|
Let's look at the code to handle the `GET`, which, again, will return an HTML representation of an editing interface
|
|
for the given resource:
|
|
|
|
[source, python]
|
|
----
|
|
@app.route("/contacts/<contact_id>/edit", methods=["GET"])
|
|
def contacts_edit_get(contact_id=0):
|
|
contact = Contact.find(contact_id)
|
|
return render_template("edit.html", contact=contact)
|
|
----
|
|
|
|
As you can see this looks an awful lot like our "Show Contact" functionality. In fact, it is nearly identical except
|
|
for the template that we render: here we render `edit.html` rather than `show.html`! There's that simplicity we
|
|
talked about again!
|
|
|
|
While our handler code looked similar to the "Show Contact" functionality, our template is going to look very similar to
|
|
the template for the "New Contact" functionality: we are going to have a form that submits values to the same URL
|
|
used to `GET` the form (see what I did there?) and that presents all the fields of a contact as inputs, along with
|
|
any error messages (we will even reuse the same Post-Redirect-Get trick!)
|
|
|
|
Here is the first bit of the form:
|
|
|
|
[source, html]
|
|
----
|
|
<form action="/contacts/{{ contact.id }}/edit" method="post"> <1>
|
|
<fieldset>
|
|
<legend>Contact Values</legend>
|
|
<p>
|
|
<label for="email">Email</label>
|
|
<input name="email" id="email" type="text" placeholder="Email" value="{{ contact.email }}"> <2>
|
|
<span class="error">{{ contact.errors['email'] }}</span>
|
|
</p>
|
|
----
|
|
<1> We issue a `POST` to the `/contacts/{{ contact.id }}/edit` path
|
|
<2> As with the `new.html` page, we have an input tied to the contact's properties
|
|
|
|
This HTML is nearly identical to our `new.html` form, except that this form is going to submit a `POST` to a different
|
|
path, based on the id of the contact that we want to update. It's worth nothing here that, rather than `POST`, I would
|
|
prefer to use a `PUT` or `PATCH`, but those are not available in plain HTML.
|
|
|
|
Following this we have the remainder of our form, again very similar to the `new.html` template, and our submit button
|
|
to submit the form.
|
|
|
|
[source, html]
|
|
----
|
|
<p>
|
|
<label for="first_name">First Name</label>
|
|
<input name="first_name" id="first_name" type="text" placeholder="First Name"
|
|
value="{{ contact.first }}">
|
|
<span class="error">{{ contact.errors['first'] }}</span>
|
|
</p>
|
|
<p>
|
|
<label for="last_name">Last Name</label>
|
|
<input name="last_name" id="last_name" type="text" placeholder="Last Name"
|
|
value="{{ contact.last }}">
|
|
<span class="error">{{ contact.errors['last'] }}</span>
|
|
</p>
|
|
<p>
|
|
<label for="phone">Phone</label>
|
|
<input name="phone" id="phone" type="text" placeholder="Phone" value="{{ contact.phone }}">
|
|
<span class="error">{{ contact.errors['phone'] }}</span>
|
|
</p>
|
|
<button>Save</button>
|
|
</fieldset>
|
|
</form>
|
|
----
|
|
|
|
In the final part of our template we have a small difference between the `new.html` and `edit.html`. Below the main
|
|
editing form, we include a second form that allows you to delete a contact. It does this by issuing a `POST`
|
|
to the `/contacts/<contact id>/delete` path. As with being able to use a `PUT` to update a contact, it sure would be
|
|
nice if we could issue a `DELETE` request to delete one, but unfortunately that also isn't possible in plain HTML!
|
|
|
|
Finally, there is a simple hyperlink back to the list of contacts.
|
|
|
|
[source, html]
|
|
----
|
|
<form action="/contacts/{{ contact.id }}/delete" method="post">
|
|
<button>Delete Contact</button>
|
|
</form>
|
|
|
|
<p>
|
|
<a href="/contacts/">Back</a>
|
|
</p>
|
|
----
|
|
|
|
Given all the similarities between the `new.html` and `edit.html` templates, you may be wondering why we are not
|
|
_refactoring_ these two templates to share logic between them. That's a great observation and, in a production system,
|
|
we would probably do just that. For our purposes, however, since our application is small and simple, we will leave the
|
|
templates separate.
|
|
|
|
.Factoring Your Applications
|
|
****
|
|
One thing that often trips people up who are coming to hypermedia applications from a JavaScript background is the
|
|
notion of "components". In JavaScript-oriented applications it is common to break your app up into small
|
|
client-side components that are then composed together. These components are often developed and tested in isolation and
|
|
provide a nice abstraction for developers to create testable code.
|
|
|
|
With Hypermedia Driven Applications, in contrast, you factor your application on the server side. As we said, the above form could be
|
|
refactored into a shared template between the edit and create templates, allowing you to achieve a reusable and DRY (Don't
|
|
Repeat Yourself) implementation.
|
|
|
|
Note that factoring on the server side tends to be coarser-grained than on the client side: you tend to split out common
|
|
_sections_ rather than create lots of individual components. This has both benefits (it tends to be simple) as well as
|
|
drawbacks (it is not nearly as isolated as client-side components) .
|
|
|
|
Overall, however, a properly factored server-side hypermedia application can be extremely DRY!
|
|
****
|
|
|
|
==== Handling The Post to `/contacts/<contact_id>`
|
|
|
|
Next we need to handle the HTTP `POST` request that the form in our `edit.html` template submits. We will declare
|
|
another route that handles the path as the `GET` above.
|
|
|
|
Here is our Python code:
|
|
|
|
[source, python]
|
|
----
|
|
@app.route("/contacts/<contact_id>/edit", methods=["POST"]) <1>
|
|
def contacts_edit_post(contact_id=0):
|
|
c = Contact.find(contact_id) <2>
|
|
c.update(request.form['first_name'], request.form['last_name'], request.form['phone'], request.form['email']) <3>
|
|
if c.save(): <4>
|
|
flash("Updated Contact!")
|
|
return redirect("/contacts/" + str(contact_id)) <5>
|
|
else:
|
|
return render_template("edit.html", contact=c) <6>
|
|
----
|
|
<1> Handle a `POST` to `/contacts/<contact_id>/edit`
|
|
<2> Look the contact up by id
|
|
<3> update the contact with the new information from the form
|
|
<4> Attempt to save it
|
|
<5> If successful, flash a success message and redirect to the show page for the contact
|
|
<6> If not successful, rerender the edit template, showing any errors.
|
|
|
|
The logic in this handler is very similar to the logic in the handler for adding a new contact. The only real difference
|
|
is that, rather than creating a new Contact, we look up a contact by id and then call the `update()` method on it with
|
|
the values that were entered in the form.
|
|
|
|
Once again, this consistency between our CRUD operations is one of the nice, simplifying aspects of traditional CRUD web
|
|
applications!
|
|
|
|
=== Deleting A Contact
|
|
|
|
We piggybacked delete functionality into the same template used to edit a contact. That form will issue an HTTP `POST`
|
|
to `/contacts/<contact_id>/delete` that we will need to handle and delete the contact in question.
|
|
|
|
Here is what the controller looks like
|
|
|
|
[source, python]
|
|
----
|
|
@app.route("/contacts/<contact_id>/delete", methods=["POST"]) <1>
|
|
def contacts_delete(contact_id=0):
|
|
contact = Contact.find(contact_id)
|
|
contact.delete() <2>
|
|
flash("Deleted Contact!")
|
|
return redirect("/contacts") <3>
|
|
----
|
|
<1> Handle a `POST` the `/contacts/<contact_id>/delete` path
|
|
<2> Look up and then invoke the `delete()` method on the contact
|
|
<3> Flash a success message and redirect to the main list of contacts
|
|
|
|
The handler code is very simple since we don't need to do any validation or conditional logic: we simply look up the
|
|
contact the same way we have been doing in our other handlers and invoke the `delete()` method on it, then redirect
|
|
back to the list of contacts with a success flash message.
|
|
|
|
No need for a template in this case!
|
|
|
|
=== Contact.App... Implemented!
|
|
|
|
Believe it or not, that's our entire contact application! Hopefully the Flask and Jinja2 code is simple enough that
|
|
you were able to follow along easily, even if Python isn't your preferred language or Flask isn't your preferred web
|
|
application framework. Again, I don't expect you to be a Python or Flask expert (I'm certainly
|
|
not!) and you shouldn't need more than a basic understanding of how they work for the remainder of the book.
|
|
|
|
Now, admittedly, this isn't a large or sophisticated application, but it does demonstrate many of the aspects of
|
|
traditional, web 1.0 applications: CRUD, the Post/Redirect/Get pattern, working
|
|
with domain logic in a controller, organizing our URLs in a coherent, resource-oriented manner.
|
|
|
|
And, furthermore, this is a deeply _hypermedia-based_ web application. Without even thinking about it (or maybe even understanding
|
|
it!) we have been using REST, HATEOAS and all the other hypermedia concepts. I would bet that this simple little app
|
|
we have built is more REST-ful than 99% of all JSON APIs ever built, and it was all effortless: just by virtue of using
|
|
a _hypermedia_, HTML, we naturally fall into the REST-ful network architecture.
|
|
|
|
So that's great. But what's the matter with this little web app? Why not end here and go off to develop the old web 1.0 style
|
|
applications people used to build?
|
|
|
|
Well, at some level, nothing is wrong with it. Particularly for an application that is as simple as this one it, the older
|
|
way of building web apps may be a fine approach!
|
|
|
|
However, the application does suffer from that "clunkiness" that we mentioned earlier when discussing web 1.0 applications:
|
|
every request replaces the entire screen, introducing a noticeable flicker when navigating between pages. You lose your
|
|
scroll state. You have to click around a bit more than you might in a more sophisticated web application. Contact.App,
|
|
at this point, just doesn't feel like a "modern" web application, does it?
|
|
|
|
Well. Are we going to have to adopt JavaScript after all? Should we pitch this hypermedia approach in the bin, install
|
|
NPM and start pulling down thousands of JavaScript dependencies, and rebuild the application using a "modern" JavaScript
|
|
library like React?
|
|
|
|
Well, I wouldn't be writing this book if that were the case, now would I?!
|
|
|
|
No, I wouldn't. It turns out that we can improve the user experience of this application _without_ abandoning the
|
|
hypermedia architecture. One way this can be accomplished is to introduce htmx, a small JavaScript library that
|
|
eXtends HTML (hence, htmx), to our application. In the next few chapters we will take a look at this library and how
|
|
it can be used to build surprisingly interactive user experiences, all within the original hypermedia architecture of
|
|
the web.
|
|
|
|
== Summary
|
|
|
|
* A Hypermedia Driven Application is an application that primarily relies on exchanging hypermedia with a server
|
|
for achieving interactivity
|
|
* Remember that Web 1.0 applications are, by definition, Hypermedia Driven Applications!
|
|
* Flask is a simple Python library for connecting routes to server-side logic, or handlers, and provides a
|
|
good foundation for building simple Hypermedia Driven Applications
|
|
* Jinja2 is the default templating library used for Server Side Rendering in Flask, it is a fairly standard
|
|
templating library
|
|
* Combining Flask and Jinja templates allowed us to implement a basic CRUD-style Web 1.0 application for managing
|
|
contacts in a basic web 1.0-style application with relatively little effort
|
|
* This web 1.0-style application works fine, but it feels a bit clunky and old fashioned. We'll look into how to
|
|
fix that problem, while still using hypermedia, next! |