To add new RSS feeds, we need a form!
Let's start by creating a partial for it:
templates/partials/add_feed.html<form class="flex flex-col items-center" action="/add_feed" method="POST">
<div>
<label for="url">URL</label>
<input type="text" id="url" name="url" class="bg-slate-100 p-2 mb-2">
</div>
<div>
<label for="title">Title</label>
<input type="text" id="title" name="title" class="bg-slate-100 p-2 mb-2">
</div>
<div>
<label for="showImages">Show Images</label>
<input type="checkbox"
id="showImages"
name="showImages"
class="bg-slate-100 p-2 mb-2">
</div>
<input type="submit"
value="Add"
class="bg-slate-600 text-white font-semibold rounded-md px-4 py-2">
</form>
Then we can either:
- Show the form by calling an endpoint that returns its HTML; or
- Show the form using JavaScript 😱 (don't worry, AlpineJS is very minimal)
Adding feeds using HTMX
We'll begin by adding two endpoints to our Flask app: one for rendering the form, and one for dealing with the form submission:
__init__.py |
---|
| import feedparser
import jinja_partials
from flask import Flask, abort, redirect, render_template, request, url_for
feeds = {
"https://blog.teclado.com/rss/": {"title": "The Teclado Blog", "href": "https://blog.teclado.com/rss/", "show_images": True, "entries": {}},
"https://www.joshwcomeau.com/rss.xml": {"title": "Josh W. Comeau", "href": "https://www.joshwcomeau.com/rss.xml", "show_images": False, "entries": {}},
}
def create_app():
app = Flask(__name__)
jinja_partials.register_extensions(app)
@app.route("/feed/")
@app.route("/feed/<path:feed_url>")
def render_feed(feed_url: str = None):
for url, feed_ in feeds.items():
parsed_feed = feedparser.parse(url)
for entry in parsed_feed.entries:
if entry.link not in feed_["entries"]:
feed_["entries"][entry.link] = entry
if feed_url is None:
feed = list(feeds.values())[0]
else:
feed = feeds[feed_url]
return render_template("feed.html", feed=feed, feeds=feeds)
@app.route("/entries/<path:feed_url>")
def render_feed_entries(feed_url: str):
try:
feed = feeds[feed_url]
except KeyError:
abort(400)
page = int(request.args.get("page", 0))
# Below we're paginating the entries even though
# in this application it's not necessary, just to
# show what it might look like if it were.
return render_template(
"partials/entry_page.html",
entries=list(feed["entries"].values())[page*5:page*5+5],
href=feed["href"],
page=page,
max_page=len(feed["entries"])//5
)
@app.route("/add_feed", methods=["POST"])
def add_feed():
feed = request.form.get("url")
title = request.form.get("title")
show_images = request.form.get("showImages")
feeds[feed] = {"title": title, "href": feed, "show_images": show_images, "entries": {}}
return redirect(url_for("render_feed", feed=feed))
@app.route("/render_add_feed")
def render_add_feed():
return render_template("partials/add_feed.html")
return app
|
Then we'll add a button in our feed.html
template that, when clicked, calls the /render_add_feed
endpoint and replaces the HTML returned into the current page:
templates/feed.html |
---|
| {% extends "base.html" %}
{% block content %}
<div class="hidden lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:w-72 lg:flex-col">
<div class="flex grow flex-col gap-y-5 overflow-y-auto bg-white px-6 py-4">
<nav class="flex flex-1 flex-col">
<ul role="list" class="flex flex-1 flex-col gap-y-3">
{% for feed_url, feed_ in feeds.items() %}
<li>
<a href="{{ url_for('render_feed', feed_url=feed_.href) }}"
hx-boost
class="{{ 'bg-green-700 text-white' if feed.href == feed_['href'] else '' }} group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold">
{{ feed_['title'] }}
</a>
</li>
{% endfor %}
</ul>
</nav>
<div class="flex justify-center items-center">
<button hx-get="/render_add_feed"
hx-swap="outerHTML"
class="bg-slate-600 text-white font-semibold rounded-md px-4 py-2">Add</button>
</div>
</div>
</div>
<div class="pl-72">
<main class="py-10">
<div class="px-4 sm:px-6 lg:px-8"
id="entries"
hx-get="{{ url_for('render_feed_entries', feed_url=feed.href, page=0) }}"
hx-swap="afterbegin"
hx-target="this"
hx-trigger="load"></div>
</main>
</div>
{% endblock content %}
|
That's it! Doesn't get much easier than that!
Adding feeds with AlpineJS
If you want to be able to hide the form once you've shown it, then you start needing JavaScript. AlpineJS offers a very minimalistic API that complements HTMX very well.
Let's bring it into our base template:
templates/base.html |
---|
| <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Feed reader</title>
<script src="https://cdn.tailwindcss.com?plugins=typography,aspect-ratio"></script>
<script src="https://unpkg.com/htmx.org@1.9.9"></script>
<script src="https://unpkg.com/alpinejs" defer></script>
</head>
<body>
{% block content %}
{% endblock content %}
</body>
</html>
|
Then let's change our feed.html
so it uses AlpineJS:
templates/feed.html |
---|
| {% extends "base.html" %}
{% block content %}
<div class="hidden lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:w-72 lg:flex-col">
<div class="flex grow flex-col gap-y-5 overflow-y-auto bg-white px-6 py-4">
<nav class="flex flex-1 flex-col">
<ul role="list" class="flex flex-1 flex-col gap-y-3">
{% for feed_url, feed_ in feeds.items() %}
<li>
<a href="{{ url_for('render_feed', feed_url=feed_.href) }}"
hx-boost
class="{{ 'bg-green-700 text-white' if feed.href == feed_['href'] else '' }} group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold">
{{ feed_['title'] }}
</a>
</li>
{% endfor %}
</ul>
</nav>
<div class="flex flex-col justify-center items-center"
x-data="{show: false}">
<!-- The below doesn't need to be a partial anymore because we're never rendering it in a Flask endpoint -->
<div x-show="show">{{ render_partial("partials/add_feed.html") }}</div>
<button x-on:click="show = !show"
class="bg-slate-600 text-white font-semibold rounded-md px-4 py-2"
x-text="show ? 'Hide' : 'Show'">Show</button>
</div>
</div>
</div>
<div class="pl-72">
<main class="py-10">
<div class="px-4 sm:px-6 lg:px-8"
id="entries"
hx-get="{{ url_for('render_feed_entries', feed_url=feed.href, page=0) }}"
hx-swap="afterbegin"
hx-target="this"
hx-trigger="load"></div>
</main>
</div>
{% endblock content %}
|
Here we have the following:
x-data="{show: false}"
on the button parent.
- The first child
div
contains our form, but it's hidden to begin with because x-show="show"
will hide it (since show: false
).
- Then there's a button that toggles
show
with x-on:click="show = !show
.
- The button text changes depending on the
show
variable, and lets us hide or show the form.
If we use Alpine, we can get rid of the /render_add_feed
template, so that's a small bonus:
__init__.py-
- @app.route("/render_add_feed")
- def render_add_feed():
- return render_template("partials/add_feed.html")
We could also move add_feed.html
outside of partials
(since we're no longer rendering it on its own). We could make it a macro. Or we could put the HTML directly in the page. But keeping it as a partial is fine too!