在 Child 中放置模态 Vue.js 中的组件

Placing Modals in Child Components in Vue.js

背景

通常,在 Vue.js 组件中使用模式时,创建可重用的 modal 组件,然后使用来自 child 组件的事件控制该组件的状态是一种规范.

例如,考虑以下代码:

App.vue

<div id="app">

    <!-- Main Application content here... -->

    <!-- Place any modal components here... -->
    <modal ref="ContactForm"></modal>

</div>

ChildComponent.Vue

要从 child 组件打开模式,我们只需触发以下事件:

bus.$emit('open-modal', 'ContactForm');

注意 1: bus 是一个单独的 Vue 实例,它允许在所有组件之间触发事件,而不管它们之间的关系如何。

注 2: 我故意遗漏了我的 modal 组件代码,因为它与问题无关。

问题

虽然上面的工作绝对没问题,但有一个关键问题...

为了将 modal 添加到我的应用程序,而不是将 modal 组件放置在引用它的 child 组件中,我必须放置 App.vue 中的所有 模态框,因为这确保它们在 DOM 树中尽可能高(以确保它们出现在所有内容之上)。

因此,我的 App.vue 有可能最终看起来像这样:

<div id="app">

    <!-- Main Application content here... -->

    <!-- Place any modal components here... -->
    <modal ref="SomeModal1"></modal>
    <modal ref="SomeModal2"></modal>
    <modal ref="SomeModal3"></modal>
    <modal ref="SomeModal4"></modal>
    <modal ref="SomeModal5"></modal>
    <modal ref="SomeModal6"></modal>
    <modal ref="SomeModal7"></modal>

</div>

如果能够将 modal 组件放在 child 组件的 DOM 中会更简洁。

但是,为了确保模式出现在 DOM 中的所有内容之上(特别是带有集合 z-index 的项目),我看不到上面的替代方法。 .

任何人都可以建议一种方法来确保我的模式能够正常工作,即使它们被放置在 child 组件中吗?

可能的解决方案

我确实考虑过以下解决方案,但它似乎很脏......

  1. 触发 open-modal 事件
  2. 将相关的 modal 组件移动到 parent App.vue 组件
  3. 显示模态

附加信息

如果以上内容不清楚,我试图避免在我的 App.vue 组件中定义 all 模态,并允许在任何 child组件。

目前我不能这样做的原因是,模态框的 HTML 必须出现在 DOM 树中尽可能高的位置,以确保它们出现在上方所有内容。

我将模态框放在我的子组件中,效果很好。我的模态实现与 modal example from the docs. I also added in basic a11y features including vue-focus-lock 基本相似,但思想是相同的。

没有事件总线、共享状态或引用 - 只是 v-if 在需要时存在模态。

这就是我所说的:

在助手中创建一个 addProgrammaticComponent 函数,按照以下行:

import Vue from 'vue';

export function addProgrammaticComponent(parent, component, dataFn, extraProps = {}) {
  const ComponentClass = Vue.extend(component);
  // this can probably be simplified. 
  // It largely depends on how much flexibility you need in building your component
  // gist being: dynamically add props and data at $mount time
  const initData = dataFn ? dataFn() : {};
  const data = {};
  const propsData = {};
  const propKeys = Object.keys(ComponentClass.options.props || {});

  Object.keys(initData).forEach((key) => {
    if (propKeys.includes(key)) {
      propsData[key] = initData[key];
    } else {
      data[key] = initData[key];
    }
  });

  // add store props if you use Vuex

  // extraProps can include dynamic methods or computed, which will be merged
  // onto what has been defined in the .vue file

  const instance = new ComponentClass({
    /* store, */ data, propsData, ...extraProps,
  });

  instance.$mount(document.createElement('div'));

  // generic helper for passing data to/from parent:
  const dataSetter = (data) => {
    Object.keys(data).forEach((key) => {
        instance[key] = data[key];
    });
  };

  // set unwatch on parent as you call it after you destroy the instance
  const unwatch = parent.$watch(dataFn || {}, dataSetter);

  return {
    instance,
    update: () => dataSetter(dataFn ? dataFn() : {}),
    dispose: () => {
        unwatch();
        instance.$destroy();
    },
  };
}

...现在,你在哪里使用它:

Modal.vue 是典型的 modal component,但您可以通过关闭 EscDel[=52= 来加强它] 按键等...

你想打开模式的地方:

 methods: {
   openFancyModal() {
     const component = addProgrammaticComponent(
       this,
       Modal,
       () => ({
         title: 'Some title',
         message: 'Some message',
         show: true,
         allowDismiss: true,
         /* any other props you want to pass to the programmatic component... */
       }),
     );

     document.body.appendChild(component.instance.$el);

     // here you have full access to both the programmatic component 
     // as well as the parent, so you can add logic

     component.instance.$once('close', component.dispose);

     // if you don't want to destroy the instance, just hide it
     component.instance.$on('cancel', () => {
       component.instance.show = false;
     });

     // define any number of events and listen to them: i.e:
     component.instance.$on('confirm', (args) => {
       component.instance.show = false;
       this.parentMethod(args);
     });
   },
   /* args from programmatic component */
   parentMethod(args) {
     /* you can even pass on the component itself, 
        and .dispose() when you no longer need it */
   }
 }    

也就是说,没有人会阻止您创建多个 Modal/Dialog/Popup 组件,因为它可能有不同的模板,或者因为它可能有重要的附加功能会污染通用 Modal组件(即:LoginModal.vueAddReportModal.vueAddUserModal.vueAddCommentModal.vue)。

这里的要点是:它们不会添加到应用程序中(添加到 DOM),直到您真正 $mount 它们。您不将标记放在父组件中。并且可以在开头定义fn传什么道具,听什么等...

除了在父级触发的unwatch方法外,所有事件都绑定到programmaticComponent实例,所以没有垃圾。

这就是我所说的,在您打开 DOM 之前,没有真正的隐藏模态实例潜伏在它上面。

甚至不能说这种方法一定比其他方法好(但它有一些优点)。从我的 POV 来看,它只是受到 Vue 的灵活性和核心原则的启发,这显然是可能的,并且它允许灵活地 .$mount 和处置任何组件(不仅是模态)onto/from 任何组件。

当您需要从同一个复杂应用的多个角落打开同一个组件并且您认真对待 DRY 时,它特别有用。

参见 vm.$mount 文档。