【问题标题】:Placing Modals in Child Components in Vue.js在 Vue.js 的子组件中放置 Modals
【发布时间】:2020-01-17 17:10:17
【问题描述】:

背景

通常,在 Vue.js 组件中使用模态框时,通常会创建一个可重用的modal 组件,然后使用来自子组件的事件来控制该组件的状态。

例如,考虑以下代码:

App.vue

<div id="app">

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

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

</div>

ChildComponent.Vue

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

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

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

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

问题

虽然上述工作绝对正常,但有一个关键问题......

为了将modal 添加到我的应用程序中,而不是将modal 组件放在引用它的子组件中,我必须将all 模态框放在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 组件放置在子组件的DOM 中会更简洁。

但是,为了确保模态显示在 DOM 中的所有内容之上(特别是带有集合 z-index 的项目),我看不到上述内容的替代方案...

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

潜在解决方案

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

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

附加信息

如果上述内容不明确,我会尽量避免在我的 App.vue 组件中定义 all 模态,并允许在任何子组件中定义我的模态。

目前我无法做到这一点的原因是,模态框的 HTML 必须在 DOM 树中尽可能高地出现,以确保它们出现在所有内容之上。

【问题讨论】:

  • 正如您所说的,“规范”是重用。例如,您只有一个模态实例,并且在打开它时动态注入内容。事实上,今天的规范是没有任何模态实例,并在需要时以编程方式创建一个,并在将回调响应传递给其触发组件后将其销毁。如果你最终得到了一堆模态实例,那么你就没有重用。你在硬编码。
  • @AndreiGheorghiu - 我措辞不好,你解释它的方式正是我的模态组件的工作方式。由于每个modal 都是有条件地呈现的,它们不会同时出现在DOM 中。
  • 在这种情况下,您只需将&lt;body&gt; 设置为模态实例的目标元素,当您追加它时。这就是避免任何z-index 问题的方法。
  • @AndreiGheorghiu - 等等,我有点困惑。 target 元素是什么意思? modal 不是由我的 JS 创建的,它是由 Vue.js 有条件地呈现的吗
  • 您有没有.modal { z-index: 2000;} 不起作用的用例?对于现代浏览器,z-index 的理论最大值约为 20 亿。所以我不再担心它在 DOM 中的位置,而是在有意义的地方添加组件。

标签: javascript css vue.js modal-dialog


【解决方案1】:

这就是我所说的:

在帮助程序中创建一个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 按键等...

您要在哪里打开模态框:

 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方法,在parent上触发,所有事件都绑定到programmaticComponent实例,所以没有垃圾。

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

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

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

请参阅vm.$mount docs。

【讨论】:

  • 谢谢,这是一个非常有趣的方法。本质上,您是在将组件动态创建到 JS 中,而不是 DOM 中,从而保持它的反应性。然后你将它附加到 DOM 任何你需要的地方,即 body 元素。根据我的研究,我认为这可能是实现所需目标的唯一方法。暂时 +1,但一旦其他人有机会参与,就会接受:-D
  • @Ben,我将此方法重用于模式、对话框、弹出窗口和为电子邮件呈现 HTML(在电子邮件的情况下,我根本没有将 programmaticComponent 添加到 DOM。只是等待所有资产加载,所以我可以对电子邮件图像内联样式属性进行硬编码 - (感谢 Outlook!:))并发送。
【解决方案2】:

我将模态框放在我的子组件中,效果很好。我的模态实现与文档中的modal example 基本相似。我还添加了基本的 a11y 功能,包括 vue-focus-lock,但想法是一样的。

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

【讨论】:

  • 感谢您的回答,但是,它并没有解决我的问题。我的 modal 实现与 Vue.js 示例非常相似。我能够将我的模态定义放在我的子组件中,从技术上讲,它们可以工作。问题是通过将它们放在我的子组件中,Vue 将它们呈现在 DOM 中的那个位置,而不是在 DOM 树的最高点。当模态框的父元素具有 z-index 值集,并且您有兄弟姐妹或其他具有 z-index 集的子元素时,这会导致问题。
  • 但这似乎只是一个 CSS 问题。为什么不将position: fixed 与大量z-index 一起使用?
  • 不幸的是,这不仅仅是一个 CSS 问题。考虑将z-index 设置为10div,然后模态在此div 内。 modal 中的最大 z-index 将是 10。您可以将z-index 设置为10000000,但它仍然只与div 中的项目相关。相信我,我已经测试过了...... :-D
  • 是的,我明白你的意思。这是谈论here。如果你不能移除父级的堆叠上下文,那么这很棘手......
猜你喜欢
  • 2021-02-26
  • 2016-12-21
  • 2016-11-14
  • 1970-01-01
  • 2022-01-11
  • 2019-10-10
  • 2017-10-22
  • 2015-05-01
  • 2018-04-18
相关资源
最近更新 更多