如何从JSON对象构建Lit元素的插槽层级结构?
问题描述
我想基于单个JSON数据对象构建Lit元素的层级结构,要求每层子元素都放入父元素对应的命名插槽中(比如library→books→pages的层级)。目前pages能正确匹配books的<slot name='pages'>插槽,但动态渲染的books无法进入library的<slot name='books'>插槽,直接出现在library的Shadow DOM里;手动编写的<test-book>能正常被插槽拾取,但没法实现动态生成。想问下能不能用类似下面的代码把动态生成的books移出Shadow DOM,正确分配到目标插槽?
library.appendChild(book); library.shadowRoot.querySelector("slot[name='books']").assign(book);
代码示例
test-library.js
import {html, css, LitElement} from 'lit'; export class Library extends LitElement { static styles = css`p { color: blue }`; static properties = { name: {type: String}, items: {type: Object} }; constructor() { super(); this.items = { "library": [ { "book": [ { "page": "p1" }, { "page": "p2" } ]}, { "book": [ { "page": "p1" }, { "page": "p2" } ]}, ] }; } render() { return html` <div id="library"> I'm a library <slot name='books'></slot> </div> ${this.items.library.map((book) => html` <test-book slot='books'> <div slot='pages'> ${book.book.map((page) => html` <test-page slot='pages'>${page.page}</test-page> `)} </div> </test-book> `)} `; } } customElements.define('test-library', Library);
test-book.js
import {html, css, LitElement} from 'lit'; export class Book extends LitElement { static styles = css`p { color: blue }`; static properties = { name: {type: String} }; constructor() { super(); } render() { return html` <div id="book">I'm a book <slot name='pages'></slot> </div> `; } } customElements.define('test-book', Book);
test-page.js
import {html, css, LitElement} from 'lit'; export class Page extends LitElement { static styles = css`p { color: blue }`; static properties = { name: {type: String} }; constructor() { super(); } render() { return html` <div id="page">Hey there, I'm a page in a slot</div> `; } } customElements.define('test-page', Page);
index.html
<!DOCTYPE html> <head> <script type="module" src="./test-library.js"></script> <script type="module" src="./test-book.js"></script> <script type="module" src="./test-page.js"></script> </head> <body> <test-library name='test'> <!-- 手动写的book能被插槽正确拾取,但没法动态生成 --> <test-book slot='books'>I'm a manually html-coded book</test-book> </test-library> </body>
问题分析与解决方法
核心问题
你现在的写法里,动态生成的<test-book>是直接渲染在Library组件的Shadow DOM内部的,但插槽的工作规则是:只有作为组件Light DOM(即组件的直接子节点,不在Shadow DOM里)的元素,才能被对应命名插槽拾取。Shadow DOM里的元素不会参与插槽匹配,所以这些动态book才会直接出现在Shadow DOM中,而非进入<slot name='books'>。
你的代码思路问题
你尝试的library.appendChild(book)是对的(把元素加到Light DOM),但slot.assign(book)完全用错了——slot.assign()是用来给匿名插槽设置备用内容的,和命名插槽的元素绑定无关。命名插槽是通过元素的slot属性和插槽的name属性自动匹配的,不需要手动调用assign方法。
正确实现方式
要让动态元素被插槽正确拾取,必须把它们放到组件的Light DOM里,以下是两种可行的Lit风格实现:
方法1:在组件生命周期中动态添加Light DOM子元素
修改Library组件,在connectedCallback中动态创建并添加<test-book>到自身(Light DOM),同时设置正确的slot属性:
import {html, css, LitElement} from 'lit'; export class Library extends LitElement { static styles = css`p { color: blue }`; static properties = { name: {type: String}, items: {type: Object} }; constructor() { super(); this.items = { "library": [ { "book": [ { "page": "p1" }, { "page": "p2" } ]}, { "book": [ { "page": "p1" }, { "page": "p2" } ]}, ] }; } connectedCallback() { super.connectedCallback(); this._renderBooksToLightDOM(); } _renderBooksToLightDOM() { // 先清空已有动态book,避免重复添加 this.querySelectorAll('test-book[slot="books"]').forEach(el => el.remove()); this.items.library.forEach(bookData => { // 创建book元素并设置slot属性 const bookEl = document.createElement('test-book'); bookEl.setAttribute('slot', 'books'); // 创建pages容器并设置slot属性 const pagesContainer = document.createElement('div'); pagesContainer.setAttribute('slot', 'pages'); // 创建page元素并添加到容器 bookData.book.forEach(pageData => { const pageEl = document.createElement('test-page'); pageEl.setAttribute('slot', 'pages'); pageEl.textContent = pageData.page; pagesContainer.appendChild(pageEl); }); bookEl.appendChild(pagesContainer); this.appendChild(bookEl); }); } render() { // 只渲染Shadow DOM的结构,不再动态生成book return html` <div id="library"> I'm a library <slot name='books'></slot> </div> `; } } customElements.define('test-library', Library);
方法2:数据驱动父级渲染Light DOM元素
如果场景允许,也可以把books数据提升到父级上下文(比如index.html对应的脚本),在父级动态生成<test-book>并作为<test-library>的子元素,这样天然就是Light DOM,会被插槽自动匹配:
<!DOCTYPE html> <head> <script type="module" src="./test-library.js"></script> <script type="module" src="./test-book.js"></script> <script type="module" src="./test-page.js"></script> </head> <body> <test-library name='test' id="myLibrary"></test-library> <script type="module"> const library = document.getElementById('myLibrary'); const booksData = [ { "book": [{"page": "p1"}, {"page": "p2"}]}, { "book": [{"page": "p1"}, {"page": "p2"}]} ]; booksData.forEach(bookData => { const bookEl = document.createElement('test-book'); bookEl.setAttribute('slot', 'books'); const pagesContainer = document.createElement('div'); pagesContainer.setAttribute('slot', 'pages'); bookData.book.forEach(pageData => { const pageEl = document.createElement('test-page'); pageEl.setAttribute('slot', 'pages'); pageEl.textContent = pageData.page; pagesContainer.appendChild(pageEl); }); bookEl.appendChild(pagesContainer); library.appendChild(bookEl); }); </script> </body>
关键总结
- 命名插槽只识别组件Light DOM中的子元素,Shadow DOM内部的元素无法被匹配
slot.assign()不是用来绑定命名插槽元素的,别用错- 动态生成的元素必须作为组件的直接子节点(Light DOM),并设置正确的
slot属性,才能被对应插槽渲染
内容的提问来源于stack exchange,提问作者html_css_js

