call AttributeChangedCallback on upgrade

This commit is contained in:
Karl Seguin
2025-11-28 13:04:42 +08:00
parent 94bcb30f11
commit 833a33678c
8 changed files with 76 additions and 17 deletions

View File

@@ -215,14 +215,12 @@ pub fn addFromElement(self: *ScriptManager, script_element: *Element.Html.Script
.url = remote_url orelse page.url,
.mode = blk: {
if (source == .@"inline") {
// inline modules are deferred, all other inline scripts have a
// normal execution flow
break :blk if (kind == .module) .@"defer" else .normal;
break :blk .normal;
}
if (element.getAttributeSafe("async") != null) {
break :blk .async;
}
if (element.getAttributeSafe("defer") != null) {
if (kind == .module or element.getAttributeSafe("defer") != null) {
break :blk .@"defer";
}
break :blk .normal;

View File

@@ -122,13 +122,16 @@
testing.expectEqual(0, callbackCalls.length);
customElements.define('upgrade-attr-element', UpgradeAttrElement);
testing.expectEqual(0, callbackCalls.length);
el.setAttribute('existing', 'after-upgrade');
testing.expectEqual(1, callbackCalls.length);
testing.expectEqual('existing', callbackCalls[0].name);
testing.expectEqual('before-upgrade', callbackCalls[0].oldValue);
testing.expectEqual('after-upgrade', callbackCalls[0].newValue);
testing.expectEqual(null, callbackCalls[0].oldValue);
testing.expectEqual('before-upgrade', callbackCalls[0].newValue);
el.setAttribute('existing', 'after-upgrade');
testing.expectEqual(2, callbackCalls.length);
testing.expectEqual('existing', callbackCalls[1].name);
testing.expectEqual('before-upgrade', callbackCalls[1].oldValue);
testing.expectEqual('after-upgrade', callbackCalls[1].newValue);
}
{

View File

@@ -151,4 +151,34 @@
customElements.upgrade(elem);
testing.expectEqual(1, alreadyUpgradedCalled);
}
{
let attributeChangedCalls = [];
class UpgradeWithAttrs extends HTMLElement {
static get observedAttributes() {
return ['data-foo', 'data-bar'];
}
attributeChangedCallback(name, oldValue, newValue) {
attributeChangedCalls.push({ name, oldValue, newValue });
}
}
const container = document.createElement('div');
container.innerHTML = '<upgrade-with-attrs data-foo="hello" data-bar="world"></upgrade-with-attrs>';
document.body.appendChild(container);
testing.expectEqual(0, attributeChangedCalls.length);
customElements.define('upgrade-with-attrs', UpgradeWithAttrs);
testing.expectEqual(2, attributeChangedCalls.length);
testing.expectEqual('data-foo', attributeChangedCalls[0].name);
testing.expectEqual(null, attributeChangedCalls[0].oldValue);
testing.expectEqual('hello', attributeChangedCalls[0].newValue);
testing.expectEqual('data-bar', attributeChangedCalls[1].name);
testing.expectEqual(null, attributeChangedCalls[1].oldValue);
testing.expectEqual('world', attributeChangedCalls[1].newValue);
}
</script>

View File

@@ -18,10 +18,13 @@
const std = @import("std");
const log = @import("../../log.zig");
const js = @import("../js/js.zig");
const Page = @import("../Page.zig");
const Element = @import("Element.zig");
const Node = @import("Node.zig");
const Element = @import("Element.zig");
const Custom = @import("element/html/Custom.zig");
const CustomElementDefinition = @import("CustomElementDefinition.zig");
const CustomElementRegistry = @This();
@@ -119,8 +122,6 @@ fn upgradeNode(self: *CustomElementRegistry, node: *Node, page: *Page) !void {
}
fn upgradeElement(self: *CustomElementRegistry, element: *Element, page: *Page) !void {
const Custom = @import("element/html/Custom.zig");
const custom = element.is(Custom) orelse {
return Custom.checkAndAttachBuiltIn(element, page);
};
@@ -133,7 +134,7 @@ fn upgradeElement(self: *CustomElementRegistry, element: *Element, page: *Page)
try upgradeCustomElement(custom, definition, page);
}
fn upgradeCustomElement(custom: *@import("element/html/Custom.zig"), definition: *CustomElementDefinition, page: *Page) !void {
fn upgradeCustomElement(custom: *Custom, definition: *CustomElementDefinition, page: *Page) !void {
custom._definition = definition;
// Reset callback flags since this is a fresh upgrade
@@ -151,6 +152,15 @@ fn upgradeCustomElement(custom: *@import("element/html/Custom.zig"), definition:
return error.CustomElementUpgradeFailed;
};
// Invoke attributeChangedCallback for existing observed attributes
var attr_it = custom.asElement().attributeIterator();
while (attr_it.next()) |attr| {
const name = attr._name.str();
if (definition.isAttributeObserved(name)) {
custom.invokeAttributeChangedCallback(name, null, attr._value.str(), page);
}
}
if (node.isConnected()) {
custom.invokeConnectedCallback(page);
}

View File

@@ -272,9 +272,9 @@ pub fn setClassName(self: *Element, value: []const u8, page: *Page) !void {
return self.setAttributeSafe("class", value, page);
}
pub fn attributeIterator(self: *Element) Attribute.Iterator {
pub fn attributeIterator(self: *Element) Attribute.InnerIterator {
const attributes = self._attributes orelse return .{};
return attributes.iterator(self);
return attributes.iterator();
}
pub fn getAttribute(self: *const Element, name: []const u8, page: *Page) !?[]const u8 {

View File

@@ -54,6 +54,7 @@ _history: History,
_storage_bucket: *storage.Bucket,
_on_load: ?js.Function = null,
_on_error: ?js.Function = null, // TODO: invoke on error?
_on_unhandled_rejection: ?js.Function = null, // TODO: invoke on error
_location: *Location,
_timer_id: u30 = 0,
_timers: std.AutoHashMapUnmanaged(u32, *ScheduleCallback) = .{},
@@ -143,6 +144,18 @@ pub fn setOnError(self: *Window, cb_: ?js.Function) !void {
}
}
pub fn getOnUnhandledRejection(self: *const Window) ?js.Function {
return self._on_unhandled_rejection;
}
pub fn setOnUnhandledRejection(self: *Window, cb_: ?js.Function) !void {
if (cb_) |cb| {
self._on_unhandled_rejection = cb;
} else {
self._on_unhandled_rejection = null;
}
}
pub fn fetch(_: *const Window, input: Fetch.Input, page: *Page) !js.Promise {
return Fetch.init(input, page);
}
@@ -390,6 +403,7 @@ pub const JsApi = struct {
pub const customElements = bridge.accessor(Window.getCustomElements, null, .{ .cache = "customElements" });
pub const onload = bridge.accessor(Window.getOnLoad, Window.setOnLoad, .{});
pub const onerror = bridge.accessor(Window.getOnError, Window.getOnError, .{});
pub const onunhandledrejection = bridge.accessor(Window.getOnUnhandledRejection, Window.setOnUnhandledRejection, .{});
pub const fetch = bridge.function(Window.fetch, .{});
pub const queueMicrotask = bridge.function(Window.queueMicrotask, .{});
pub const setTimeout = bridge.function(Window.setTimeout, .{});

View File

@@ -29,6 +29,7 @@ const Request = @import("Request.zig");
const Response = @import("Response.zig");
const Allocator = std.mem.Allocator;
const IS_DEBUG = @import("builtin").mode == .Debug;
const Fetch = @This();
@@ -54,6 +55,10 @@ pub fn init(input: Input, page: *Page) !js.Promise {
const http_client = page._session.browser.http_client;
const headers = try http_client.newHeaders();
if (comptime IS_DEBUG) {
log.debug(.http, "fetch", .{ .url = request._url });
}
try http_client.request(.{
.ctx = fetch,
.url = request._url,

View File

@@ -19,8 +19,6 @@
const std = @import("std");
const js = @import("../../js/js.zig");
const IS_DEBUG = @import("builtin").mode == .Debug;
const log = @import("../../../log.zig");
const Http = @import("../../../http/Http.zig");
@@ -32,6 +30,7 @@ const EventTarget = @import("../EventTarget.zig");
const XMLHttpRequestEventTarget = @import("XMLHttpRequestEventTarget.zig");
const Allocator = std.mem.Allocator;
const IS_DEBUG = @import("builtin").mode == .Debug;
const XMLHttpRequest = @This();
_page: *Page,