Skip to content

[feat]: Light DOM <slot/> substitute#26

Closed
arielsalminen wants to merge 21 commits intomainfrom
feature/slottable
Closed

[feat]: Light DOM <slot/> substitute#26
arielsalminen wants to merge 21 commits intomainfrom
feature/slottable

Conversation

@arielsalminen
Copy link
Copy Markdown
Member

@arielsalminen arielsalminen commented Apr 14, 2026

Overview

This PR adds a slot() template helper that projects the host element’s children into a Primitive Component’s rendered <template>, similar to how <slot> works in Shadow DOM, but for Light DOM.

This is an alternative and more performant approach to #10 (MutationObserver). Instead of observing external DOM mutations, this inverts the logic: the component explicitly declares where external content goes, and Elena leaves it alone. Inspired by the slottable-request community protocol.

Why is this being introduced?

Elena's Primitive Components own their internal DOM through render() while still using Light DOM. The problem is that on first render, replaceChildren() replaces everything inside the host with the component’s own rendered template (on purpose). But this leads to any children that a framework may put there being destroyed.

This means frameworks can't pass dynamic content as children:

// Broken: Works on page load, but gets destroyed on framework triggered re-renders
<elena-button>{count}</elena-button>

The workaround with the text prop:

// Works on page load and with re-renders, but may feel unnatural
<elena-button text={count} />

slot() solves this by letting the component declare where children go in its template. With these proposed changes, Elena saves the original children, renders the template around them, and marks that area as off-limits during re-renders. Frameworks keep ownership of the projected nodes and can update them freely.

Before (workaround via text prop):

<elena-button>initial count</elena-button>

// Later updates
<elena-button text={count}></elena-button>

After:

<elena-button>{count}</elena-button>

You can now also pass any html to a Primitive Component, not just text:

<elena-button href="mailto:x">
   Send email
   <elena-icon name="email"></elena-icon>
   <div>Some random content</div>
</elena-button>

How it works

import { Elena, html, slot } from "@elenajs/core";

class Button extends Elena(HTMLElement) {
  static tagName = "elena-button";
  render() {
    return html`<button>${slot(this)}</button>`;
  }
}
  1. On first render, slot() produces a comment marker (<!--slotId-->) that survives template stringification.
  2. The renderer finds the comment, moves the host's children into that position, and marks the parent element as projected.
  3. On re-renders, Elena skips projected children, preserving framework-owned DOM.
  4. textContent and appendChild on the host are redirected to the projected element, so frameworks can update content without destroying the <template>.

Closes #9, closes #24, supersedes #10

TODO

  • Fix SSR hydration and @elenajs/ssr compatibility
  • Update example component library to use slot() instead
  • Update live examples page to use the new pattern
  • Test with React
  • Test with Next.js
  • Test with Angular
  • Test with Vue
  • Test with Svelte
  • Test with plain JavaScript
  • Make sure performance benchmarks don’t get a hit
  • Explore other approaches for ${slot(this)}
  • Re-address naming concerns
  • Fix visual regression tests.
  • Update the relevant playground examples
  • Finish documentation improvements
  • Make sure we’re not introducing breaking changes

@netlify
Copy link
Copy Markdown

netlify Bot commented Apr 14, 2026

Deploy Preview for elenajs ready!

Name Link
🔨 Latest commit dbad957
🔍 Latest deploy log https://app.netlify.com/projects/elenajs/deploys/69df3e25aef609000888606b
😎 Deploy Preview https://deploy-preview-26--elenajs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 14, 2026

Dependency Review

The following issues were found:
  • ✅ 0 vulnerable package(s)
  • ✅ 0 package(s) with incompatible licenses
  • ✅ 0 package(s) with invalid SPDX license definitions
  • ⚠️ 1 package(s) with unknown licenses.
See the Details below.

Snapshot Warnings

⚠️: No snapshots were found for the head SHA dbad957.
Ensure that dependencies are being submitted on PR branches and consider enabling retry-on-snapshot-warnings. See the documentation for more information and troubleshooting advice.

License Issues

docs/package.json

PackageVersionLicenseIssue Type
vue^3.5.32NullUnknown License

OpenSSF Scorecard

Scorecard details
PackageVersionScoreDetails
npm/vue ^3.5.32 UnknownUnknown
npm/@babel/parser 7.29.2 🟢 7.2
Details
CheckScoreReason
Maintained🟢 1030 commit(s) and 18 issue activity found in the last 90 days -- score normalized to 10
Code-Review🟢 9Found 27/30 approved changesets -- score normalized to 9
Security-Policy🟢 10security policy file detected
Packaging⚠️ -1packaging workflow not detected
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
CII-Best-Practices⚠️ 2badge detected: InProgress
License🟢 10license file detected
Token-Permissions🟢 9detected GitHub workflow tokens with excessive permissions
Branch-Protection⚠️ -1internal error: error during branchesHandler.setup: internal error: some github tokens can't read classic branch protection rules: https://github.com/ossf/scorecard-action/blob/main/docs/authentication/fine-grained-auth-token.md
Signed-Releases⚠️ -1no releases found
Pinned-Dependencies⚠️ 0dependency not pinned by hash detected -- score normalized to 0
Binary-Artifacts🟢 10no binaries found in the repo
Fuzzing⚠️ 0project is not fuzzed
SAST⚠️ 0SAST tool is not run on all commits -- score normalized to 0
npm/@vue/compiler-core 3.5.32 🟢 5.4
Details
CheckScoreReason
Security-Policy🟢 10security policy file detected
Code-Review🟢 3Found 5/14 approved changesets -- score normalized to 3
Maintained🟢 1030 commit(s) and 11 issue activity found in the last 90 days -- score normalized to 10
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Token-Permissions⚠️ 0detected GitHub workflow tokens with excessive permissions
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Binary-Artifacts🟢 10no binaries found in the repo
Pinned-Dependencies⚠️ 0dependency not pinned by hash detected -- score normalized to 0
License🟢 10license file detected
Signed-Releases⚠️ -1no releases found
Fuzzing⚠️ 0project is not fuzzed
Branch-Protection🟢 3branch protection is not maximal on development and all release branches
Packaging🟢 10packaging workflow detected
SAST⚠️ 0SAST tool is not run on all commits -- score normalized to 0
npm/@vue/compiler-dom 3.5.32 🟢 5.4
Details
CheckScoreReason
Security-Policy🟢 10security policy file detected
Code-Review🟢 3Found 5/14 approved changesets -- score normalized to 3
Maintained🟢 1030 commit(s) and 11 issue activity found in the last 90 days -- score normalized to 10
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Token-Permissions⚠️ 0detected GitHub workflow tokens with excessive permissions
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Binary-Artifacts🟢 10no binaries found in the repo
Pinned-Dependencies⚠️ 0dependency not pinned by hash detected -- score normalized to 0
License🟢 10license file detected
Signed-Releases⚠️ -1no releases found
Fuzzing⚠️ 0project is not fuzzed
Branch-Protection🟢 3branch protection is not maximal on development and all release branches
Packaging🟢 10packaging workflow detected
SAST⚠️ 0SAST tool is not run on all commits -- score normalized to 0
npm/@vue/compiler-sfc 3.5.32 🟢 5.4
Details
CheckScoreReason
Security-Policy🟢 10security policy file detected
Code-Review🟢 3Found 5/14 approved changesets -- score normalized to 3
Maintained🟢 1030 commit(s) and 11 issue activity found in the last 90 days -- score normalized to 10
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Token-Permissions⚠️ 0detected GitHub workflow tokens with excessive permissions
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Binary-Artifacts🟢 10no binaries found in the repo
Pinned-Dependencies⚠️ 0dependency not pinned by hash detected -- score normalized to 0
License🟢 10license file detected
Signed-Releases⚠️ -1no releases found
Fuzzing⚠️ 0project is not fuzzed
Branch-Protection🟢 3branch protection is not maximal on development and all release branches
Packaging🟢 10packaging workflow detected
SAST⚠️ 0SAST tool is not run on all commits -- score normalized to 0
npm/@vue/compiler-ssr 3.5.32 🟢 5.4
Details
CheckScoreReason
Security-Policy🟢 10security policy file detected
Code-Review🟢 3Found 5/14 approved changesets -- score normalized to 3
Maintained🟢 1030 commit(s) and 11 issue activity found in the last 90 days -- score normalized to 10
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Token-Permissions⚠️ 0detected GitHub workflow tokens with excessive permissions
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Binary-Artifacts🟢 10no binaries found in the repo
Pinned-Dependencies⚠️ 0dependency not pinned by hash detected -- score normalized to 0
License🟢 10license file detected
Signed-Releases⚠️ -1no releases found
Fuzzing⚠️ 0project is not fuzzed
Branch-Protection🟢 3branch protection is not maximal on development and all release branches
Packaging🟢 10packaging workflow detected
SAST⚠️ 0SAST tool is not run on all commits -- score normalized to 0
npm/@vue/reactivity 3.5.32 🟢 5.4
Details
CheckScoreReason
Security-Policy🟢 10security policy file detected
Code-Review🟢 3Found 5/14 approved changesets -- score normalized to 3
Maintained🟢 1030 commit(s) and 11 issue activity found in the last 90 days -- score normalized to 10
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Token-Permissions⚠️ 0detected GitHub workflow tokens with excessive permissions
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Binary-Artifacts🟢 10no binaries found in the repo
Pinned-Dependencies⚠️ 0dependency not pinned by hash detected -- score normalized to 0
License🟢 10license file detected
Signed-Releases⚠️ -1no releases found
Fuzzing⚠️ 0project is not fuzzed
Branch-Protection🟢 3branch protection is not maximal on development and all release branches
Packaging🟢 10packaging workflow detected
SAST⚠️ 0SAST tool is not run on all commits -- score normalized to 0
npm/@vue/runtime-core 3.5.32 🟢 5.4
Details
CheckScoreReason
Security-Policy🟢 10security policy file detected
Code-Review🟢 3Found 5/14 approved changesets -- score normalized to 3
Maintained🟢 1030 commit(s) and 11 issue activity found in the last 90 days -- score normalized to 10
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Token-Permissions⚠️ 0detected GitHub workflow tokens with excessive permissions
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Binary-Artifacts🟢 10no binaries found in the repo
Pinned-Dependencies⚠️ 0dependency not pinned by hash detected -- score normalized to 0
License🟢 10license file detected
Signed-Releases⚠️ -1no releases found
Fuzzing⚠️ 0project is not fuzzed
Branch-Protection🟢 3branch protection is not maximal on development and all release branches
Packaging🟢 10packaging workflow detected
SAST⚠️ 0SAST tool is not run on all commits -- score normalized to 0
npm/@vue/runtime-dom 3.5.32 🟢 5.4
Details
CheckScoreReason
Security-Policy🟢 10security policy file detected
Code-Review🟢 3Found 5/14 approved changesets -- score normalized to 3
Maintained🟢 1030 commit(s) and 11 issue activity found in the last 90 days -- score normalized to 10
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Token-Permissions⚠️ 0detected GitHub workflow tokens with excessive permissions
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Binary-Artifacts🟢 10no binaries found in the repo
Pinned-Dependencies⚠️ 0dependency not pinned by hash detected -- score normalized to 0
License🟢 10license file detected
Signed-Releases⚠️ -1no releases found
Fuzzing⚠️ 0project is not fuzzed
Branch-Protection🟢 3branch protection is not maximal on development and all release branches
Packaging🟢 10packaging workflow detected
SAST⚠️ 0SAST tool is not run on all commits -- score normalized to 0
npm/@vue/server-renderer 3.5.32 🟢 5.4
Details
CheckScoreReason
Security-Policy🟢 10security policy file detected
Code-Review🟢 3Found 5/14 approved changesets -- score normalized to 3
Maintained🟢 1030 commit(s) and 11 issue activity found in the last 90 days -- score normalized to 10
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Token-Permissions⚠️ 0detected GitHub workflow tokens with excessive permissions
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Binary-Artifacts🟢 10no binaries found in the repo
Pinned-Dependencies⚠️ 0dependency not pinned by hash detected -- score normalized to 0
License🟢 10license file detected
Signed-Releases⚠️ -1no releases found
Fuzzing⚠️ 0project is not fuzzed
Branch-Protection🟢 3branch protection is not maximal on development and all release branches
Packaging🟢 10packaging workflow detected
SAST⚠️ 0SAST tool is not run on all commits -- score normalized to 0
npm/@vue/shared 3.5.32 🟢 5.4
Details
CheckScoreReason
Security-Policy🟢 10security policy file detected
Code-Review🟢 3Found 5/14 approved changesets -- score normalized to 3
Maintained🟢 1030 commit(s) and 11 issue activity found in the last 90 days -- score normalized to 10
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Token-Permissions⚠️ 0detected GitHub workflow tokens with excessive permissions
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Binary-Artifacts🟢 10no binaries found in the repo
Pinned-Dependencies⚠️ 0dependency not pinned by hash detected -- score normalized to 0
License🟢 10license file detected
Signed-Releases⚠️ -1no releases found
Fuzzing⚠️ 0project is not fuzzed
Branch-Protection🟢 3branch protection is not maximal on development and all release branches
Packaging🟢 10packaging workflow detected
SAST⚠️ 0SAST tool is not run on all commits -- score normalized to 0
npm/postcss 8.5.8 🟢 5.5
Details
CheckScoreReason
Security-Policy🟢 10security policy file detected
Packaging⚠️ -1packaging workflow not detected
Maintained🟢 1021 commit(s) and 1 issue activity found in the last 90 days -- score normalized to 10
Code-Review⚠️ 0Found 2/30 approved changesets -- score normalized to 0
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Token-Permissions⚠️ 0detected GitHub workflow tokens with excessive permissions
Binary-Artifacts🟢 10no binaries found in the repo
Pinned-Dependencies🟢 10all dependencies are pinned
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Fuzzing⚠️ 0project is not fuzzed
License🟢 10license file detected
Signed-Releases⚠️ -1no releases found
Branch-Protection🟢 3branch protection is not maximal on development and all release branches
SAST⚠️ 0SAST tool is not run on all commits -- score normalized to 0
npm/vue 3.5.32 🟢 5.4
Details
CheckScoreReason
Security-Policy🟢 10security policy file detected
Code-Review🟢 3Found 5/14 approved changesets -- score normalized to 3
Maintained🟢 1030 commit(s) and 11 issue activity found in the last 90 days -- score normalized to 10
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Token-Permissions⚠️ 0detected GitHub workflow tokens with excessive permissions
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Binary-Artifacts🟢 10no binaries found in the repo
Pinned-Dependencies⚠️ 0dependency not pinned by hash detected -- score normalized to 0
License🟢 10license file detected
Signed-Releases⚠️ -1no releases found
Fuzzing⚠️ 0project is not fuzzed
Branch-Protection🟢 3branch protection is not maximal on development and all release branches
Packaging🟢 10packaging workflow detected
SAST⚠️ 0SAST tool is not run on all commits -- score normalized to 0

Scanned Files

  • docs/package.json
  • pnpm-lock.yaml

@arielsalminen arielsalminen self-assigned this Apr 14, 2026
@arielsalminen arielsalminen added the enhancement New feature or request label Apr 14, 2026
@arielsalminen arielsalminen changed the title [feat]: Light DOM slot() for Primitive Components [feat]: Light DOM slot() Apr 14, 2026
@arielsalminen arielsalminen changed the title [feat]: Light DOM slot() [feat]: Light DOM slot() for Primitive Components Apr 14, 2026
@arielsalminen arielsalminen marked this pull request as draft April 14, 2026 17:59
@arielsalminen arielsalminen marked this pull request as ready for review April 14, 2026 18:12
Comment thread packages/core/src/elena.js
Comment thread packages/core/src/common/utils.js Outdated
@arielsalminen arielsalminen marked this pull request as draft April 14, 2026 19:04
Comment thread docs/components/templates.md
@arielsalminen arielsalminen marked this pull request as ready for review April 15, 2026 07:30
@arielsalminen arielsalminen changed the title [feat]: Light DOM slot() for Primitive Components [feat]: Light DOM slot() Apr 15, 2026
@arielsalminen arielsalminen changed the title [feat]: Light DOM slot() [feat]: Light DOM <slot/> substitute Apr 15, 2026
@sorvell
Copy link
Copy Markdown

sorvell commented Apr 15, 2026

I would not recommend vending this. It's too easy to confuse with the actual slot element and expect it to work like it does. It does not and cannot without a full polyfill.

Here is a codepen that I tried with your linked preview above and it didn't work. This is just an example of the can of worms this opens. Feel free to explore of course, but I think you'll likely conclude that this is too weird and there are just too many edge cases.

Instead, for composable light DOM rendering I think your text property is the right direction and it should just support nodes and maybe be renamed.

@arielsalminen
Copy link
Copy Markdown
Member Author

arielsalminen commented Apr 15, 2026

@sorvell yeah, I am starting to think that this may be too difficult path to go down without also accounting for a million edge cases.

I did get the example you shared working locally btw (the codepen you linked is using the old release from unpgk (import { Elena, html, slot } from "https://unpkg.com/@elenajs/core/bundle";), but it does reveal locally other kind of issues which I don’t think are worth trying to solve.

Thanks for the good feedback so far! Will explore the other direction a tad later in a separate PR.

@arielsalminen arielsalminen marked this pull request as draft April 15, 2026 18:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[question]: how does it work without slots? [feat]: Support dynamic textContent with Primitive Components

2 participants