Updating State (reactive toggle & edit)

Goal: Let users flip a Todo between active and completed and edit its text. You'll meet update, let, stronger param validation and conditional if.

Estimated time: 20 minutes.

« Part 2: Adding Data

1 What we're building

2 Extend templates/todos.pageql

2.1 Computed counts & flags


{%let active_count    = COUNT(*) from todos WHERE completed = 0%}
{%let completed_count = COUNT(*) from todos WHERE completed = 1%}
{%let total_count     = COUNT(*) from todos%}
{%let all_complete    = (:active_count == 0 AND :total_count > 0)%}

{%param edit_id type=integer optional%}
{%let active_count = COUNT(*) from todos WHERE completed = 0%} {%let completed_count = COUNT(*) from todos WHERE completed = 1%} {%let total_count = COUNT(*) from todos%} {%let all_complete = (:active_count == 0 AND :total_count > 0)%} {%param edit_id type=integer optional%}

The let directive calculates values that will be used throughout the template. Here we're computing counts of todos in various states (active, completed, total) and deriving a flag to indicate if all todos are complete. These variables are scoped to the current template or partial.

Note how the all_complete flag uses the colon syntax :variable to reference other variables in expressions. In PageQL, the colon prefix is required when using variables in SQL-like expressions to distinguish them from column names and prevent SQL injection. :active_count refers to the previously set variable.

The param directive declares and validates incoming request parameters. Here, edit_id is declared as an optional integer parameter that will be used to track which todo is being edited.

2.2 Toggle checkbox inside the list


<li {{if completed}}class="completed"{{/if}}>
<input hx-post="/todos/{{id}}/toggle" class="toggle" type="checkbox"
           {{if completed}}checked{{/if}}>
  <label hx-get="/?edit_id={{id}}">{{text}}</label>
</li>
<li {%if completed%}class="completed"{%end if%}> <input hx-post="/todos/{{id}}/toggle" class="toggle" type="checkbox" {%if completed%}checked{%end if%}> <label hx-get="/?edit_id={{id}}">{{text}}</label> </li>

This markup creates a list item for each todo with a checkbox for toggling completion status. Note how PageQL conditional syntax if completed is used twice:

  1. To conditionally add a CSS class when a todo is completed
  2. To check the checkbox for completed todos

In simple variable cases like {%if completed%}, PageQL allows omitting the colon prefix. Inside the from loop (not shown here), completed, id, and text become available as variables from the query results.

The hx-get attribute loads the same page with an edit_id parameter, triggering edit mode.

2.3 Edit mode (conditional)


{{if :edit_id == :id}}
  <li class="editing">
    <form hx-post="/todos/save" style="margin:0">
      <input type="hidden" name="id" value="{{id}}">
      <input class="edit" name="text" value="{{text}}" autofocus>
    </form>
  </li>
{{else}}
  …view version from 2.2…
{{/if}}
{%if :edit_id == :id%} <li class="editing"> <form hx-post="/todos/save" style="margin:0"> <input type="hidden" name="id" value="{{id}}"> <input class="edit" name="text" value="{{text}}" autofocus> </form> </li> {%else%} …view version from 2.2… {%end if%}

This conditional section demonstrates how PageQL handles more complex expressions. Notice that when comparing variables (:edit_id == :id), the colon prefix is required on both sides because we're using a complex expression rather than a simple variable check.

When a todo's ID matches the edit_id parameter, we show an edit form instead of the regular view. This creates an "inline editing" experience without requiring JavaScript for the basic functionality.

The else directive provides alternative content when the condition isn't met - in this case, showing the normal view mode from section 2.2.

2.4 Toggle-all checkbox & footer counts


<form hx-post="/todos/toggle_all" id="toggle-all-form" style="display:block">
  <input id="toggle-all" class="toggle-all" type="checkbox"
         {{if all_complete}}checked{{/if}}
         >
  <label for="toggle-all">Mark all as complete</label>
</form>

<span class="todo-count">
  <strong>{{active_count}}</strong>
  item{{if :active_count != 1}}s{{/if}} left
</span>
<form hx-post="/todos/toggle_all" id="toggle-all-form" style="display:block"> <input id="toggle-all" class="toggle-all" type="checkbox" {%if all_complete%}checked{%end if%}> <label for="toggle-all">Mark all as complete</label> </form> <span class="todo-count"> <strong>{{active_count}}</strong> item{%if :active_count != 1%}s{%end if%} left </span>

This section showcases how PageQL variables can be used to:

  1. Control UI state - the all_complete flag determines whether the toggle-all checkbox is checked
  2. Display dynamic counts - active_count shows how many todos remain active
  3. Control pluralization - adding "s" conditionally based on count (if :active_count != 1)

Notice that we need the colon syntax in :active_count != 1 because we're using a comparison operator, while the simple variable reference {{active_count}} doesn't need it when just outputting the value.

2.5 New public partials


{{partial post :id/toggle}}
  {{param id required type=integer min=1}}
  {{update todos set completed = 1 - completed WHERE id = :id}}
  {{redirect '/todos'}}
{{/partial}}

{{partial public save}}
  {{param id required type=integer min=1}}
  {{param text required minlength=1}}
  {{update todos set text = :text WHERE id = :id}}
  {{redirect '/todos'}}
{{/partial}}

{%partial public toggle_all%}
  {%let active_count COUNT(*) from todos WHERE completed = 0%}
  {%update todos set completed = IIF(:active_count = 0, 0, 1)%}
  {%redirect '/todos'%}
{%end partial%}
{%partial post :id/toggle%} {%param id required type=integer min=1%} {%update todos set completed = 1 - completed WHERE id = :id%} {%redirect '/todos'%} {%end partial%} {%partial public save%} {%param id required type=integer min=1%} {%param text required minlength=1%} {%update todos set text = :text WHERE id = :id%} {%redirect '/todos'%} {%end partial%} {%partial public toggle_all%} {%let active_count = COUNT(*) from todos WHERE completed = 0%} {%update todos set completed = IIF(:active_count = 0, 0, 1)%} {%redirect '/todos'%} {%end partial%}

These partials define server-side endpoints that handle data modifications. The public keyword exposes them directly via HTTP POST requests, making them accessible at paths like /todos/toggle.

Each partial follows a consistent pattern:

  1. Validate inputs: The param directive enforces type checking and validation rules
  2. Modify data: Using update to change database records
  3. Redirect: Send the user back to the main todo list

Note that toggle_all needs its own active_count variable inside its scope. In PageQL, each partial has its own variable scope if it is called as a page, so we must recalculate values needed within the partial rather than referencing those from the outer template.

The toggle partial uses a clever SQL trick completed = 1 - completed to flip between 0 and 1 without needing an if statement, since 1-0=1 and 1-1=0.

3 Walk-through

PiecePurpose
updateMutate an existing row (toggle, edit, bulk).
letCompute derived counts used in the UI.
if …Swaps markup between view and edit modes.
1 - completedA SQL trick to flip 0⇄1 with one statement.

3.1 Request cycle in 2 steps

  1. Browser: hx-post /todos/toggle
  2. Server: toggle partial updates row → list updates automatically.

POST /todos/toggle → UI updates automatically

4 Try it out

  1. Open http://localhost:8000/todos.
  2. Click a checkbox—row turns grey instantly without reloading.
  3. Double-click a label, edit text, hit Enter.
  4. Use the header checkbox to toggle all items on/off.

5 Recap

« Back: Part 2: Adding Data Next: Part 4: Deleting & Bulk Clear