5: Voting

A poll without votes is a list. In this chapter every option becomes a form, a POST route receives the vote, the count goes up, and a redirect re-renders the page. Plain HTTP, no JavaScript yet: this version works in any browser, with everything off.

5.1: The vote forms

In poll.phlo, turn each button in the choices view into a tiny form:

view choices:
<section.card>
<foreach type_poll::records() AS $option>
    <form method=post action="/poll/vote/$option->id">
        <button>$option->option</button>
    </form>
</foreach>
</section>

Each form posts to its own URL: /poll/vote/1, /poll/vote/2, and so on. The id travels in the path, so the form needs no hidden fields. Note the quotes around the action value: attributes containing variables or slashes must be quoted. The form: display: inline rule from chapter 3 keeps the buttons on one row.

Reload http://localhost/poll: looks identical, but every button now submits a form. Clicking one gives a 404, because the route does not exist yet.

5.2: The POST route

Add the route to poll.phlo, near the GET route:

route POST poll vote $id {
    if (!$option = type_poll::record(id: (int)$id)) return false
    type_poll::change('id=?', (int)$id, votes: $option->votes + 1)
    location('/poll')
}

Read it top to bottom:

Reload and click Phlo. The page reloads, the count reads 1, and the bar jumps to 100%. Click another option and watch the percentages redistribute. Check data/poll.json to see the votes on disk.

5.3: Reading a payload instead

The id in the path is one style. The other is a request body, which you read through %payload. First enable the resource in data/app.json:

{
    "resources": [
        "DB/DB",
        "DB/model",
        "DB/JSONDB",
        "DB/JSON.result",
        "payload"
    ]
}

A payload-based version of the same route looks like this:

route POST poll vote @option {
    $id = (int)%payload->option
    if (!$option = type_poll::record(id: $id)) return false
    type_poll::change('id=?', $id, votes: $option->votes + 1)
    location('/poll')
}

@option validates the request body: the payload must contain exactly the key option, or the route does not match. Body keys are never bound as parameters; you read them via %payload->option. The matching form would be one form with <button name=option value="$option->id"> per choice.

Both styles are idiomatic. Keep the $id variant from X.2 for this tutorial; it transitions cleanly into the async version.

5.4: What you have now

Run the checks once more:

php www/app.php build::run
php www/app.php build::lint

Both settle at []. You have a complete, working poll: server-rendered, refresh-safe, zero JavaScript. Vote a few times, watch data/poll.json change, and notice the one thing that still feels 2004: the full page reload on every vote. That is the next chapter.

We use essential cookies to make this site work. With your permission we also use analytics to improve the site.