You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Stencil.js中如何移动嵌套插槽子组件至父组件且不丢失引用

问题核心原因

嵌套的<ifx-navbar-item>属于父<ifx-navbar-item>的内部内容,它们的slot属性仅对父组件生效,而非外层的<ifx-navbar>。外层Navbar的插槽(如first-layer)只能捕获直接子元素的slot标记,无法感知嵌套在子组件Shadow DOM中的元素,所以修改嵌套item的slot属性不会让它们被外层Navbar的移动端容器接收。

解决方案

方案一:组件注册+层级标记

通过组件间通信让所有层级的navbar-item向外层Navbar注册,再根据层级分配对应的移动端插槽。

  1. 修改<ifx-navbar-item>组件,添加注册逻辑:
componentDidLoad() {
  const parentNavbar = this.el.closest('ifx-navbar');
  if (!parentNavbar) return;

  // 计算当前层级:顶级item为1,嵌套子item层级+1
  const parentItem = this.el.closest('ifx-navbar-item');
  const level = parentItem === this.el ? 1 : parseInt(parentItem.dataset.navLevel) + 1;
  this.el.dataset.navLevel = level;
  // 记录原父元素,用于桌面模式恢复
  this.el.dataset.parentRef = parentItem ? parentItem : null;

  // 向Navbar注册自身
  parentNavbar.registerNavItem(this.el);

  // 递归注册嵌套子item
  const childItems = this.el.shadowRoot.querySelectorAll('ifx-navbar-item');
  childItems.forEach(item => item.componentDidLoad());
}
  1. 修改<ifx-navbar>组件,维护注册列表并处理视口切换:
navItems = [];

registerNavItem(item) {
  // 避免重复注册
  if (!this.navItems.includes(item)) {
    this.navItems.push(item);
  }
}

componentWillLoad() {
  const mediaQueryList = window.matchMedia('(max-width: 800px)');
  mediaQueryList.addEventListener('change', (e) => this.moveNavItemsToSidebar(e));
  // 初始加载时执行一次
  this.moveNavItemsToSidebar(mediaQueryList);
}

moveNavItemsToSidebar(e) {
  if (e.matches) {
    this.navItems.forEach(item => {
      const level = parseInt(item.dataset.navLevel);
      // 第一层item放入mobile-menu-top,其余放入first-layer(可扩展多层)
      const targetSlot = level === 1 ? 'mobile-menu-top' : 'first-layer';
      // 将item移动到Navbar直接子级,才能被外层插槽捕获
      this.el.appendChild(item);
      item.setAttribute('slot', targetSlot);
    });
  } else {
    this.navItems.forEach(item => {
      const parentRef = item.dataset.parentRef;
      if (parentRef) {
        // 移回原父组件的Shadow DOM
        parentRef.shadowRoot.appendChild(item);
      } else {
        // 顶级item移回left-item插槽
        this.el.appendChild(item);
        item.setAttribute('slot', 'left-item');
      }
      // 清除移动端slot标记
      item.removeAttribute('slot');
    });
  }
}

方案二:DOM迁移(直接操作节点)

在视口切换时,将嵌套的item从父组件Shadow DOM中迁移到Navbar直接子级,恢复时移回原位置。

修改<ifx-navbar>的moveNavItemsToSidebar和migrateChildItems方法:

moveNavItemsToSidebar(e) {
  if (e.matches) {
    const leftMenuItems = this.el.querySelectorAll('[slot="left-item"]');
    leftMenuItems.forEach(item => {
      item.setAttribute('slot', 'mobile-menu-top');
      // 给顶级item添加唯一标识,用于恢复时定位
      item.dataset.navId = `nav-${Date.now()}-${Math.random()}`;
      // 递归迁移嵌套子item
      this.migrateChildItems(item, 'first-layer');
    });
  } else {
    const mobileItems = this.el.querySelectorAll('[slot="mobile-menu-top"], [slot="first-layer"]');
    mobileItems.forEach(item => {
      const originalParentSelector = item.dataset.originalParent;
      if (originalParentSelector) {
        const originalParent = document.querySelector(originalParentSelector);
        originalParent.shadowRoot.appendChild(item);
        item.removeAttribute('slot');
        delete item.dataset.originalParent;
      } else {
        item.setAttribute('slot', 'left-item');
        delete item.dataset.navId;
      }
    });
  }
}

migrateChildItems(parentItem, targetSlot) {
  const childItems = parentItem.shadowRoot.querySelectorAll('ifx-navbar-item');
  childItems.forEach(item => {
    // 记录原父组件的选择器,用于恢复
    item.dataset.originalParent = `[data-nav-id="${parentItem.dataset.navId}"]`;
    // 移到Navbar直接子级并设置slot
    this.el.appendChild(item);
    item.setAttribute('slot', targetSlot);
    // 递归处理更深层级的子item
    this.migrateChildItems(item, targetSlot); // 可根据层级设置不同slot
  });
}
关键说明
  • 外层Navbar的插槽只能捕获直接子元素的slot属性,所以必须将嵌套item移动到Navbar的直接子级才能被移动端容器接收。
  • 无论哪种方案,都需要记录嵌套item的原始父元素,以便在桌面模式下恢复原结构,避免丢失引用。

内容的提问来源于stack exchange,提问作者happy_story

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.29 16:02:01