From ecbe347873ccd8d38dbb3291f665999a53a33425 Mon Sep 17 00:00:00 2001 From: "Dilip Kr. Shukla" Date: Fri, 24 Oct 2025 13:56:25 +0000 Subject: [PATCH 1/8] feat: add support for serializable option in shadow DOM --- src/index.d.ts | 1 + src/index.js | 6 +++++- test/index.test.jsx | 16 ++++++++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/index.d.ts b/src/index.d.ts index 8fc37c3..3703fe9 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -8,6 +8,7 @@ type Options = shadow: true; mode?: 'open' | 'closed'; adoptedStyleSheets?: CSSStyleSheet[]; + serializable?: boolean; }; /** diff --git a/src/index.js b/src/index.js index 5148a0d..23cdc1e 100644 --- a/src/index.js +++ b/src/index.js @@ -15,7 +15,11 @@ export default function register(Component, tagName, propNames, options) { inst._vdomComponent = Component; if (options && options.shadow) { - inst._root = inst.attachShadow({ mode: options.mode || 'open' }); + const shadowOptions = { mode: options.mode || 'open' }; + if (options.serializable !== undefined) { + shadowOptions.serializable = !!options.serializable; + } + inst._root = inst.attachShadow(shadowOptions); if (options.adoptedStyleSheets) { inst._root.adoptedStyleSheets = options.adoptedStyleSheets; diff --git a/test/index.test.jsx b/test/index.test.jsx index 4fce9fe..7481fe8 100644 --- a/test/index.test.jsx +++ b/test/index.test.jsx @@ -421,4 +421,20 @@ describe('web components', () => { '

Light DOM Children

Child 1

Child 2

' ); }); + + it('supports the `serializable` option', async () => { + function SerializableComponent() { + return
Serializable Shadow DOM
; + } + + registerElement(SerializableComponent, 'x-serializable', [], { + shadow: true, + serializable: true, + }); + + root.innerHTML = ``; + + const el = document.querySelector('x-serializable'); + assert.isTrue(el.shadowRoot.serializable); + }); }); From e4992a93e755579fcd68903b9e62f4b60b8dca06 Mon Sep 17 00:00:00 2001 From: "Dilip Kr. Shukla" Date: Sat, 25 Oct 2025 01:39:11 +0200 Subject: [PATCH 2/8] docs: document `serializable` option for declarative shadow DOM and SSR --- README.md | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index a960cde..222080d 100644 --- a/README.md +++ b/README.md @@ -14,10 +14,10 @@ const Greeting = ({ name = 'World' }) => (

Hello, {name}!

); -register(Greeting, 'x-greeting', ['name'], { shadow: true, mode: 'open', adoptedStyleSheets: [] }); -// ^ ^ ^ ^ ^ ^ -// | HTML tag name | use shadow-dom | use adoptedStyleSheets -// Component definition Observed attributes Encapsulation mode for the shadow DOM tree +register(Greeting, 'x-greeting', ['name'], { shadow: true, mode: 'open', adoptedStyleSheets: [], serializable: true }); +// ^ ^ ^ ^ ^ ^ ^ +// | HTML tag name | use shadow-dom | use adoptedStyleSheets | +// Component definition Observed attributes Encapsulation mode for the shadow DOM tree Enable declarative shadow DOM ``` > _**\* Note:** as per the [Custom Elements specification](https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name), the tag name must contain a hyphen._ @@ -81,7 +81,7 @@ register(FullName, 'full-name'); ### Passing slots as props -The `register()` function also accepts an optional fourth parameter, an options bag. At present, it allows you to opt-in to using shadow DOM for your custom element by setting the `shadow` property to `true`, and if so, you can also specify the encapsulation mode with `mode`, which can be either `'open'` or `'closed'`. +The `register()` function also accepts an optional fourth parameter, an options bag. At present, it allows you to opt-in to using shadow DOM for your custom element by setting the `shadow` property to `true`, and if so, you can also specify the encapsulation mode with `mode`, which can be either `'open'` or `'closed'`. Additionally, you can enable declarative shadow DOM by setting `serializable` to `true`, which is useful for server-side rendering scenarios. When using shadow DOM, you can make use of named `` elements in your component to forward the custom element's children into specific places in the shadow tree. @@ -103,6 +103,24 @@ register(TextSelection, 'text-selection', [], { shadow: true }); My Heading Some content goes here. + +### Declarative Shadow DOM (serializable option) + +The `serializable` option enables declarative shadow DOM for server-side rendering: + +```js +register(MyComponent, 'my-element', [], { + shadow: true, + serializable: true +}); + +// Usage with getHTML() for SSR +const el = document.querySelector('my-element'); +const html = el.getHTML({ serializableShadowRoots: true }); + +// Test serializable shadow root +console.log(el.shadowRoot.serializable); // true +``` ``` ### Static Properties From f0d3d8b78c8112ea3a354014e56f784a6f3d2381 Mon Sep 17 00:00:00 2001 From: "Dilip Kr. Shukla" Date: Sat, 25 Oct 2025 01:43:00 +0200 Subject: [PATCH 3/8] docs: fix code fence --- README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 222080d..87e0f4f 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,7 @@ register(TextSelection, 'text-selection', [], { shadow: true }); My Heading Some content goes here. +``` ### Declarative Shadow DOM (serializable option) @@ -114,13 +115,10 @@ register(MyComponent, 'my-element', [], { serializable: true }); -// Usage with getHTML() for SSR const el = document.querySelector('my-element'); const html = el.getHTML({ serializableShadowRoots: true }); -// Test serializable shadow root -console.log(el.shadowRoot.serializable); // true -``` +console.log(el.shadowRoot.serializable); ``` ### Static Properties From 3f083ad8523f5c72397f36414d55d4e36824706c Mon Sep 17 00:00:00 2001 From: "Dilip Kr. Shukla" Date: Sat, 25 Oct 2025 02:00:19 +0200 Subject: [PATCH 4/8] Update src/index.js Co-authored-by: Ryan Christian <33403762+rschristian@users.noreply.github.com> --- src/index.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/index.js b/src/index.js index 23cdc1e..1ae5828 100644 --- a/src/index.js +++ b/src/index.js @@ -15,11 +15,10 @@ export default function register(Component, tagName, propNames, options) { inst._vdomComponent = Component; if (options && options.shadow) { - const shadowOptions = { mode: options.mode || 'open' }; - if (options.serializable !== undefined) { - shadowOptions.serializable = !!options.serializable; - } - inst._root = inst.attachShadow(shadowOptions); + inst._root = inst.attachShadow({ + mode: options.mode || 'open', + serializable: options.serializable ?? false, + }); if (options.adoptedStyleSheets) { inst._root.adoptedStyleSheets = options.adoptedStyleSheets; From 3d0b17743ba36e60fd96be426d7faff91359be47 Mon Sep 17 00:00:00 2001 From: "Dilip Kr. Shukla" Date: Sat, 25 Oct 2025 02:05:28 +0200 Subject: [PATCH 5/8] docs: clarify serializable option wording in README --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 87e0f4f..34479c9 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ const Greeting = ({ name = 'World' }) => ( register(Greeting, 'x-greeting', ['name'], { shadow: true, mode: 'open', adoptedStyleSheets: [], serializable: true }); // ^ ^ ^ ^ ^ ^ ^ // | HTML tag name | use shadow-dom | use adoptedStyleSheets | -// Component definition Observed attributes Encapsulation mode for the shadow DOM tree Enable declarative shadow DOM +// Component definition Observed attributes Encapsulation mode for the shadow DOM tree shadow root may be serialized ``` > _**\* Note:** as per the [Custom Elements specification](https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name), the tag name must contain a hyphen._ @@ -81,7 +81,7 @@ register(FullName, 'full-name'); ### Passing slots as props -The `register()` function also accepts an optional fourth parameter, an options bag. At present, it allows you to opt-in to using shadow DOM for your custom element by setting the `shadow` property to `true`, and if so, you can also specify the encapsulation mode with `mode`, which can be either `'open'` or `'closed'`. Additionally, you can enable declarative shadow DOM by setting `serializable` to `true`, which is useful for server-side rendering scenarios. +The `register()` function also accepts an optional fourth parameter, an options bag. At present, it allows you to opt-in to using shadow DOM for your custom element by setting the `shadow` property to `true`, and if so, you can also specify the encapsulation mode with `mode`, which can be either `'open'` or `'closed'`. Additionally, the shadow DOM may be serialized by setting `serializable` to `true`, which is useful for server-side rendering scenarios. When using shadow DOM, you can make use of named `` elements in your component to forward the custom element's children into specific places in the shadow tree. @@ -105,9 +105,9 @@ register(TextSelection, 'text-selection', [], { shadow: true }); ``` -### Declarative Shadow DOM (serializable option) +### `serializable` option -The `serializable` option enables declarative shadow DOM for server-side rendering: +The `serializable` option is set, the shadow root may be serialized. ```js register(MyComponent, 'my-element', [], { From 5d24a0f46b633f6358aebd6711e76a24d80659c4 Mon Sep 17 00:00:00 2001 From: "Dilip Kr. Shukla" Date: Sat, 25 Oct 2025 02:08:38 +0200 Subject: [PATCH 6/8] docs: correct wording for serializable option description in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 34479c9..2e57a75 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ register(TextSelection, 'text-selection', [], { shadow: true }); ### `serializable` option -The `serializable` option is set, the shadow root may be serialized. +If the `serializable` option is set, the shadow root may be serialized. ```js register(MyComponent, 'my-element', [], { From 95a948745c1242d04610ac5a0f8cc98ef4089dd9 Mon Sep 17 00:00:00 2001 From: "Dilip Kr. Shukla" Date: Sat, 25 Oct 2025 02:43:53 +0200 Subject: [PATCH 7/8] test: extend serializable option tests to cover non-serializable/undefined cases and verify serialization output --- test/index.test.jsx | 60 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 57 insertions(+), 3 deletions(-) diff --git a/test/index.test.jsx b/test/index.test.jsx index 7481fe8..38009a7 100644 --- a/test/index.test.jsx +++ b/test/index.test.jsx @@ -427,14 +427,68 @@ describe('web components', () => { return
Serializable Shadow DOM
; } + function NonSerializableComponent() { + return ( +
+ Non-serializable Shadow DOM +
+ ); + } + + function UndefinedSerializableComponent() { + return
Undefined Serializable
; + } + registerElement(SerializableComponent, 'x-serializable', [], { shadow: true, serializable: true, }); - root.innerHTML = ``; + registerElement(NonSerializableComponent, 'x-non-serializable', [], { + shadow: true, + }); + + registerElement( + UndefinedSerializableComponent, + 'x-serializable-undefined', + [], + { + shadow: true, + serializable: undefined, + } + ); - const el = document.querySelector('x-serializable'); - assert.isTrue(el.shadowRoot.serializable); + root.innerHTML = ` + + + + `; + + const serializableEl = document.querySelector('x-serializable'); + const nonSerializableEl = document.querySelector('x-non-serializable'); + const undefinedEl = document.querySelector('x-serializable-undefined'); + + // The serializable option sets the shadowRoot.serializable property, + // which enables declarative shadow DOM for server-side rendering + assert.isTrue(serializableEl.shadowRoot.serializable); + assert.isFalse(nonSerializableEl.shadowRoot.serializable); + assert.isFalse(undefinedEl.shadowRoot.serializable); + + // Test getHTML() if available (declarative shadow DOM support) + if (typeof serializableEl.getHTML === 'function') { + const serializableHtml = serializableEl.getHTML({ + serializableShadowRoots: true, + }); + const nonSerializableHtml = nonSerializableEl.getHTML({ + serializableShadowRoots: true, + }); + + // getHTML() is available - verify it includes shadow DOM serialization + if (serializableHtml) { + assert.include(serializableHtml, 'shadowrootmode="open"'); + assert.include(serializableHtml, 'shadowrootserializable'); + assert.notInclude(nonSerializableHtml, 'shadowrootserializable'); + } + } }); }); From a5c8187b0c98060b744d94c5cd95fcbc142542b7 Mon Sep 17 00:00:00 2001 From: Ryan Christian Date: Fri, 24 Oct 2025 22:31:46 -0500 Subject: [PATCH 8/8] refactor: PR suggestions --- README.md | 24 ++++---------------- test/index.test.jsx | 53 +++++++++++---------------------------------- 2 files changed, 17 insertions(+), 60 deletions(-) diff --git a/README.md b/README.md index 2e57a75..496c70d 100644 --- a/README.md +++ b/README.md @@ -15,9 +15,9 @@ const Greeting = ({ name = 'World' }) => ( ); register(Greeting, 'x-greeting', ['name'], { shadow: true, mode: 'open', adoptedStyleSheets: [], serializable: true }); -// ^ ^ ^ ^ ^ ^ ^ -// | HTML tag name | use shadow-dom | use adoptedStyleSheets | -// Component definition Observed attributes Encapsulation mode for the shadow DOM tree shadow root may be serialized +// ^ ^ ^ ^ ^ ^ ^ +// | HTML tag name | use shadow-dom | use adoptedStyleSheets | +// Component definition Observed attributes Encapsulation mode for the shadow DOM tree Root is serializable ``` > _**\* Note:** as per the [Custom Elements specification](https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name), the tag name must contain a hyphen._ @@ -81,7 +81,7 @@ register(FullName, 'full-name'); ### Passing slots as props -The `register()` function also accepts an optional fourth parameter, an options bag. At present, it allows you to opt-in to using shadow DOM for your custom element by setting the `shadow` property to `true`, and if so, you can also specify the encapsulation mode with `mode`, which can be either `'open'` or `'closed'`. Additionally, the shadow DOM may be serialized by setting `serializable` to `true`, which is useful for server-side rendering scenarios. +The `register()` function also accepts an optional fourth parameter, an options bag. At present, it allows you to opt-in to using shadow DOM for your custom element by setting the `shadow` property to `true`, and if so, you can also specify the encapsulation mode with `mode`, which can be either `'open'` or `'closed'`. Additionally, you may mark the shadow root as being serializable with the boolean `serializable` property. When using shadow DOM, you can make use of named `` elements in your component to forward the custom element's children into specific places in the shadow tree. @@ -105,22 +105,6 @@ register(TextSelection, 'text-selection', [], { shadow: true }); ``` -### `serializable` option - -If the `serializable` option is set, the shadow root may be serialized. - -```js -register(MyComponent, 'my-element', [], { - shadow: true, - serializable: true -}); - -const el = document.querySelector('my-element'); -const html = el.getHTML({ serializableShadowRoots: true }); - -console.log(el.shadowRoot.serializable); -``` - ### Static Properties We support a number of static properties on your component that map to special behaviors of the custom element. These can be set on components like so: diff --git a/test/index.test.jsx b/test/index.test.jsx index 38009a7..734ab6c 100644 --- a/test/index.test.jsx +++ b/test/index.test.jsx @@ -424,19 +424,11 @@ describe('web components', () => { it('supports the `serializable` option', async () => { function SerializableComponent() { - return
Serializable Shadow DOM
; + return
Serializable Shadow DOM
; } function NonSerializableComponent() { - return ( -
- Non-serializable Shadow DOM -
- ); - } - - function UndefinedSerializableComponent() { - return
Undefined Serializable
; + return
Non-serializable Shadow DOM
; } registerElement(SerializableComponent, 'x-serializable', [], { @@ -448,47 +440,28 @@ describe('web components', () => { shadow: true, }); - registerElement( - UndefinedSerializableComponent, - 'x-serializable-undefined', - [], - { - shadow: true, - serializable: undefined, - } - ); - root.innerHTML = ` - `; const serializableEl = document.querySelector('x-serializable'); const nonSerializableEl = document.querySelector('x-non-serializable'); - const undefinedEl = document.querySelector('x-serializable-undefined'); - // The serializable option sets the shadowRoot.serializable property, - // which enables declarative shadow DOM for server-side rendering assert.isTrue(serializableEl.shadowRoot.serializable); assert.isFalse(nonSerializableEl.shadowRoot.serializable); - assert.isFalse(undefinedEl.shadowRoot.serializable); - // Test getHTML() if available (declarative shadow DOM support) - if (typeof serializableEl.getHTML === 'function') { - const serializableHtml = serializableEl.getHTML({ - serializableShadowRoots: true, - }); - const nonSerializableHtml = nonSerializableEl.getHTML({ - serializableShadowRoots: true, - }); + const serializableHtml = serializableEl.getHTML({ + serializableShadowRoots: true, + }); + const nonSerializableHtml = nonSerializableEl.getHTML({ + serializableShadowRoots: true, + }); - // getHTML() is available - verify it includes shadow DOM serialization - if (serializableHtml) { - assert.include(serializableHtml, 'shadowrootmode="open"'); - assert.include(serializableHtml, 'shadowrootserializable'); - assert.notInclude(nonSerializableHtml, 'shadowrootserializable'); - } - } + assert.equal( + serializableHtml, + '' + ); + assert.isEmpty(nonSerializableHtml); }); });