Implement full form submission pipeline: textarea form data collection with CR+LF newline normalization, submit button value inclusion via submitter tracking (Option<NodeId>), multipart/form-data and text/plain encoding support, configurable content_type threading through navigate_post → NetworkStack → HttpLoader, and cancelable submit event dispatch with preventDefault support at all three trigger sites. Includes review fixes for UTF-8 safety in newline normalization, RFC 7578 field name escaping in multipart encoding, and boundary collision avoidance. 15 unit tests, 3 integration tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
17 KiB
Story 4.4: Form Submission
Status: done
Story
As a web user, I want forms to submit my data to the server, so that I can log in, search, and interact with web applications.
Acceptance Criteria
-
GET form submission: A
<form method="GET">with filled controls serializes data asapplication/x-www-form-urlencodedand appends it to the action URL as a query string, then navigates to the resulting URL. Per HTML §4.10.21.3. -
POST form submission: A
<form method="POST">with filled controls serializes data asapplication/x-www-form-urlencodedin the request body, then navigates to the action URL with the POST body. Per HTML §4.10.21.3. -
Multipart/form-data encoding: A
<form enctype="multipart/form-data" method="POST">serializes data using multipart MIME format with correct boundary delimiters per RFC 2388. Each field becomes a MIME part withContent-Disposition: form-data; name="..."headers. -
Excluded form controls: Disabled controls, controls without
nameattributes, unchecked checkboxes/radios, and buttons that are not the submitter are excluded from submitted data. Per HTML §4.10.21.3. -
Form action & method attributes: The
actionattribute determines the submission URL (defaults to current page URL). Themethodattribute determines GET or POST (defaults to GET, case-insensitive). Theenctypeattribute determines encoding (defaults toapplication/x-www-form-urlencoded). Per HTML §4.10.6. -
Textarea form submission: A
<textarea name="...">element includes its runtime text content (frominput_states) in form data. Newlines are normalized to CR+LF per HTML §4.10.11. -
Submit button value inclusion: When a specific submit button triggers submission, its
name/valuepair is included in the form data. Other submit buttons in the same form are excluded. Per HTML §4.10.21.3. -
Submit event dispatch: Before actual submission, a
submitevent (cancelable, bubbles) is dispatched on the form element. Ifevent.preventDefault()is called, submission is cancelled. Per HTML §4.10.21.3. -
Integration tests verify each submission method and encoding, golden tests are not affected (form submission is runtime behavior), and
just cipasses.
What NOT to Implement
- No file input support --
<input type="file">is deferred; multipart encoding only handles text fields in this story. - No
<input type="image">coordinate submission -- image inputs deferred. - No
formaction/formmethod/formenctypeoverrides on buttons -- deferred to a future story. - No
targetattribute support -- all submissions navigate in the current browsing context. - No form validation before submission -- that's Story 4.5. This story submits unconditionally (except submit event cancellation).
- No
<input type="hidden">special handling -- hidden inputs already work as text inputs with name/value.
Tasks / Subtasks
-
Task 1: Add textarea to form data collection (AC: #6)
- 1.1 In
form.rscollect_form_data_recursive(), add a"textarea"branch: read runtime text frominput_states(falling back to DOMtext_content()), addname=valueto form data - 1.2 Normalize textarea newlines to CR+LF (
\r\n) before encoding per HTML §4.10.11 - 1.3 Add unit tests in
form_tests.rs: textarea with runtime value, textarea with DOM-only value, textarea without name (excluded), disabled textarea (excluded)
- 1.1 In
-
Task 2: Add submit button value to form data (AC: #7)
- 2.1 Add
submitter: Option<NodeId>parameter tosubmit_form()andcollect_form_data()— the NodeId of the button/input that triggered submission - 2.2 In
collect_form_data_recursive(), when encounteringtype="submit"input or<button type="submit">: include itsname/valueONLY if it matches the submitter NodeId - 2.3 Thread
submitterfrom click handlers inevent_handler.rsthrough tosubmit_form() - 2.4 For Enter-key submission, submitter is
None(no button value included) — matches spec behavior - 2.5 Add unit tests: submit button value included when it's the submitter, excluded when it's not the submitter, excluded when submitter is None
- 2.1 Add
-
Task 3: Implement multipart/form-data encoding (AC: #3)
- 3.1 Add
enctypeattribute reading insubmit_form(): parseenctypefrom form element (defaultapplication/x-www-form-urlencoded, also supportmultipart/form-dataandtext/plain) - 3.2 Create
encode_multipart_form_data()function inform.rs: generate a random boundary string, encode each name=value pair as a MIME part withContent-Disposition: form-data; name="..."header, separated by--boundarylines - 3.3 Update
FormSubmission::Postto includecontent_type: Stringfield (instead of hardcodedapplication/x-www-form-urlencoded) - 3.4 Update
navigate_post()inapp_state.rsandpost_form()innetto accept and use the content type from FormSubmission - 3.5 In
http_loader.rspost_form_with_headers(), use the provided content type instead of hardcodedapplication/x-www-form-urlencoded - 3.6 Add unit tests: multipart encoding format correctness (boundary, headers, body parts),
text/plainencoding (simple name=value lines)
- 3.1 Add
-
Task 4: Dispatch submit event before submission (AC: #8)
- 4.1 In
event_handler.rs, before callingsubmit_form(), dispatch asubmitevent on the form element:{type: "submit", bubbles: true, cancelable: true} - 4.2 Use the existing event dispatch infrastructure (
dispatch_dom_eventor similar) — check howchangeevent is dispatched in Story 4.1/4.3 for the pattern - 4.3 If the submit event's
defaultPreventedis true after dispatch, skip the form submission entirely - 4.4 Add integration tests in
js_events.rs: submit event fires, submit event bubbles, submit event can be cancelled via preventDefault
- 4.1 In
-
Task 5: Wire everything together and update navigation (AC: #1, #2, #5)
- 5.1 Update all three form submission trigger sites in
event_handler.rs(input[type=submit], button[type=submit], Enter key) to: dispatch submit event first (Task 4), pass submitter NodeId (Task 2), then callsubmit_form() - 5.2 Update
navigate_post()inapp_state.rsto accept content_type parameter and pass it through toNetworkStack::post_form() - 5.3 Update
NetworkStack::post_form()to accept and use content_type parameter - 5.4 Verify GET submission still appends query string correctly (existing behavior)
- 5.5 Verify POST submission still sends body correctly (existing behavior)
- 5.1 Update all three form submission trigger sites in
-
Task 6: Integration tests and CI (AC: #9)
- 6.1 Add integration test: GET form submission with mixed controls (text, checkbox, radio, select, textarea) produces correct query string
- 6.2 Add integration test: POST form submission produces correct body
- 6.3 Add integration test: disabled controls and nameless controls excluded
- 6.4 Add integration test: submit button value included only for submitter
- 6.5 Verify all existing form tests pass (no regressions)
- 6.6 Run
just ci— must pass - 6.7 Update
docs/HTML5_Implementation_Checklist.mdwith form submission support
Dev Notes
Existing Infrastructure (DO NOT REBUILD)
Form data collection already works (crates/app_browser/src/form.rs):
submit_form()(lines ~155-240): Readsmethod/actionfrom form, resolves URLs, collects data, encodes, returnsFormSubmission::Get{url}orFormSubmission::Post{url, body}collect_form_data_recursive()(lines ~53-119): Walks DOM tree, handles<input>(text/checkbox/radio),<select>(viacollect_select_data()). MISSING: textarea and submit button value.find_ancestor_form()(lines ~17-28): Walks up DOM to find parent<form>- URL encoding via
url::form_urlencoded::Serializeralready works
Navigation after submission already works (crates/app_browser/src/app_state.rs):
navigate(): For GET — parses URL, fetches viaNetworkStack::load_sync(), runs pipelinenavigate_post(): For POST — same flow but usesNetworkStack::post_form()- Both clear input/checked/select states, push history, trigger re-render
Network POST already works (crates/net/src/):
NetworkStack::post_form()→HttpLoader::post_form_with_headers(): Sends POST with body, handles redirects (302/303→GET, 307/308→POST), reads response- Currently hardcodes
Content-Type: application/x-www-form-urlencoded— must make configurable for multipart
Event dispatch exists (crates/app_browser/src/event_handler.rs):
dispatch_change_event_with_swap()in form.rs dispatches change events — follow same pattern for submit event- Three form submission trigger sites: input[type=submit] (~line 375), button[type=submit] (~line 449), Enter key (~line 1010)
Form submission tests exist (crates/app_browser/src/tests/form_tests.rs):
- 23 existing tests covering GET/POST, URL resolution, encoding, checkbox/radio/select inclusion/exclusion
- No textarea tests, no submit button value tests, no multipart tests, no submit event tests
What Needs to Be Built
-
Textarea in form data: Add
"textarea"branch tocollect_form_data_recursive(). Read frominput_states(runtime value) with DOMtext_content()fallback. Normalize newlines to CR+LF. -
Submit button value: Add
submitter: Option<NodeId>parameter threading from click handlers throughsubmit_form()→collect_form_data()→collect_form_data_recursive(). Include submit button name/value only when it matches the submitter. -
Multipart encoding: New
encode_multipart_form_data()function. UpdateFormSubmission::Postto carrycontent_type. Thread content type throughnavigate_post()→NetworkStack::post_form()→HttpLoader::post_form_with_headers(). -
Submit event: Dispatch cancelable
submitevent on form element before submission. If cancelled, skip submission. Use existing event dispatch pattern from change events.
Architecture Compliance
| Rule | How This Story Complies |
|---|---|
| Layer boundaries | All form logic in app_browser (Layer 3). Network in net (Layer 1). No upward dependencies. |
| Unsafe policy | No unsafe code needed. |
| Pipeline sequence | Form submission is runtime behavior, doesn't affect style/layout/paint pipeline. |
| Arena ID pattern | Uses existing NodeId for submitter tracking. No new ID types. |
| Existing patterns | Follows checked_states/select_states patterns for runtime state. Follows dispatch_change_event pattern for submit event. |
File Modification Plan
| File | Change |
|---|---|
crates/app_browser/src/form.rs |
Add textarea branch in collect_form_data_recursive(), add submitter param, add encode_multipart_form_data(), read enctype attribute, update FormSubmission::Post to include content_type |
crates/app_browser/src/event_handler.rs |
Dispatch submit event before submission at all 3 trigger sites, pass submitter NodeId through to submit_form() |
crates/app_browser/src/app_state.rs |
Update navigate_post() to accept and pass content_type |
crates/net/src/lib.rs |
Update post_form() signature to accept content_type |
crates/net/src/http_loader.rs |
Update post_form_with_headers() to use provided content_type instead of hardcoded value |
crates/app_browser/src/tests/form_tests.rs |
Add textarea tests, submit button value tests, multipart encoding tests |
tests/js_events.rs |
Add submit event integration tests (fires, bubbles, preventDefault cancels) |
docs/HTML5_Implementation_Checklist.md |
Update form submission status |
Testing Strategy
- Unit tests (in
form_tests.rs): Textarea collection, submit button value inclusion/exclusion, multipart encoding format, text/plain encoding, enctype parsing - Integration tests (in
js_events.rs): Submit event dispatch, cancellation, bubble. Full form submission with mixed control types. - Existing tests: All 23 existing form tests must pass without modification (except adding new
submitterparam withNonedefault) - No golden tests needed: Form submission is runtime behavior, not visual rendering
Previous Story Intelligence
Story 4.3 established patterns this story MUST follow:
- Parameter threading: When adding new params to
submit_form(), all callers must be updated (3 trigger sites in event_handler.rs + all test calls in form_tests.rs) - Runtime state over DOM: Always prefer
input_states/checked_states/select_statesover DOM attributes - Event dispatch pattern:
dispatch_change_event_with_swap()shows how to dispatch events with proper bubbling just ciafter every task: Don't batch — verify incrementally- Deferred items are OK: Mark tasks as deferred with clear rationale rather than implementing half-baked solutions
- Review found H2/H3 bugs: Invalidation and state threading through fast paths — when adding content_type to navigation, ensure it reaches all code paths
Git Intelligence
Recent commits show stable codebase:
71f263fStory 4.3 complete (select menus)f8e0c47Story 4.2 complete (buttons, checkboxes, radio)e9d3ffdCode review fixes for 4.164a3439Story 4.1 complete (text inputs, textareas)- No in-flight refactoring. Convention: descriptive commit messages without story numbers.
References
- [Source: crates/app_browser/src/form.rs#L155-L240] —
submit_form()main entry - [Source: crates/app_browser/src/form.rs#L53-L119] —
collect_form_data_recursive()(needs textarea + submitter) - [Source: crates/app_browser/src/event_handler.rs#L375-L401] — Input submit trigger site
- [Source: crates/app_browser/src/event_handler.rs#L449-L469] — Button submit trigger site
- [Source: crates/app_browser/src/event_handler.rs#L1010-L1032] — Enter key submit trigger site
- [Source: crates/net/src/http_loader.rs#L331-L456] —
post_form_with_headers()(hardcoded content type) - [Source: crates/app_browser/src/app_state.rs#L114-L142] —
navigate_post() - [HTML §4.10.21.3] — Form submission algorithm
- [HTML §4.10.11] — Textarea element (newline normalization)
- [RFC 2388] — Multipart form-data encoding
- [RFC 7231] — HTTP POST semantics
Dev Agent Record
Agent Model Used
Claude Opus 4.6 (1M context)
Debug Log References
No blocking issues encountered. All tasks completed in a single pass.
Completion Notes List
- Task 1: Added
collect_textarea_data()with runtime value (input_states) fallback to DOM text_content, CR+LF newline normalization vianormalize_newlines_crlf(). 5 unit tests. - Task 2: Added
submitter: Option<NodeId>parameter threaded throughsubmit_form()→collect_form_data()→collect_form_data_recursive(). Submit button (<input type="submit">and<button type="submit">) name/value included only when matching submitter. All 3 event_handler trigger sites updated. 4 unit tests. - Task 3: Added
enctypeattribute reading,encode_multipart_form_data()(RFC 2388 with boundary),encode_text_plain(). UpdatedFormSubmission::Postto carrycontent_type. Threaded content_type throughnavigate_post()→NetworkStack::post_form()→HttpLoader::post_form_with_headers(). Replaced hardcodedapplication/x-www-form-urlencoded. 3 unit tests. - Task 4: Added
dispatch_submit_event()toweb_apiandbrowser_runtime. Addeddispatch_submit_event_with_swap()helper in event_handler. Wired submit event dispatch before submission at all 3 trigger sites. IfdefaultPrevented, submission is skipped. 3 integration tests in js_events.rs. - Task 5: Already completed during Tasks 2-4 (all wiring done incrementally).
- Task 6: Added 3 mixed-control integration tests.
just cipasses. HTML5 checklist updated.
File List
crates/app_browser/src/form.rs— Added textarea collection, submitter param, multipart/text-plain encoding, enctype reading, content_type in FormSubmission::Postcrates/app_browser/src/event_handler.rs— Submit event dispatch before submission at all 3 trigger sites, submitter NodeId threading, dispatch_submit_event_with_swap helpercrates/app_browser/src/app_state.rs— navigate_post accepts content_type paramcrates/app_browser/src/tests/form_tests.rs— 15 new tests (textarea: 5, submit button: 4, multipart: 3, mixed-controls: 3)crates/net/src/lib.rs— post_form accepts content_type paramcrates/net/src/http_loader.rs— post_form/post_form_with_headers accept content_type, use it instead of hardcoded valuecrates/web_api/src/lib.rs— dispatch_submit_event (bubbles, cancelable)crates/browser_runtime/src/lib.rs— dispatch_submit_event wrapper returning default_preventedtests/js_events.rs— 3 submit event integration tests (fires, bubbles, preventDefault cancels)docs/HTML5_Implementation_Checklist.md— Updated form submission statustests/manual/js_test.html— Manual test page for form submission (debug artifact)
Change Log
- 2026-03-29: Implemented Story 4.4 Form Submission — textarea form data, submit button value (submitter tracking), multipart/form-data and text/plain encoding, submit event dispatch with preventDefault cancellation, content_type threading through navigation stack. 15 new unit tests, 3 integration tests. All ACs satisfied,
just cipasses.