From 1b9b49f0455fdc18a8056452cf67d852bcb791ba Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Fri, 21 Nov 2025 15:38:41 +0800 Subject: [PATCH] customElements.upgrade --- .../tests/custom_elements/upgrade.html | 59 +++++++++++++++++ src/browser/tests/element/attributes.html | 6 -- src/browser/webapi/CustomElementRegistry.zig | 66 +++++++++++++++---- 3 files changed, 111 insertions(+), 20 deletions(-) diff --git a/src/browser/tests/custom_elements/upgrade.html b/src/browser/tests/custom_elements/upgrade.html index 1b917023..44f37bd6 100644 --- a/src/browser/tests/custom_elements/upgrade.html +++ b/src/browser/tests/custom_elements/upgrade.html @@ -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 = ''; + + 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 = ''; + document.body.appendChild(elem); + + customElements.define('already-upgraded', AlreadyUpgraded); + testing.expectEqual(1, alreadyUpgradedCalled); + + customElements.upgrade(elem); + testing.expectEqual(1, alreadyUpgradedCalled); +} diff --git a/src/browser/tests/element/attributes.html b/src/browser/tests/element/attributes.html index c1ce0767..5fb92883 100644 --- a/src/browser/tests/element/attributes.html +++ b/src/browser/tests/element/attributes.html @@ -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')); } diff --git a/src/browser/webapi/CustomElementRegistry.zig b/src/browser/webapi/CustomElementRegistry.zig index 62213754..2028bda8 100644 --- a/src/browser/webapi/CustomElementRegistry.zig +++ b/src/browser/webapi/CustomElementRegistry.zig @@ -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");