Vue JS - 无法将使用innerHTML动态创建的组件绑定到事件

3

最近我刚开始学习Vue JS - 迄今为止很喜欢它。现在我面临的问题是,我正在尝试创建一个(非常复杂的)表格(使用vue-good-table插件),其中每个单元格都是一个独立的组件。

根据插件文档的说明,可以创建HTML列类型,您只需使用原始HTML即可:

https://xaksis.github.io/vue-good-table/guide/configuration/column-options.html#html

简化起见,这是我的情况 - 一个包含表格和名为Test.vue的子组件的Vue组件(称为Dashboard2.vue)。

我正在为每个相关单元格动态创建Test组件,并将其分配给相关行单元格。由于我已将列定义为HTML类型,因此我正在使用innerHTML属性从Vue组件中提取原始HTML。 (按照本文 https://css-tricks.com/creating-vue-js-component-instances-programmatically/),一切都进行得很顺利,仪表板看起来正好符合我的要求,但是当单击每个Test组件内的按钮时,什么也没有发生。

我怀疑由于我使用了innerHTML属性,它会以某种方式跳过Vue事件处理程序机制,所以我有些困惑。

以下是相关组件部分:

Dashboard2.vue:

<template>
  <div>
    <vue-good-table
      :columns="columns"
      :rows="rows"
      :search-options="{enabled: true}"
      styleClass="vgt-table condensed bordered"
      max-height="700px"
      :fixed-header="true"
      theme="black-rhino">
    </vue-good-table>
  </div>
</template>

<script>
import axios from 'axios';
import Vue from 'vue';
import { serverURL } from './Config.vue';
import Test from './Test.vue';

export default {
  name: 'Dashboard2',
  data() {
    return {
      jobName: 'team_regression_suite_for_mgmt',
      lastXBuilds: 7,
      builds: [],
      columns: [
        {
          label: 'Test Name',
          field: 'testName',
        },
      ],
      rows: [],
    };
  },
  methods: {
    fetchResults() {
      const path = `${serverURL}/builds?name=${this.jobName}&last_x_builds=${this.lastXBuilds}`;
      axios.get(path)
        .then((res) => {
          this.builds = res.data;
          this.builds.forEach(this.createColumnByBuildName);
          this.createTestsColumn();
          this.fillTable();
        })
        .catch((error) => {
          // eslint-disable-next-line no-console
          console.error(error);
        });
    },
    createBaseRow(build) {
      return {
        id: build.id,
        name: build.name,
        cluster: build.resource_name,
        startTime: build.timestamp,
        runtime: build.duration_min,
        estimatedRuntime: build.estimated_duration_min,
        result: build.result,
      };
    },
    addChildRows(build, children) {
      const row = this.createBaseRow(build);
      // eslint-disable-next-line no-plusplus
      for (let i = 0; i < build.sub_builds.length; i++) {
        const currentBuild = build.sub_builds[i];
        if (currentBuild.name === '') {
          this.addChildRows(currentBuild, children);
        } else {
          children.push(this.addChildRows(currentBuild, children));
        }
      }
      return row;
    },
    createColumnByBuildName(build) {
      this.columns.push({ label: build.name, field: build.id, html: true });
    },
    addRow(build) {
      const row = this.createBaseRow(build);
      row.children = [];
      this.addChildRows(build, row.children);
      this.rows.push(row);
    },
    createTestsColumn() {
      const build = this.builds[0];
      const row = this.createBaseRow(build);
      row.children = [];
      this.addChildRows(build, row.children);
      // eslint-disable-next-line no-plusplus
      for (let i = 0; i < row.children.length; i++) {
        this.rows.push({ testName: row.children[i].name });
      }
    },
    fillBuildColumn(build) {
      const row = this.createBaseRow(build);
      row.children = [];
      this.addChildRows(build, row.children);
      // eslint-disable-next-line no-plusplus
      for (let i = 0; i < row.children.length; i++) {
        const childBuild = row.children[i];
        const TestSlot = Vue.extend(Test);
        const instance = new TestSlot({
          propsData: {
            testName: childBuild.name,
            result: childBuild.result,
            runTime: childBuild.runtime.toString(),
            startTime: childBuild.startTime,
            estimatedRunTime: childBuild.estimatedRuntime.toString(),
          },
        });
        instance.$mount();
        this.rows[i] = Object.assign(this.rows[i], { [build.id]: instance.$el.innerHTML });
      }
    },
    fillTable() {
      this.builds.forEach(this.fillBuildColumn);
    },
  },
  created() {
    this.fetchResults();
  },
};
</script>

<style scoped>

</style>

Test.vue

<template>
    <div>
  <b-card :header="result" class="mb-2" :bg-variant="variant"
          text-variant="white">
    <b-card-text>Started: {{ startTime }}<br>
      Runtime: {{ runTime }} min<br>
      Estimated: {{ estimatedRunTime }} min
    </b-card-text>
    <b-button @click="sayHi" variant="primary">Hi</b-button>
  </b-card>
</div>
</template>

<script>
export default {
  name: 'Test',
  props: {
    id: String,
    testName: String,
    build: String,
    cluster: String,
    startTime: String,
    runTime: String,
    estimatedRunTime: String,
    result: String,
  },
  computed: {
    variant() {
      if (this.result === 'SUCCESS') { return 'success'; }
      if (this.result === 'FAILURE') { return 'danger'; }
      if (this.result === 'ABORTED') { return 'warning'; }
      if (this.result === 'RUNNING') { return 'info'; }
      return 'info';
    },
  },
  methods: {
    sayHi() {
      alert('hi');
    },
  },
};
</script>

<style scoped>

</style>

我知道这是很多代码。 具体相关部分(在Dashboard2.vue中)是fillBuildColumn

再次强调 - 我是Vue JS的新手 - 但我的直觉告诉我,在这里我做了很多错误的事情。

任何帮助都将不胜感激。

编辑

通过去除innerHTML属性和html类型,我得到了一个:

浏览器抛出“RangeError:超出最大调用堆栈大小”。 不确定是什么原因。


你尝试过使用 refs 来获取元素,而不是使用原生的 js 吗? - Michael
@Michael 不确定如何将其应用于我的用例。 - Ben
这样只渲染HTML部分。您没有获取单元格元素,也没有将组件附加为子级。您只是设置了组件的外观。有避免使用插槽的原因吗? - Eldar
@Eldar 没有任何原因 - 不确定如何在方法内动态创建组件后应用它们。从我手头拿到组件实例的角度出发,不确定如何将其附加/应用到插槽上。 - Ben
为什么需要动态创建组件?将其放入模板并使用行数据填充其属性不能处理您的情况吗? - Eldar
显示剩余3条评论
1个回答

2

我制作了一个CodeSandbox示例。可能会有数据部分出现问题,但这只是为了说明问题。

fillBuildColumn(build) {
  const row = this.createBaseRow(build);
  row.children = [];
  this.addChildRows(build, row.children);
  // eslint-disable-next-line no-plusplus
  for (let i = 0; i < row.children.length; i++) {
    const childBuild = row.children[i];
// i might have messed up with the data here
    const propsData = {
      testName: childBuild.name,
      result: childBuild.result,
      runTime: childBuild.runtime.toString(),
      startTime: childBuild.startTime,
      estimatedRunTime: childBuild.estimatedRuntime.toString()
    };

    this.rows[i] = Object.assign(this.rows[i], {
      ...propsData
    });
  }
}

createColumnByBuildName(build) {
  this.columns.push({
    label: build.name,
    field: "build" + build.id //guessable column name
  });
}
<vue-good-table :columns="columns" :rows="rows">
  <template slot="table-row" slot-scope="props">
          <span v-if="props.column.field.startsWith('build')">
            <Cell
              :testName="props.row.testName"
              :build="props.row.build"
              :cluster="props.row.cluster"
              :startTime="props.row.startTime"
              :runTime="props.row.runTime"
              :estimatedRunTime="props.row.estimatedRunTime"
              :result="props.row.result"
            ></Cell>
          </span>
          <span v-else>{{props.formattedRow[props.column.field]}}</span>
        </template>
</vue-good-table>

这个想法是在模板中有条件地渲染组件。给出可猜测的列名的原因是为了使用类似于 <span v-if="props.column.field.startsWith('build')"> 的条件语句。由于只有一个静态字段,其余都是动态的,您也可以使用 props.column.field !== 'testName'。我在渲染方面遇到了问题,必须全局注册表格插件和单元格组件。


非常感谢你。 虽然我需要在你的解决方案中进行一些修改,但最终我成功地克服了它,现在它像魔法一样运行 :) 你太棒了! - Ben

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接