customElements.upgrade

This commit is contained in:
Karl Seguin
2025-11-21 15:38:41 +08:00
parent 095413c6c5
commit 1b9b49f045
3 changed files with 111 additions and 20 deletions

View File

@@ -92,4 +92,63 @@
document.body.appendChild(container);
testing.expectEqual(1, connectedCalled);
}
{
let constructorCalled = 0;
let connectedCalled = 0;
class ManualUpgrade extends HTMLElement {
constructor() {
super();
constructorCalled++;
this.manuallyUpgraded = true;
}
connectedCallback() {
connectedCalled++;
}
}
customElements.define('manual-upgrade', ManualUpgrade);
const container = document.createElement('div');
container.innerHTML = '<manual-upgrade id="m1"><manual-upgrade id="m2"></manual-upgrade></manual-upgrade>';
testing.expectEqual(2, constructorCalled);
testing.expectEqual(0, connectedCalled);
customElements.upgrade(container);
testing.expectEqual(2, constructorCalled);
testing.expectEqual(0, connectedCalled);
const m1 = container.querySelector('#m1');
const m2 = container.querySelector('#m2');
testing.expectEqual(true, m1.manuallyUpgraded);
testing.expectEqual(true, m2.manuallyUpgraded);
document.body.appendChild(container);
testing.expectEqual(2, connectedCalled);
}
{
let alreadyUpgradedCalled = 0;
class AlreadyUpgraded extends HTMLElement {
constructor() {
super();
alreadyUpgradedCalled++;
}
}
const elem = document.createElement('div');
elem.innerHTML = '<already-upgraded></already-upgraded>';
document.body.appendChild(elem);
customElements.define('already-upgraded', AlreadyUpgraded);
testing.expectEqual(1, alreadyUpgradedCalled);
customElements.upgrade(elem);
testing.expectEqual(1, alreadyUpgradedCalled);
}
</script>

View File

@@ -107,30 +107,24 @@
{
const el1 = $('#attr1');
// Toggle non-existent attribute (should add it and return true)
testing.expectEqual(false, el1.hasAttribute('toggle-test'));
testing.expectEqual(true, el1.toggleAttribute('toggle-test'));
testing.expectEqual(true, el1.hasAttribute('toggle-test'));
testing.expectEqual('', el1.getAttribute('toggle-test'));
// Toggle existing attribute (should remove it and return false)
testing.expectEqual(false, el1.toggleAttribute('toggle-test'));
testing.expectEqual(false, el1.hasAttribute('toggle-test'));
// Toggle with force=true when attribute doesn't exist (should add and return true)
testing.expectEqual(false, el1.hasAttribute('toggle-test'));
testing.expectEqual(true, el1.toggleAttribute('toggle-test', true));
testing.expectEqual(true, el1.hasAttribute('toggle-test'));
// Toggle with force=true when attribute exists (should keep and return true)
testing.expectEqual(true, el1.toggleAttribute('toggle-test', true));
testing.expectEqual(true, el1.hasAttribute('toggle-test'));
// Toggle with force=false when attribute exists (should remove and return false)
testing.expectEqual(false, el1.toggleAttribute('toggle-test', false));
testing.expectEqual(false, el1.hasAttribute('toggle-test'));
// Toggle with force=false when attribute doesn't exist (should keep removed and return false)
testing.expectEqual(false, el1.toggleAttribute('toggle-test', false));
testing.expectEqual(false, el1.hasAttribute('toggle-test'));
}

View File

@@ -21,6 +21,7 @@ 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 CustomElementDefinition = @import("CustomElementDefinition.zig");
const CustomElementRegistry = @This();
@@ -88,24 +89,11 @@ pub fn define(self: *CustomElementRegistry, name: []const u8, constructor: js.Fu
continue;
}
custom._definition = definition;
const node = custom.asNode();
const prev_upgrading = page._upgrading_element;
page._upgrading_element = node;
defer page._upgrading_element = prev_upgrading;
var result: js.Function.Result = undefined;
_ = definition.constructor.newInstance(&result) catch |err| {
log.warn(.js, "custom element upgrade", .{ .name = name, .err = err });
upgradeCustomElement(custom, definition, page) catch {
_ = page._undefined_custom_elements.swapRemove(idx);
continue;
};
if (node.isConnected()) {
custom.invokeConnectedCallback(page);
}
_ = page._undefined_custom_elements.swapRemove(idx);
}
}
@@ -115,6 +103,55 @@ pub fn get(self: *CustomElementRegistry, name: []const u8) ?js.Function {
return definition.constructor;
}
pub fn upgrade(self: *CustomElementRegistry, root: *Node, page: *Page) !void {
try upgradeNode(self, root, page);
}
fn upgradeNode(self: *CustomElementRegistry, node: *Node, page: *Page) !void {
if (node.is(Element)) |element| {
try upgradeElement(self, element, page);
}
var it = node.childrenIterator();
while (it.next()) |child| {
try upgradeNode(self, child, page);
}
}
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);
};
if (custom._definition != null) return;
const name = custom._tag_name.str();
const definition = self._definitions.get(name) orelse return;
try upgradeCustomElement(custom, definition, page);
}
fn upgradeCustomElement(custom: *@import("element/html/Custom.zig"), definition: *CustomElementDefinition, page: *Page) !void {
custom._definition = definition;
const node = custom.asNode();
const prev_upgrading = page._upgrading_element;
page._upgrading_element = node;
defer page._upgrading_element = prev_upgrading;
var result: js.Function.Result = undefined;
_ = definition.constructor.newInstance(&result) catch |err| {
log.warn(.js, "custom element upgrade", .{ .name = definition.name, .err = err });
return error.CustomElementUpgradeFailed;
};
if (node.isConnected()) {
custom.invokeConnectedCallback(page);
}
}
fn validateName(name: []const u8) !void {
if (name.len == 0) {
return error.InvalidCustomElementName;
@@ -166,6 +203,7 @@ pub const JsApi = struct {
pub const define = bridge.function(CustomElementRegistry.define, .{ .dom_exception = true });
pub const get = bridge.function(CustomElementRegistry.get, .{ .null_as_undefined = true });
pub const upgrade = bridge.function(CustomElementRegistry.upgrade, .{});
};
const testing = @import("../../testing.zig");