diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig
index fb49e01e..9d4d6310 100644
--- a/src/browser/js/bridge.zig
+++ b/src/browser/js/bridge.zig
@@ -853,6 +853,7 @@ pub const JsApis = flattenTypes(&.{
@import("../webapi/event/InputEvent.zig"),
@import("../webapi/event/PromiseRejectionEvent.zig"),
@import("../webapi/event/SubmitEvent.zig"),
+ @import("../webapi/event/FormDataEvent.zig"),
@import("../webapi/MessageChannel.zig"),
@import("../webapi/MessagePort.zig"),
@import("../webapi/media/MediaError.zig"),
diff --git a/src/browser/tests/net/form_data.html b/src/browser/tests/net/form_data.html
index 12363038..fdfe4bc1 100644
--- a/src/browser/tests/net/form_data.html
+++ b/src/browser/tests/net/form_data.html
@@ -734,3 +734,101 @@
testing.expectEqual([['field', 'data'], ['x', '0'], ['y', '0']], entries);
}
+
+
+
+
+
+
+
+
+
+
diff --git a/src/browser/webapi/Event.zig b/src/browser/webapi/Event.zig
index bbac984d..54a53d7e 100644
--- a/src/browser/webapi/Event.zig
+++ b/src/browser/webapi/Event.zig
@@ -77,6 +77,7 @@ pub const Type = union(enum) {
ui_event: *@import("event/UIEvent.zig"),
promise_rejection_event: *@import("event/PromiseRejectionEvent.zig"),
submit_event: *@import("event/SubmitEvent.zig"),
+ form_data_event: *@import("event/FormDataEvent.zig"),
};
pub const Options = struct {
@@ -176,6 +177,7 @@ pub fn is(self: *Event, comptime T: type) ?*T {
.pop_state_event => |e| return if (T == @import("event/PopStateEvent.zig")) e else null,
.promise_rejection_event => |e| return if (T == @import("event/PromiseRejectionEvent.zig")) e else null,
.submit_event => |e| return if (T == @import("event/SubmitEvent.zig")) e else null,
+ .form_data_event => |e| return if (T == @import("event/FormDataEvent.zig")) e else null,
.ui_event => |e| {
if (T == @import("event/UIEvent.zig")) {
return e;
diff --git a/src/browser/webapi/event/FormDataEvent.zig b/src/browser/webapi/event/FormDataEvent.zig
new file mode 100644
index 00000000..6947aeab
--- /dev/null
+++ b/src/browser/webapi/event/FormDataEvent.zig
@@ -0,0 +1,95 @@
+// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
+//
+// Francis Bouvier
+// Pierre Tachoire
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const String = @import("../../../string.zig").String;
+const Page = @import("../../Page.zig");
+const Session = @import("../../Session.zig");
+const js = @import("../../js/js.zig");
+
+const Event = @import("../Event.zig");
+const UIEvent = @import("UIEvent.zig");
+
+const FormData = @import("../net/FormData.zig");
+
+/// https://developer.mozilla.org/en-US/docs/Web/API/FormDataEvent
+const FormDataEvent = @This();
+
+_proto: *Event,
+_form_data: ?*FormData = null,
+
+const Options = Event.inheritOptions(FormDataEvent, struct {
+ formData: ?*FormData = null,
+});
+
+pub fn init(typ: []const u8, maybe_options: Options, page: *Page) !*FormDataEvent {
+ const arena = try page.getArena(.{ .debug = "FormDataEvent" });
+ errdefer page.releaseArena(arena);
+ const type_string = try String.init(arena, typ, .{});
+ return initWithTrusted(arena, type_string, maybe_options, false, page);
+}
+
+pub fn initTrusted(typ: String, _opts: ?Options, page: *Page) !*FormDataEvent {
+ const arena = try page.getArena(.{ .debug = "FormDataEvent.trusted" });
+ errdefer page.releaseArena(arena);
+ return initWithTrusted(arena, typ, _opts, true, page);
+}
+
+fn initWithTrusted(arena: Allocator, typ: String, maybe_options: ?Options, trusted: bool, page: *Page) !*FormDataEvent {
+ const options = maybe_options orelse Options{};
+
+ const event = try page._factory.event(
+ arena,
+ typ,
+ FormDataEvent{
+ ._proto = undefined,
+ ._form_data = options.formData,
+ },
+ );
+
+ Event.populatePrototypes(event, options, trusted);
+ return event;
+}
+
+pub fn deinit(self: *FormDataEvent, shutdown: bool, session: *Session) void {
+ self._proto.deinit(shutdown, session);
+}
+
+pub fn asEvent(self: *FormDataEvent) *Event {
+ return self._proto;
+}
+
+pub fn getFormData(self: *const FormDataEvent) ?*FormData {
+ return self._form_data;
+}
+
+pub const JsApi = struct {
+ pub const bridge = js.Bridge(FormDataEvent);
+
+ pub const Meta = struct {
+ pub const name = "FormDataEvent";
+ pub const prototype_chain = bridge.prototypeChain();
+ pub var class_id: bridge.ClassId = undefined;
+ pub const weak = true;
+ pub const finalizer = bridge.finalizer(FormDataEvent.deinit);
+ };
+
+ pub const constructor = bridge.constructor(FormDataEvent.init, .{});
+ pub const formData = bridge.accessor(FormDataEvent.getFormData, null, .{});
+};
diff --git a/src/browser/webapi/event/SubmitEvent.zig b/src/browser/webapi/event/SubmitEvent.zig
index 80cb4d36..fbb1af06 100644
--- a/src/browser/webapi/event/SubmitEvent.zig
+++ b/src/browser/webapi/event/SubmitEvent.zig
@@ -26,7 +26,7 @@ const Event = @import("../Event.zig");
const HtmlElement = @import("../element/Html.zig");
const Allocator = std.mem.Allocator;
-// https://developer.mozilla.org/en-US/docs/Web/API/SubmitEvent
+/// https://developer.mozilla.org/en-US/docs/Web/API/SubmitEvent
const SubmitEvent = @This();
_proto: *Event,
diff --git a/src/browser/webapi/net/FormData.zig b/src/browser/webapi/net/FormData.zig
index 69246f93..73a5a5c8 100644
--- a/src/browser/webapi/net/FormData.zig
+++ b/src/browser/webapi/net/FormData.zig
@@ -35,10 +35,22 @@ _arena: Allocator,
_list: KeyValueList,
pub fn init(form: ?*Form, submitter: ?*Element, page: *Page) !*FormData {
- return page._factory.create(FormData{
+ const form_data = try page._factory.create(FormData{
._arena = page.arena,
._list = try collectForm(page.arena, form, submitter, page),
});
+
+ // Dispatch `formdata` event if form provided.
+ if (form) |_form| {
+ const form_data_event = try (@import("../event/FormDataEvent.zig")).initTrusted(
+ comptime .wrap("formdata"),
+ .{ .bubbles = true, .cancelable = false, .formData = form_data },
+ page,
+ );
+ try page._event_manager.dispatch(_form.asNode().asEventTarget(), form_data_event.asEvent());
+ }
+
+ return form_data;
}
pub fn get(self: *const FormData, name: []const u8) ?[]const u8 {