Adding Filters (All • Active • Completed)
Goal: Finish our TodoMVC by letting users slice the list between All, Active, and Completed items. You'll declare one more query‑parameter, adjust the #from
query, and add a footer navigation that highlights the selected filter.
Estimated time: 10 minutes.
« Part 4: Deleting & Bulk Actions
1 New concepts
Concept | Purpose |
---|---|
#param … pattern="…" default | Declare and validate a query parameter (filter ) in GET requests. |
Context‑aware #if | Dynamically add the selected CSS class to the active link. |
SQL condition using bound params | Show only the rows that match the chosen filter. |
2 Declare the filter
parameter
Near the top of templates/todos.pageql, right after any existing #set
or #create
tags, insert:
{{#param filter default='all' pattern="^(all|active|completed)$" optional}}
{{#param filter default='all' pattern="^(all|active|completed)$" optional}}
optional
means the page also works without the query string.
3 Update the #from
loop
Replace the simple query with a conditional one:
{{#from todos WHERE (:filter == 'all') OR (:filter == 'active' AND completed = 0) OR (:filter == 'completed' AND completed = 1) ORDER BY id}} … list‑item markup … {{/from}}
{{#from todos WHERE (:filter == 'all') OR (:filter == 'active' AND completed = 0) OR (:filter == 'completed' AND completed = 1) ORDER BY id}} … list‑item markup … {{/from}}
The entire boolean expression is sent to SQLite; :filter
is safely bound.
4 Add the filter links in the footer
Inside the <footer class="footer">
block (just after the item‑count), add:
<div class="filters"> <a {{#if :filter == 'all'}}class="selected"{{/if}} href="/todos?filter=all">All</a> | <a {{#if :filter == 'active'}}class="selected"{{/if}} href="/todos?filter=active">Active</a> | <a {{#if :filter == 'completed'}}class="selected"{{/if}} href="/todos?filter=completed">Completed</a> </div>
<div class="filters"> <a {{#if :filter == 'all'}}class="selected"{{/if}} href="/todos?filter=all">All</a> | <a {{#if :filter == 'active'}}class="selected"{{/if}} href="/todos?filter=active">Active</a> | <a {{#if :filter == 'completed'}}class="selected"{{/if}} href="/todos?filter=completed">Completed</a> </div>
The CSS from TodoMVC highlights the link that carries class="selected"
.
5 Try it out
- Open http://localhost:8000/todos — All is selected by default.
- Click Active — only incomplete items remain.
- Tick a few checkboxes so they disappear from the Active view, then click Completed to confirm they moved.
- Manually tamper with the URL (e.g.
?filter=hack
) → PageQL blocks the request because it fails the regex pattern.
6 How it works
#param
validates early—before any SQL—so you never build an unsafe query.- The three‑way
WHERE
clause is still a single index‑friendly query. - Highlighting uses a tiny
#if
—no loops or enums required.
7 Wrap‑up
Congratulations 🎉 You have:
- Full CRUD (add, edit, toggle, delete)
- Bulk actions (toggle‑all, clear‑completed)
- Dynamic filtering — all without JavaScript beyond form submission helpers.
PageQL let us ship a reactive, database‑driven TodoMVC in ≈120 lines of HTML+SQL. From here you can explore:
- Component extraction (
#import
,#render
) to DRY up markup. - Deployment — serve with any WSGI host or container.
- Live query reactivity (alpha) to auto‑push DB changes to connected browsers.
Enjoy building with PageQL, and thanks for following the tutorial! 🎊