Vue:传送 inheritAttrs false 不起作用

Vue: Teleport inheritAttrs false not working

我正在尝试创建一个 BaseOverlay 组件,基本上将其内容传送到我的应用程序的特定区域。它工作得很好,除了在 v-show 中使用它时出现问题...我认为因为我的组件的根是 Teleport v-show 将无法工作,因为 Teleport 是一个模板。

我想我可以在内部内容上使用 inheritAttrs: falsev-bind="$attrs"...这会引发 Vue 的警告说 Runtime directive used on component with non-element root node. The directives will not function as intended. 它导致 v-show 不在 MyComponent 上工作,但 v-if 确实有效。

关于我做错了什么的任何线索?

Example

App.vue

<script setup>
import MyComponent from "./MyComponent.vue";
import {ref} from "vue";
  
const showOverlay = ref(false);
function onClickButton() {
  showOverlay.value = !showOverlay.value;
}
</script>

<template>
  <button @click="onClickButton">
    Toggle Showing
  </button>
  <div id="overlays" />
  <div>
    Hello World
  </div>
  <MyComponent v-show="showOverlay" text="Doesn't work" />
  <MyComponent v-if="showOverlay" text="Works" />
</template>

BaseOverlay.vue

<template>
  <Teleport to="#overlays">
    <div
      class="overlay-container"
      v-bind="$attrs"
    >
      <slot />
    </div>
  </Teleport>
</template>

<script>
export default {
  name: "BaseOverlay",
  inheritAttrs: false,
};
</script>

MyComponent.vue

<template>
    <BaseOverlay>
    {{text}}
  </BaseOverlay>
</template>

<script>
import BaseOverlay from "./BaseOverlay.vue";

export default {
  name: "MyComponent",
  components: {
    BaseOverlay
  },
  props: {
    text: {
      type: String,
      default: ""
    }
  }
}
</script>

我会考虑将 modal/overlay 依赖项移出组件并移至应用组合中,以使其更易于重用。

注意 isMounted check 检查 - 这是为了在出口容器尚未定义的情况下增加延迟。如果您的插座没有安装在支架上,您可能需要添加额外的处理,例如<div id="outlet" v-if="false">

const {
  createApp,
  defineComponent,
  ref,
  onMounted
} = Vue


// 
// Modal 
// 
const MyModal = defineComponent({
  name: 'MyModal',
  props: {
    to: {
      type: String,
      required: true
    },
    show: Boolean
  },
  setup(){
    const isMounted = ref(false);
    onMounted(()=> isMounted.value = true )
    return { isMounted }
  },
  template: `
    <teleport :to="to" v-if="isMounted">
      <div class="overlay-container" v-show="show">
        <slot />
      </div>
    </teleport>
  `
})


// 
// Component 
// 
const MyComp = defineComponent({
  name: 'MyComp',
  template: `<div>Component</div>`
})




// 
// App 
// 
const MyApp = defineComponent({
  name: 'MyApp',
  components: {
    MyModal,
    MyComp
  },
  setup() {
    const modalShow = ref(false);
    const modalOutlet = ref('#outletOne');

    const toggleModalShow = () => {
      modalShow.value = !modalShow.value
    }
    const toggleModalOutlet = () => {
      modalOutlet.value = modalOutlet.value == '#outletOne' 
        ? '#outletTwo' 
        : '#outletOne'
    }
    return {
      toggleModalShow,
      modalShow,
      toggleModalOutlet,
      modalOutlet,
    }
  },
  template: `
    <div>
      <button @click="toggleModalShow">{{ modalShow ? 'Hide' : 'Show' }} Modal</button>
      <button @click="toggleModalOutlet">Toggle Modal Outlet {{ modalOutlet }} </button>    
    </div>
 

    <MyModal :to="modalOutlet" :show="modalShow">
      <MyComp /> 
    </MyModal>

    <div id="outletOne">
      <h2>Outlet One</h2>
      <!-- outlet one -->
    </div>

    <div id="outletTwo">
      <h2>Outlet Two</h2>   
      <!-- outlet two -->
    </div>

  `
})



//
// Assemble
// 
const app = createApp(MyApp)
app.mount('body')
/* just some styling */
#outletOne { color: tomato; }
#outletTwo { color: olive; }
h2 { margin: 0; }
[id*=outlet]{ display: inline-flex; flex-direction: column; padding: 1rem; }
button { margin: 1rem; }
<script src="https://unpkg.com/vue@3.1.4/dist/vue.global.js"></script>

想 follow-up 对此。我开始 运行 解决了 Teleport 的许多其他问题,例如无法将其用作组件中的根元素(例如 Dialog 组件,因为我想将所有对话框传送到特定区域应用程序),以及 KeepAlive.

的一些其他奇怪问题

我最终推出了自己的 WebComponent 并使用了它。我有一个在 BaseOverlay 组件中使用的 OverlayManager WebComponent,每次安装 BaseOverlay 时,它都会将自己添加到 OverlayManager.

Example

OverlayManager.js

export class OverlayManager extends HTMLElement {
  constructor() {
    super();
    this.classList.add("absolute", "top-100", "left-0")
    document.body.appendChild(this);
  }
  
  add(element) {
    this.appendChild(element);
  }
  
  remove(element) {
    this.removeChild(element);
  }
}

customElements.define("overlay-manager", OverlayManager);

BaseOverlay.vue

<template>
  <div class="overlay-container" ref="rootEl">
    <slot />
  </div>
</template>

<script>
import {ref, onMounted, onBeforeUnmount, inject} from "vue";

  export default {
  name: "BaseOverlay",
  setup() {
    const rootEl = ref(null);
    const OverlayManager = inject("OverlayManager");
    onMounted(() => {
      OverlayManager.add(rootEl.value);
    });
    onBeforeUnmount(() => {
      OverlayManager.remove(rootEl.value);
    });
    return {
      rootEl
    }
  }
};
</script>

App.vue

<script setup>
import {OverlayManager} from "./OverlayManager.js";
import MyComponent from "./MyComponent.vue";
import {ref, provide} from "vue";
  
const manager = new OverlayManager();
provide("OverlayManager", manager);
  
const showOverlay = ref(false);
function onClickButton() {
  showOverlay.value = !showOverlay.value;
}
</script>

<template>
  <button @click="onClickButton">
    Toggle Showing
  </button>
  <div>
    Hello World
  </div>
  <MyComponent v-show="showOverlay" text="Now Works" />
  <MyComponent v-if="showOverlay" text="Works" />
</template>

<style>
  .absolute {
    position: absolute;
  }
  .top-100 {
    top: 100px;
  }
  .left-0 {
    left: 0;
  }
</style>

这完全符合我的需要,我不必处理 Teleport 引入的怪癖,它允许我有一个负责我所有覆盖的单例。另一个好处是我可以访问 BaseOverlay 最初添加到 HTML 的父级(而不是它移动的位置)。老实说,我不确定这是否是一个好的做法,但我对它的可爱程度以及 Vue 与它的集成程度感到满意。