Validate a form on both sides
Use semantic form controls with labels and meaningful names. Add Zotonic validators for immediate feedback, then validate the submitted values again at the server boundary.
Test an empty form, invalid input, and a valid submission. Verify that a failed submission leaves the entered values available and explains which field needs attention.
Treat repeated submissions as a normal possibility. A disabled submit button helps interaction but cannot guarantee a write happens only once. Enforce any uniqueness or retry rules in the operation itself.
Start with a named, required email field:
<form id="garden-contact" method="post" action="postback">
<label for="garden-email">{_ Email _}</label>
<input id="garden-email" name="email" type="email">
{% validate id="garden-email" name="email" type={presence} type={email} %}
<button type="submit">{_ Check address _}</button>
</form>
{% wire id="garden-contact" type="submit" postback={check_email} delegate=`mod_garden` %}
Add this clause to the exported event/2 in mod_garden, using the header and declarations from the postback task. Broaden its spec to accept #postback{} | #submit{} and separate clauses with a semicolon:
event(#submit{message = {check_email, []}}, Context) ->
case z_context:get_q_validated(<<"email">>, Context) of
Email when is_binary(Email), Email =/= <<>> ->
z_render:growl(?__("Address checked; nothing was sent.", Context), Context);
_ ->
z_render:growl_error(?__("Enter an email address.", Context), Context)
end.
Zotonic's submit pipeline runs the attached validators before calling the handler. This example checks input and shows feedback; it does not store an address or send mail. Do not treat validation as permission to subscribe or email someone. Check missing, malformed, and valid addresses, and also reject invalid input in any API that calls the same domain operation.
Handle a postback on the server
Use the active Garden module as the delegate. Add this header, export, and callback to mod_garden.erl, merging with declarations and event clauses already present:
-include_lib("zotonic_core/include/zotonic.hrl").
-export([event/2]).
-spec event(Event, Context) -> z:context()
when Event :: #postback{}, Context :: z:context().
event(#postback{message = {garden_hello, []}}, Context) ->
z_render:growl(?__("Welcome to the garden", Context), Context).
In a page with the standard JavaScript includes, mod_wires, and a final {% script %}:
<button id="garden-hello" type="button">{_ Say hello _}</button>
{% wire id="garden-hello" postback={garden_hello} delegate=`mod_garden` %}
Compile and load the module, reload the page, and click the button. Expect the greeting notification. The returned context contains the queued browser response; returning the original context after another response call can lose that response.
This public example changes no stored data. For a write, read only supported binary query keys, validate input, and authorize with the supplied context before calling a model. A signed postback does not make arbitrary form values trustworthy. A form submit delivers #submit{}, not #postback{}.