Merge pull request #2639 from lightpanda-io/form-no-validate

Validate form constraints on interactive submit; add HTMLFormElement.…
This commit is contained in:
Karl Seguin
2026-06-04 21:19:04 +08:00
committed by GitHub
4 changed files with 84 additions and 0 deletions

View File

@@ -4139,6 +4139,21 @@ pub fn submitForm(self: *Frame, submitter_: ?*Element, form_: ?*Element.Html.For
form._firing_submission_events = true;
defer form._firing_submission_events = false;
// Per the HTML "submit a form element" algorithm: unless the form (or the
// submitter, via formnovalidate) is in the no-validate state, interactively
// validate the form's constraints and abort submission if it fails.
// checkValidity() fires the `invalid` events on the offending controls.
// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#concept-form-submit
const skip_validation = form.getNoValidate() or blk: {
const s = submit_button orelse break :blk false;
if (s.is(Element.Html.Form.Input)) |input| break :blk input.getFormNoValidate();
if (s.is(Element.Html.Form.Button)) |button| break :blk button.getFormNoValidate();
break :blk false;
};
if (!skip_validation and !try form.checkValidity(self)) {
return;
}
// Per HTML spec "submit a form element" algorithm: SubmitEvent.submitter
// must be null when the submitter is the form itself, which is what
// Form.requestSubmit() passes when called with no submitter argument.

View File

@@ -13,6 +13,8 @@
<form id="empty-form"></form>
<form id="novalidate-form" novalidate></form>
<script id="surface">
{
const f = $('#valid-form');
@@ -46,6 +48,22 @@
}
</script>
<script id="no_validate_reflects_attribute">
{
// novalidate content attribute reflects to the noValidate IDL boolean
testing.expectEqual(true, $('#novalidate-form').noValidate);
testing.expectEqual(false, $('#valid-form').noValidate);
const f = $('#empty-form');
f.noValidate = true;
testing.expectEqual(true, f.noValidate);
testing.expectEqual('', f.getAttribute('novalidate'));
f.noValidate = false;
testing.expectEqual(false, f.noValidate);
testing.expectEqual(null, f.getAttribute('novalidate'));
}
</script>
<script id="report_validity_matches">
{
testing.expectEqual(false, $('#invalid-form').reportValidity());

View File

@@ -490,6 +490,44 @@
}
</script>
<!-- Test: interactive submission validates constraints unless no-validate is set -->
<form id="test_form_validate" action="/should-not-navigate-v" method="get">
<input type="text" name="q" required>
<button id="v_submit" type="submit">Go</button>
<button id="v_submit_novalidate" type="submit" formnovalidate>Go</button>
</form>
<script id="requestSubmit_validates_constraints">
{
const form = $('#test_form_validate');
const field = form.querySelector('input[name=q]');
let submitFired = 0;
let invalidFired = 0;
form.addEventListener('submit', (e) => { e.preventDefault(); submitFired++; });
field.addEventListener('invalid', () => { invalidFired++; });
// Required field is empty: submission is blocked and `invalid` fires.
form.requestSubmit($('#v_submit'));
testing.expectEqual(0, submitFired);
testing.expectEqual(1, invalidFired);
// A submitter with formnovalidate bypasses validation.
form.requestSubmit($('#v_submit_novalidate'));
testing.expectEqual(1, submitFired);
// form.noValidate also bypasses validation.
form.noValidate = true;
form.requestSubmit($('#v_submit'));
testing.expectEqual(2, submitFired);
// Once valid, validation passes and submission proceeds.
form.noValidate = false;
field.value = 'ok';
form.requestSubmit($('#v_submit'));
testing.expectEqual(3, submitFired);
}
</script>
<!-- Test: requestSubmit() with submitter not owned by form throws NotFoundError -->
<form id="test_form_rs2" action="/should-not-navigate5" method="get">
<input type="text" name="q" value="test">

View File

@@ -142,6 +142,18 @@ pub fn setAcceptCharset(self: *Form, value: []const u8, frame: *Frame) !void {
try self.asElement().setAttributeSafe(.wrap("accept-charset"), .wrap(value), frame);
}
pub fn getNoValidate(self: *const Form) bool {
return self.asConstElement().getAttributeSafe(comptime .wrap("novalidate")) != null;
}
pub fn setNoValidate(self: *Form, value: bool, frame: *Frame) !void {
if (value) {
try self.asElement().setAttributeSafe(comptime .wrap("novalidate"), .wrap(""), frame);
} else {
try self.asElement().removeAttribute(comptime .wrap("novalidate"), frame);
}
}
pub fn getEnctype(self: *const Form) []const u8 {
return normalizeEnctype(self.asConstElement().getAttributeSafe(comptime .wrap("enctype")), "application/x-www-form-urlencoded");
}
@@ -241,6 +253,7 @@ pub const JsApi = struct {
pub const target = bridge.accessor(Form.getTarget, Form.setTarget, .{ .ce_reactions = true });
pub const acceptCharset = bridge.accessor(Form.getAcceptCharset, Form.setAcceptCharset, .{ .ce_reactions = true });
pub const enctype = bridge.accessor(Form.getEnctype, Form.setEnctype, .{ .ce_reactions = true });
pub const noValidate = bridge.accessor(Form.getNoValidate, Form.setNoValidate, .{ .ce_reactions = true });
pub const elements = bridge.accessor(Form.getElements, null, .{});
pub const length = bridge.accessor(Form.getLength, null, .{});
pub const submit = bridge.function(Form.submit, .{});