[Vuejs]-Accordion table with vue.js and "v-for" in root element

0👍

You can have multiple tbody tags in a table. Wrap risk-component in a tbody and you can have multiple trs in it. You will need to do a little restructuring so that the tbodys don’t nest.

var risks = '[{"title":"Risk1", "controls":[{"title":"Control1"}, {"title":"Control2"}, {"title":"Control3"}]},' +
  '{"title":"Risk2", "controls":[{"title":"Control1"}, {"title":"Control2"}, {"title":"Control3"}]},' +
  '{"title":"Risk3", "controls":[{"title":"Control1"}, {"title":"Control2"}, {"title":"Control3"}]}]';

var programs = '[{"title":"Program1", "practice":"IT", "auditType":"GAT", "version":"0.01", "programId":"2017.1", "status":"draft", "risks":' + risks + '},' +
  '{"title":"Program2", "practice":"IT", "auditType":"On-request", "version":"0.01", "programId":"2017.2", "status":"draft", "risks":""},' +
  '{"title":"Program3", "practice":"CA", "auditType":"GAT", "version":"0.01", "programId":"2017.3", "status":"approved", "risks":' + risks + '},' +
  '{"title":"Program4", "practice":"CA", "auditType":"On-request", "version":"0.01", "programId":"2018.1", "status":"draft", "risks":' + risks + '},' +
  '{"title":"Program5", "practice":"OA\FA", "auditType":"GAT", "version":"0.01", "programId":"2019.1", "status":"draft", "risks":' + risks + '},' +
  '{"title":"Program6", "practice":"OA\FA", "auditType":"On-request", "version":"0.01", "programId":"2020.1", "status":"approved", "risks":' + risks + '}]';


var dataO = JSON.parse(programs);

Vue.component('control-component', {
  props: ['control'],
  template: '<tr class="control" >' +
    '<td></td>' +
    '<td colspan="6"><a href="#">{{ control.title }}</a></td>' +
    '</tr>'
})

Vue.component('risk-component', {
  props: ['risk'],
  template: '<tbody><tr class="risk" >' +
    '<td></td>' +
    '<td colspan="5"><a href="#">{{risk.title}}</a></td>' +
    '<td><span class="signrisk"></span></td>' +
    '</tr>' +
    '<control-component v-for="control in risk.controls" :control="control" :key="control.title"></control-component>' +
    '</tbody>'
});


var programTable = new Vue({
  el: '#programTable',
  data: {
    programs: ''
  }
})
programTable.programs = dataO;
.signprogram:after {
  content: "+";
  display: inline-block;
  cursor: pointer;
}

.expandprogram:after {
  content: "-";
  cursor: pointer;
}

.signrisk:after {
  content: "+";
  display: inline-block;
  cursor: pointer;
}

.expandrisk:after {
  content: "-";
  cursor: pointer;
}

th {
  background-color: #e0e0e0;
}

.program {
  background-color: #e9e9e9;
}

.risk {
  background-color: #eeeeee;
}

.control {
  background-color: #f2f2f2;
}

.spacing {
  background-color: white;
}
<script src="//unpkg.com/vue@latest/dist/vue.js"></script>
<table class="table" width="300px" id="programTable">
  <thead>
    <tr>
      <th>Status</th>
      <th>Title</th>
      <th>Practice</th>
      <th>Audit Type</th>
      <th>Version</th>
      <th>Program</th>
      <th>&nbsp;</th>
    </tr>
  </thead>
  <template v-for="program in programs">
    <tbody>
      <tr class="program" >
        <td v-if="program.status == 'draft'" style="width: 20px; background-color: lightblue;">&nbsp;&nbsp;&nbsp;</td>
        <td v-if="program.status == 'approved'" style="width: 20px; background-color: lightgreen;">&nbsp;&nbsp;&nbsp;</td>
        <td><a href="/auditprograms/1">{{program.title}}</a></td>
        <td>{{program.practice}}</td>
        <td>{{program.auditType}}</td>
        <td>{{program.version}}</td>
        <td>{{program.programId}}</td>		
        <td><span class="signprogram"></span></td>
      </tr>
    </tbody>
    <risk-component v-for="risk in program.risks" :risk="risk" :key="program.title + risk.title"></risk-component>
  </template>
</table>

0👍

For a similar question I purpose an other answer : Vue js error: Component template should contain exactly one root element

In your case you have to delegate the <tbody/> to a functionnal component tha will construct a list of <tr/> components.
Or to delegate the <table/> to a functionnal component tha will construct a list of <tbody/> components.
Or both.

copy/paste here :

if, for any reasons, you don’t want to add a wrapper (in my first case it was for <tr/> components), you can use a functionnal component.

Instead of having a single components/MyCompo.vue you will have few files in a components/MyCompo folder :

  • components/MyCompo/index.js
  • components/MyCompo/File.vue
  • components/MyCompo/Avatar.vue

With this structure, the way you call your component won’t change.

components/MyCompo/index.js file content :

import File from './File';
import Avatar from './Avatar';   

const commonSort=(a,b)=>b-a;

export default {
  functional: true,
  name: 'MyCompo',
  props: [ 'someProp', 'plopProp' ],
  render(createElement, context) {
    return [
        createElement( File, { props: Object.assign({light: true, sort: commonSort},context.props) } ),
        createElement( Avatar, { props: Object.assign({light: false, sort: commonSort},context.props) } )
    ]; 
  }
};

And if you have some function or data used in both templates, passed them as properties and that’s it !

I let you imagine building list of components and so much features with this pattern.

Leave a comment