3

I've just recently started to dive in into Vue JS - loved it so far. I'm facing an issue now where I'm trying to create a (non-trivial) table (using vue-good-table plugin) in which each cell is a component by it's own.

Having read the documentation of the plugin, it's being mentioned that it's possible to create an HTML column types where you can just use, well, a raw HTML (I guess): https://xaksis.github.io/vue-good-table/guide/configuration/column-options.html#html

To simplify things, here's what I have - a Vue component (called Dashboard2.vue) that holds the table and the child component called Test.vue

I'm creating the Test components dynamically per each relevant cell and assigning it to the relevant row cell. since I've defined the columns to be HTML types, I'm using the innerHTML property to extract the raw HTML out of the Vue component. (following this article https://css-tricks.com/creating-vue-js-component-instances-programmatically/) It all goes very well and the dashboard looks exactly how I wanted it to be, but when clicking the button inside each Test component, nothing happens.

I suspect that since I've used the innerHTML property it just skips Vue even handler mechanism somehow, so I'm kinda stuck.

Here's the relevant components section:

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>

I know this is a lot of code. The specific relevant section (in Dashboard2.vue) is fillBuildColumn

Again - I'm a newbie to Vue JS - that being said my hunch tells me I'm doing many things wrong here.

Any help will be greatly appreciated.

EDIT:

by loosing the innerHTML property and the html type I'm ending up with a:

"RangeError: Maximum call stack size exceeded" thrown by the browser. Not sure what's causing it

Ben
  • 421
  • 6
  • 19
  • Have you tried using `refs` instead of native js to grab elements? – Michael Dec 02 '19 at 08:48
  • @Michael Not sure how to apply it to my use case. – Ben Dec 02 '19 at 09:11
  • This way it only renders html part. You are not getting cell element and not appending your component as a child. You just setting your components outlook. Is there a reason to avoid using slots? – Eldar Dec 02 '19 at 09:21
  • @Eldar no reason at all - not sure how to apply them after creating the components dynamically inside the method. From the point I've the component instance in hand - not sure how to attach/apply it to slot – Ben Dec 02 '19 at 09:24
  • Why you need to create your component dynamically? Putting it in a template and populating its properties with row data is not handling your scenario? – Eldar Dec 02 '19 at 09:30
  • @Eldar The table is not trivial and it's kinda reversed table in which the columns are bring filled first. the size of the table varies – Ben Dec 02 '19 at 09:34
  • That is a matter of data population and it shouldn't bother the cell template. If all **field** cells have the same look is there a need to create it dynamically? – Eldar Dec 02 '19 at 09:38
  • @Eldar If you can point me to a new direction (either slots or component :is) it will be great. – Ben Dec 02 '19 at 09:41

1 Answers1

2

I have made a CodeSandbox sample. I might have messed with the data part. But it gives the idea.

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>

The idea is rendering a component inside a template and do it conditionally. The reason giving guessable column name is to use condition like <span v-if="props.column.field.startsWith('build')">. Since you have only 1 static field the rest is dynamic you can also use props.column.field !== 'testName'. I had problems with rendering i had to register table plugin and Cell component globally.

Eldar
  • 9,781
  • 2
  • 10
  • 35
  • Thanks a lot man. Although it required me to alter some things in your solution, I've finally managed to overcome it and it's now works like a charm :) you rock! – Ben Dec 02 '19 at 19:27