olifolkerd / tabulator

Interactive Tables and Data Grids for JavaScript
http://tabulator.info
MIT License
6.66k stars 814 forks source link

Vue Composition API not working #4017

Open ghost opened 1 year ago

ghost commented 1 year ago

https://tabulator.info/docs/5.4/frameworks

As with many newbie simpley copy and paste the code from Composition API example for Vue 3.

No devs will able to understand what is gong on and create a negative experience that made us spent hours without success.

[plugin:vite:vue] [@vue/compiler-sfc] 'return' outside of function. (18:2)
ghost commented 1 year ago

When remove the return ... line, I could only see the table header and not the data.

<script setup>
  import {ref, reactive, onMounted} from 'vue';
  import {TabulatorFull as Tabulator} from 'tabulator-tables';

  const table = ref(null);
  const tabulator = ref(null);
  const tableData = reactive([
    { id: 1, name: "Oli Bob", age: "1" },
    { id: 2, name: "Mary May", age: "1", col: "blue", dob: "14/05/1982" },
  ]);

  onMounted(() => {
      tabulator.value = new Tabulator(table.value, {
        data: tableData.value, //link data to table
        reactiveData:true, //enable data reactivity
        columns: [
            { title: "Name", field: "name", width: 150 },
            { title: "Age", field: "age", hozAlign: "left", formatter: "progress" },
            { title: "Favourite Color", field: "col" },
        ], //define table columns
      });
      console.log(table)
  })
</script>

<template>
  <div ref="table"></div>
</template>
olifolkerd commented 1 year ago

That example was provided by a member of the community. If you would like to see any amends to it, please post a working example code here and I would be happy to update the website

ghost commented 1 year ago

Waiting for contributors.

olifolkerd commented 1 year ago

@GuidanceZero the hint is on you to provide the contribution when you resolve your issue. That's how open source works, we all help eachother

ghost commented 1 year ago

Seem to have the same issue, the header is appear but tableData is not showing.

<script setup>
  import {ref, reactive, onMounted} from 'vue';
  import {TabulatorFull as Tabulator} from 'tabulator-tables';

  const table = ref(null);
  const tabulator = ref(null);
  const tableData = reactive([
    { id: 1, name: "Oli Bob", age: "1" },
    { id: 2, name: "Mary May", age: "1", col: "blue", dob: "14/05/1982" },
  ]);

  onMounted(() => {
      tabulator.value = new Tabulator(table.value, {
        data: tableData.value, //link data to table
        reactiveData:true, //enable data reactivity
        columns: [
            { title: "Name", field: "name", width: 150 },
            { title: "Age", field: "age", hozAlign: "left", formatter: "progress" },
            { title: "Favourite Color", field: "col" },
        ],
      });

      console.log(table.value)
      return { tabulator, table };
  }) 
</script>

<template>
  <div ref="table"></div>
</template>
Armer7 commented 1 year ago

ReactiveData not work for me also in Vue 2.5.11 with Vuex 3.0.1. If I use the watch for listTaskMap with setData data is showing.

<template>
  <div class="list-task-table">
    <div ref="listTaskTable"></div>
  </div>
</template>

<script>
import { mapGetters } from 'vuex';
import { Tabulator, SortModule, FormatModule, ReactiveDataModule } from 'tabulator-tables';
import 'tabulator-tables/dist/css/tabulator_bootstrap3.css';

export default {
  name: 'ListTaskTable',
  data() {
    return {
      tabulator: undefined,
    };
  },
  mounted() {
    Tabulator.registerModule([ SortModule, FormatModule, ReactiveDataModule ]);
    this.tabulator = new Tabulator(this.$refs.listTaskTable, {
      data: this.listTaskMap,
      debugInvalidComponentFuncs: false,
      reactiveData: true,
      columns: this.columns,
    });
  },
  computed: {
    ...mapGetters({
      listTaskMap: 'listTask/getListTaskMap',
      dateFormat: 'organization/getDateFormat',
    }),

    columns() {
      const bind = this;
      const column = [
        {
          title: 'Number',
          field: 'number',
        },
        {
          title: 'Created At',
          field: 'createdAt',
          formatter(cell, formatterParams) {
            const value = cell.getValue();
            return bind.$moment.unix(value)
              .format(bind.dateFormat);
          },
        },
      ];
      return column;
    },
  },
};
</script>
joaquinjsb commented 1 year ago

reactivity not working for me either on Vue 3.2.45, I guess it's a problem on tabulator on the way it handles the data with the proxy

olifolkerd commented 1 year ago

Hey Guys,

Im happy help resolve this, but i am not a user of Vuex so will need a member of the community to investigate this one further if they want a resolution.

Start up a PR if you are interested in helping out.

Cheers

Oli :)

AaronAMcGuire commented 1 year ago

@GuidanceZero I believe there is a slight issue with the example in the documentation which is why it is not working for you. Similar to what @joaquinjsb alluded to it's related to how Vue provides you with the proxy reference to your reactive data.

<template>
  <div ref="table"></div>
  <button @click="addRow">Add Data</button>
</template>

<script setup>
  import {ref, reactive, onMounted} from 'vue';
  import {TabulatorFull as Tabulator} from 'tabulator-tables';

  const table = ref(null);
  const tabulator = ref(null);
  const tableData = reactive([
    { id: 1, name: "Oli Bob", age: "1" },
    { id: 2, name: "Mary May", age: "1", col: "blue", dob: "14/05/1982" },
  ]);

  const addRow = () => {
        tableData.push({id:3,name:'Aaron',age:"28"})
      }

  onMounted(() => {
      tabulator.value = new Tabulator(table.value, {
        data: tableData, //link data to table
        reactiveData:true, //enable data reactivity
        columns: [
            { title: "Name", field: "name", width: 150 },
            { title: "Age", field: "age", hozAlign: "left", formatter: "progress" },
            { title: "Favourite Color", field: "col" },
        ],
      });

  }) 

</script>
<style></style>

As you can see from my example when you're using a reactive reference you don't need to reference it with .value inside the data instance of Tabulator. This is because it's value is automatically proxied by Vue so by adding .value you are actually getting undefined back. For further context If you were using a ref then you would need to use .value

Also since you are using the script setup version of Vue you can remove the return statement altogether, this is only required if you are using script setup with a setup() function e.g.

<template>
  <div ref="table"></div>
  <button @click="addRow">Add Data</button>
</template>

<script>

import {ref,reactive,onMounted} from 'vue';
import {TabulatorFull as Tabulator} from 'tabulator-tables'; //import Tabulator library

export default {
  setup() {

    const table = ref(null); //reference to your table element
  const tabulator = ref(null); //variable to hold your table
  const tableData = reactive([
    {id:1, name:"Billy Bob", age:"12", gender:"male", height:1, col:"red", dob:"", cheese:1},
    {id:2, name:"Mary May", age:"1", gender:"female", height:2, col:"blue", dob:"14/05/1982", cheese:true},
    {id:3, name:"Christine Lobowski", age:"42", height:0, col:"green", dob:"22/05/1982", cheese:"true"},
    {id:4, name:"Brendon Philips", age:"125", gender:"male", height:1, col:"orange", dob:"01/08/1980"},
    {id:5, name:"Margret Marmajuke", age:"16", gender:"female", height:5, col:"yellow", dob:"31/01/1999"},
]); //data for table to display

    const addRow = () => {
      tableData.push({id:6, name:"Aaron Mcguire", age:"28", gender:"male", height:1, col:"yellow", dob:"", cheese:false})
    }
  onMounted(() => {
    //instantiate Tabulator when element is mounted
      tabulator.value = new Tabulator(table.value, {
        data: tableData, //link data to table
        reactiveData:true, //enable data reactivity
        columns: [
  { title: "Name", field: "name", width: 150 },
  { title: "Age", field: "age", hozAlign: "left", formatter: "progress" },
  { title: "Favourite Color", field: "col" },
  { title: "Date Of Birth", field: "dob", hozAlign: "center" },
  { title: "Rating", field: "rating", hozAlign: "center", formatter: "star" },
  { title: "Passed?", field: "passed", hozAlign: "center", formatter: "tickCross" }
], //define table columns
      });
  })
     // expose to template and other options API hooks
    return {table,tabulator,addRow }
  },
}
</script>

<style></style>

Hopefully this comment helps a few people that like me were confused by the subtle issue.

olifolkerd commented 1 year ago

@AaronAMcGuire thank you very much for the detailed answer, that is very helpful. I will get the docs update this week to make it easier to understand.

Cheers

Oli :)

AaronAMcGuire commented 1 year ago

@olifolkerd Apologies after a little more real-world testing there is actually a valid issue

I'll try my best to give an accurate description.

<template>
  {{tableData}}
</template>

<script setup>
  import {ref} from 'vue';
  const tableData = ref([]);

  fetch("https://api.nationalize.io/?name=aaron",{method:"GET",headers:{"Content-Type":"application/json"}})
  .then(response => response.json())
  .then(data => {
    tableData.value.push(...data.country)
    console.log(tableData);
  }).catch(error => { console.error(error)})
</script>
<style></style>

Take the basic example above this is not using Tabulator if you run this SFC Playground Demo

You can see an api call has been sent, the data has been returned and the tableData ref has automatically updated it's value on the page, this is an example of everything working as expected.

If we now introduce Tabulator :

<template>
  <div ref="table"></div>
  {{tableData}}
</template>

<script setup>
  import {ref, onMounted} from 'vue';
  import {TabulatorFull as Tabulator} from 'tabulator-tables';

  const table = ref(null);
  const tabulator = ref(null);
  const tableData = ref([]);

  fetch("https://api.nationalize.io/?name=aaron",{method:"GET",headers:{"Content-Type":"application/json"}})
  .then(response => response.json())
  .then(data => {
    tableData.value.push(...data.country)
  }).catch(error => { console.error(error)})

  onMounted(() => {
      tabulator.value = new Tabulator(table.value, {
        data: tableData.value, //link data to table
        reactiveData:true, //enable data reactivity
        columns: [
            { title: "Country ID", field: "country_id" },
            { title: "Probability", field: "probability"},
        ],
      });
  }) 

</script>
<style></style>

What happens now is the api call is made, the data in the table is updated as you would expect but the tableData reference does not update as you would expect indicating that the definition has lost reactivity.

Next I tried to confirm it was Tabulator breaking the reactivity. So I looked in ReactiveData.js to see how it worked and noticed you were re-defining methods temporarily then running a .apply to call the original method once you had done your processing. I found a method you hadn't redefined so I created an example with both an Object.assign and a .push the theory being that since Tabulator doesn't touch Object.assign it will retain reactivity and on the flip side using .push will break reactivity but will still update the table content.

<template>
  <div ref="table"></div>
  {{tableData}}
</template>

<script setup>
  import {ref,reactive, onMounted} from 'vue';
  import {TabulatorFull as Tabulator} from 'tabulator-tables';

  const table = ref(null);
  const tabulator = ref(null);
  const tableData = reactive([]);

  fetch("https://api.nationalize.io/?name=aaron",{method:"GET",headers:{"Content-Type":"application/json"}})
  .then(response => response.json())
  .then(data => {
    Object.assign(tableData,data.country)
    tableData.push(...data.country);
  }).catch(error => { console.error(error)})

  onMounted(() => {
      tabulator.value = new Tabulator(table.value, {
        data: tableData, //link data to table
        reactiveData:true, //enable data reactivity
        columns: [
            { title: "Country ID", field: "country_id" },
            { title: "Probability", field: "probability"},
        ],
      });
  }) 

</script>
<style></style>

This confirmed that it related to the Tabulator reactivity module, so I stepped through the code for the .push. As expected it does the standard processing then does a .apply to call the vanilla method passing in the context and arguments result = self.origFuncs.push.apply(data, arguments);

Which then runs Vue's version which does a very similar thing..

 ['push', 'pop', 'shift', 'unshift', 'splice'].forEach(key => {
        instrumentations[key] = function (...args) {
            pauseTracking();
            const res = toRaw(this)[key].apply(this, args);
            resetTracking();
            return res;
        };
    });

From the minimal debugging I have conducted it looks like the tabulator data reactivity module runs it's version of .push Part of that process includes a self.block to prevent infinite looping.

It then runs a .apply - which triggers a call to Vue which does it's own version of .apply ( as seen in the snippet above) which in turn triggers another instance of the tabular reactivity module but since the very first instance added a self.block the processing fails the if statement which means Vue's version of .apply is essentially terminated by tabulators function.

I hope that makes as much sense as it does in my head!

aditya-thimphu commented 1 year ago

Hi,

I faced the same issue. Any update on this?

Thanks

rajmondburgaj commented 1 year ago

The same here, the reactivity does not work when the data are loaded from an API, but it works just fine if I initialize the table object after the data are finished loading. Also adding new elements in the array do not reflect on the table. Unfortunately using the library at this state in Vuejs is simply impossible due to these critical bugs. Hopefully these issues will be addressed as this seems to be a great library with many options otherwise. For now have to look somewhere else and find something more suitable for my basic use cases.

Robinhoeh commented 1 year ago

Hey Folks!

Recativity in Nuxt3

IN the example below, we have a simple table that uses reactive data on an object thats passed into the tabulator instance on mounted

In this simple example, we expect the tabulator table to update after 1000 miliseconds, however it does not

The data in the vue/nuxt devtools shows that the array has indeed been updated with the new array item however the table does not reflect those changes

<template>
    <div ref="tableDiv" />
</template>

<script setup lang="ts">

const props = defineProps({
    data: {
        type: Array,
        required: true
    },
    cols: {
        type: Array,
        required: true
    },
    options: {
        type: Object,
        default: () => ({})
    }
})

const tableDiv = ref(null)

const tabulator = ref(null)

const cols = ref([
    { title: "ID", field: "ID" },
    { title: "Name", field: "Name" }
])
let data = reactive([
    { ID: 1, Name: "John" },
    { ID: 2, Name: "Jane" }
])

// static defaults for basic table
const defaults = {
    layout: "fitColumns",
    responsiveLayout: "collapse",
    reactiveData: true,
    data: data,
    columns: cols.value
}

// Merge defaults with any options passed in
const options = ref(Object.assign({}, defaults, props.options))

onMounted(() => {
    // instantiate Tabulator when element is mounted
    // @ts-ignore
    tabulator.value = new Tabulator(tableDiv.value, options.value)
})

setTimeout(() => {
    data = [{ ID: 3, Name: "Bob" }]
}, 1000)

</script>

In a more complex example, we call our API, set the local data and update it based on a filter. Again, the vue API updates the array, but tabulators table is not updated in the UI. Seems that reactive data is not working 😦 seems to be a universal issue with vue/nuxt users! Appreciate it!

joaquinjsb commented 1 year ago

What I don't understand why they label it as suggested feature and not as bug fix.

Robinhoeh commented 1 year ago

FIX:

Alright, in my code above we have a watch however, it wasn't returning the prop value. This works now and makes the data reactive:

watch(() => props.data, (newData) => {
    // update table data when props.data changes
    tabulator.value.replaceData(newData)
}, { deep: true })

Notice the watch() now returns the props.data - this is a vue 3 composition API thing. You can't watch the prop directly, you must return it!

ALSO: the reactiveData: true does nothing seemingly, so I removed it

NOW, the this ref

const tabulator = ref<any>(null)

gets the updated value(array) and boom, its reactive!

Robinhoeh commented 1 year ago

What I don't understand why they label it as suggested feature and not as bug fix.

see fix below!

jbhaywood commented 8 months ago

I'll echo what other's have said. This seems like a great library, but as far as I can tell it's completely broken when trying to use with Vue 3. The document here should be removed until the issues have been fixed.

leamsigc commented 7 months ago

I got a small test working locally with Nuxt and vue 3

<script lang="ts" setup>
/**
 *
 * Component Description:Desc
 *
 * @author Reflect-Media <Leamsigc>
 * @version 0.0.1
 *
 * @todo [ ] Test the component
 * @todo [ ] Integration test.
 * @todo [✔] Update the typescript.
 */
import "tabulator-tables/dist/css/tabulator.min.css";
import {
  TabulatorFull as Tabulator,
  type ColumnDefinition,
} from "tabulator-tables";
const table = ref(null); //reference to your table element
const tabulator = ref<Tabulator | null>(null); //variable to hold your table

const columns = ref<ColumnDefinition[]>([
  { title: "Name", field: "name", width: 150 },
  { title: "Age", field: "age", hozAlign: "left", formatter: "progress" },
  { title: "Favourite Color", field: "col" },
  { title: "Date Of Birth", field: "dob", hozAlign: "center" },
  { title: "Rating", field: "rating", hozAlign: "left", formatter: "star" },
  {
    title: "Passed?",
    field: "passed",
    hozAlign: "center",
    formatter: "tickCross",
  },
]);
const data = ref<Record<string, any>[]>([]);

onMounted(() => {
  if (!table.value) return;
  tabulator.value = new Tabulator(table.value, {
    data: data.value, //link data to table
    reactiveData: true, //enable data reactivity
    columns: columns.value, //define table columns
    layout: "fitDataStretch",
  });

  // Set time out and create a mock promise
  setTimeout(() => {
    console.log("set timeout");

    data.value = [
      { id: 1, name: "Oli Bob", age: "12", col: "red", dob: "" },
      { id: 2, name: "Mary May", age: "1", col: "blue", dob: "14/05/1982" },
      {
        id: 3,
        name: "Christine Lobowski",
        age: "42",
        col: "green",
        rating: "5",
        dob: "22/05/1982",
      },
      {
        id: 4,
        name: "Brendon Philips",
        age: "125",
        col: "orange",
        dob: "01/08/1980",
      },
      {
        id: 5,
        name: "Margret Marmajuke",
        age: "16",
        col: "yellow",
        dob: "31/01/1999",
      },
    ];
    tabulator.value?.setData(data.value);
  }, 5000);
});
</script>

<template>
  <div class="max-w-screen-xl w-full">
    <h3>Table here</h3>
    <div ref="table"></div>
  </div>
</template>
<style scoped>
</style>

Hope this help, This project seems to be a really nice project

jbhaywood commented 7 months ago

Just to be clear, you're using the API like anyone would using plain javascript. .setData is doing all the work here. There's no need for the 'data' property to exist at all, you just gather your data however you want and use .setData to set it. There's also no need for reactiveData: true in this case. Again, .setData is doing all the work of replacing the current data with new data and redrawing the table.

The original point of the post still stands. The example is broken and the claims of it working with Vue reactivity should be removed. For anyone having trouble using Tabulator with Vue, just forgo any attempts at using Vue refs (except for referencing the div table) and use the Tabulator API as it's described in the documentation.

leamsigc commented 7 months ago

Yeah, that make sense, but in the documentation as well is this disclaimer

Local Data Only Reactive data functionality is only available with local data arrays set on the data property or the setData function. It is not available on ajax data sources.

That why I assume that to update the data was ok to set it this way, but it can be that is not the case, but let me have a look at it and see if I can find an example where ref to the data is working correctly

https://tabulator.info/docs/5.6/reactivity

Once a data array has been added to the data property it will be watched for changes using any of the standard row manipulation functions, push, pop, shift, unshift, and splice. Using any of these functions on the array will automatically trigger an update on the table

From what I understood base on the documentation is that you set the data, from the ref data then you can push,pop,shift or slice. The changes will be visible in the ui