Datatable empty cells using inertia and vue3 - javascript

I am using Laravel Inertia with Vue3. I used the package https://datatables.net/blog/2022-06-22-vue for the data tables. The problem is I passed the data from the controller to vue as props but it wont render correctly. I got empty cells but it has the correct data count.
I got this alert when I go to the component
DataTables warning: table id=DataTables_Table_17 -
Requested unknown parameter '0' for row 0, column 0.
For more information about this error, please see http://datatables.net/tn/4
Here is my datatable component code
<DataTable :data="tenants" class="display">
<thead>
<tr>
<th>Id</th>
<th>Database</th>
<th>Email</th>
<th>Actions</th>
</tr>
</thead>
</DataTable>
Here is my vue devtools props details
I just found a way to partially work by doing
<DataTable class="display">
<thead>
<tr>
<th>Id</th>
<th>Database</th>
<th>Email</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="tenant in tenants" :key="tenant.id">
<td>{{ tenant.id }}</td>
<td>{{ tenant.tenancy_db_name }}</td>
<td>{{ tenant.email }}</td>
<td>
<button class="btn btn-outline-info">Update</button>
 
<button #click="deleteTenant(tenant.id)" class="btn btn-outline-danger">Delete</button>
</td>
</tr>
</tbody>
</DataTable>
But the problem is when I add or delete on the table I need to switch to other component and go back in order for the table to update.
Code from controller
public function index(){
$tenants = Tenant::all()->toArray();
return Inertia::render('Tenants/Index',[
'tenants' => $tenants
]);
}
Then in my component
defineProps({
tenants: Array,
});
Thanks in advance!

If the data structure is an array of objects then according to the documentation, we need to use the columns to populate the object properties.
Working with Reactive Data is also mentioned in the documentation where the package will automatically reflect the changes made to the data.
So, if you can try the following, it might help-
<DataTable
class="display"
:columns="columns"
:data="tenants"
:options="{ select: true }"
ref="table"
/>
where columns data according to tenants array could be-
const columns = [
{ data: 'id' },
{ data: 'tenancy_db_name' },
{ data: 'email' },
];

Maybe you cannot loop the Reactive array based on the props you posted. Also based on this link it needs to be some kind of normal array.
You might need to convert your props from reactive to a normal array.
const data = [
[1, 2],
[3, 4],
];

Related

Vue - Dynamic component event listener

Problem : I am trying to create a table component for my app which would be use by other components to render a table. It could have three possible cell values :
Text
HTML
Component
I am able to render all the above values but I am stuck at binding an event listener. What I am trying to achieve is something like this :
Pass a method and event which is to be binded to the component and the table should bind that with respective cell.
So for example :
TABLE JSON
{
"cell-1":{
"type":"html",
"data":"<h4>text-1</h4>",
"method": someMethod
}
}
TABLE COMPONENT
<tbody>
<template>
<tr>
<td >
<span
v-if="type == 'html'"
v-html="data"
v-on:click.native="$emit(someMethod)"
v-on:click.native="someMethod"
></span>
</td>
</tr>
</template>
</tbody>
Above is just a snippet of what I am trying, the table loops through the object passed and renders accordingly.
I have already tried
SO Solution 1
SO Solution 2
Please let me know if any more info is required.
The best way is to have the method/handler inside the parent component and then trigger is using the emit functionality such that in
TABLE COMPONENT
<tbody>
<template>
<tr>
<td >
<span
v-if="type == 'html'"
v-html="data"
v-on:click.native="$emit('trigger-handler', {method: 'method1', data: {}})"
></span>
</td>
</tr>
</template>
</tbody>
and in
Parent.vue
<table-component #trigger-handler="triggerHandler" />
inside script
export default {
data() {
},
methods: {
triggerHandler(payload) {
// payload is actually the object passed from the child
this[payload.method](payload.data); // call a specific method
},
method1(data) {
},
method2(data) {
},
method3(data) {
}
}
}

Vue js send data from api to new page

I am new to Vue Js and I would like to know how I can send data between two components. I am building a vue app that gets a list of users from an api and displays them. I would like to know I can move data between two components so that I can view more details on a new page.
here is my html code to display the data in a table
<table class="table table-hover table-striped">
<thead>
<tr>
<th scope="col">ID</th>
<th scope="col">Name</th>
<th scope="col">Occupation</th>
<th scope="col">Email</th>
<th scope="col">Bio</th>
<th scope="col">View</th>
<th scope="col">Edit</th>
</tr>
</thead>
<tbody>
<tr v-for="user in users" :key="user.id">
<th scope="row">{{ user.id }}</th>
<td>{{ user.name }}</td>
<td>{{ user.username}}</td>
<td>{{ user.email }}</td>
<td>{{ user.phonenumber}}</td>
<router-link to="/users" class="btn btn-primary">View</router-link>
</td>
<td>
<router-link #click="shareData" :key="user.id" to="/edit" class="btn btn-primary">Edit</router-link>
</td>
</tr>
</tbody>
</table>
this is my js code
<script>
import axios from "axios";
import moment from "moment";
export default {
name: 'Home',
created() {
this.getUsers();
},
data() {
return {
users: [],
};
},
methods: {
getUsers() {
axios
.get("https://607e868602a23c0017e8b79e.mockapi.io/api/v1/users")
.then((response) => {
console.log(response.data);
this.users = response.data;
})
.catch((error) => {
console.log(error);
});
},
format_date(value) {
if (value) {
return moment(String(value)).format("DD-MM-YYYY");
}
},
shareData(){
this.$router.push({name:"Users", params:{data:this.users}})
},
editData(){
}
},
};
where do how I move the data to the view and edit routes/components
You can use vue-router's dynamic route matching for this kind of problem. You can simply refactor your /user endpoint to /user/:id and in the component you are calling when landing on /user, you can simply make your api call and fill in the details. You need to update your first router link to have some id in the form of: /user/123 and in the component, you can get that id by calling this.$route.params.id.
Depending on the complexity of your Application, you may use VUEX to store all your Application's state, or if you have a small application and do want to spend time learning VUEX, you may simply use props. With props you can pass objects ( data ) to your components or even routes.
If you want to pass data from one sibling component to another sibling component ( components having same parent) then you can use Event Bus.
Here's an article which I used to learn Event Bus Event Bus Article
If you want to pass data from Parent component to child Component , you can simply use a props.
If you want to get data anywhere in your code , then use Vuex.
Vuex sure is very handy. Happy learning.

VueJS Render VNode

tl;dr:
Given a VueJS VNode object, how do I get the HTML element that would be generated if it were rendered?
e.g.:
> temp1
VNode {tag: "h1", data: undefined, children: Array(1), text: undefined, elm: undefined, …}
> temp1.children[0]
VNode {tag: undefined, data: undefined, children: undefined, text: "Test", elm: undefined, …}
> doSomething(temp1)
<h1>Test</h1>
Goal
I'm attempting to build a small VueJS wrapper around the DataTables.net library.
To mimic the behavior of HTML tables in my markup, I want something like the following:
<datatable>
<thead>
<tr>
<th>Name</th>
<th>Age</th>
<th>Salary</th>
</tr>
</thead>
<tbody>
<datatable-row v-for="person in people">
<td>{{ person.name }}</td>
<td>{{ person.age }}</td>
<td>{{ person.salary }}</td>
</datatable-row>
</tbody>
</datatable>
What I've done so far
I've started to implement this as follows:
DataTable.vue
<template>
<table ref="table" class="display table table-striped" cellspacing="0" width="100%">
<slot></slot>
</table>
</template>
<script>
/* global $ */
export default {
data: () => ({
instance: null
}),
mounted() {
this.instance = $(this.$refs.table).dataTable();
this.$el.addEventListener("dt.row_added", function(e) {
this.addRow(e.detail);
});
},
methods: {
addRow(row) {
// TODO <-----
console.log(row);
}
}
};
</script>
DataTableRow.vue
<script>
/* global CustomEvent */
export default {
mounted() {
this.$nextTick(() => {
this.$el.dispatchEvent(new CustomEvent("dt.row_added", {
bubbles: true,
detail: this.$slots.default.filter(col => col.tag === "td")
}));
});
},
render() { return ""; }
};
What this currently does:
When the page loads, the DataTable is initialized. So the column headers are properly formatted and I see "Showing 0 to 0 of 0 entries" in the bottom left
The CustomEvent is able to bubble up past the <tbody> and be caught by the DataTable element successfully (circumventing the limitation in VueJS that you can't listen to events on slots)
What this does not do:
Actually add the row
My event is giving me an array of VNode objects. There's one VNode per column in my row. The DataTables API has an addRow function which can be called like so:
this.instance.row.add(["col1", "col2", "col3"]);
In my case, I want the resultant element from the rendering of the VNode to be the elements in this array.
var elems = [];
for (var i = 0; i < row.length; i++)
elems[i] = compile(row[i]);
this.instance.row.add(elems);
Unfortunately this compile method eludes me. I tried skimming the VueJS documentation and I tried Googling it, but no dice. I tried manually passing the createElement function (the parameter passed to the render method) but this threw an error. How can I ask VueJS to render a VNode without injecting the result into the DOM?
I ran into the same issue wanting to do basically the same thing with a row details template for DataTables.net.
One solution could be to create a generic component that renders out a VNode and instantiate that programmatically. Here is how my setup for a dynamic detail row that I insert using datatable's row.child() API.
RenderNode.js
export default {
props: ['node'],
render(h, context) {
return this.node ? this.node : ''
}
}
Datatables.vue
Include the renderer component from above
import Vue from 'vue'
import nodeRenderer from './RenderNode'
Instantiate and mount the renderer to get the compiled HTML
// Assume we have `myVNode` and want its compiled HTML
const DetailConstructor = Vue.extend(nodeRenderer)
const detailRenderer = new DetailConstructor({
propsData: {
node: myVNode
}
})
detailRenderer.$mount()
// detailRenderer.$el is now a compiled DOM element
row.child(detailRenderer.$el).show()
You should define your components like with:
import {createApp} from 'vue';
import {defineAsyncComponent} from "vue";
createApp({
components: {
'top-bar': defineAsyncComponent(() => import('#Partials/top-bar'))
}
}).mount("#app")

How to display async data in vue template

I'm interesting in the case of displaying in vue template data which loaded asynchroniously. In my particular situation I need to show title attribute of product object:
<td class="deals__cell deals__cell_title">{{ getProduct(deal.metal).title }}</td>
But the product isn't currently loaded so that the title isn't rendered at all. I found a working solution: if the products aren't loaded then recall getProduct function after the promise will be resolved:
getProduct (id) {
if (!this.rolledMetal.all.length) {
this.getRolledMetal()
.then(() => {
this.getProduct(id)
})
return {
title: ''
}
} else {
return this.getRolledMetalById(id)
}
}
However maybe you know more elegant solution because I think this one is a little bit sophisticated :)
I always use a loader or a spinner when data is loading!
<template>
<table>
<thead>
<tr>
<th>One</th>
<th>Two</th>
<th>Three</th>
</tr>
</thead>
<tbody>
<template v-if="loading">
<spinner></spinner> <!-- here use a loaded you prefer -->
</template>
<template v-else>
<tr v-for="row in rows">
<td>{{ row.name }}</td>
<td>{{ row.lastName }}</td>
</tr>
</template>
</tbody>
</table>
</template>
And the script:
<script>
import axios from 'axios'
export default {
data() {
return {
loading: false,
rows: []
}
},
created() {
this.getDataFromApi()
},
methods: {
getDataFromApi() {
this.loading = true
axios.get('/youApiUrl')
.then(response => {
this.loading = false
this.rows = response.data
})
.catch(error => {
this.loading = false
console.log(error)
})
}
}
}
</script>
There are a few good methods of handling async data in Vue.
Call a method that fetches the data in the created lifecycle hook that assigns it to a data property. This means that your component has a method for fetching the data and a data property for storing it.
Dispatch a Vuex action that fetches the data. The component has a computed property that gets the data from Vuex. This means that the function for fetching the data is in Vuex and your component has a computed property for accessing it.
In this case, it looks like your component needs to have a RolledMetal and based on that it retrieves a product. To solve this you can add methods that fetch both of them, and call them on the created lifecycle. The second method should be called in a then-block after the first one to ensure it works as expected.

Nested component not visible

I'm new to Angular 2 and my nested components are not visible or even processed.
Can you please help? I think i might haven't declared any directive....
My main.ts :
import {bootstrap} from 'angular2/platform/browser';
import {MatchListComponent} from "./match/match.component";
import { HTTP_PROVIDERS } from 'angular2/http';
bootstrap(
MatchListComponent,
[ HTTP_PROVIDERS ]
);
My MatchListComponent looks like that :
#Component({
selector: "matches",
template: `
<table class="table">
<match-row *ngFor="#list of matches" match="list" >
</match-row>
</table>
`,
providers: [MatchService]
})
export class MatchListComponent implements OnInit...
No match-row becomes displayed, but about 150 of them are present in the dom
Edit: match-row
#Component({
selector: "match-row",
template: `
<tr >
<td>
{{match.matchday.number}}
</td>
<td>
{{match.displayValue}}
</td>
</tr>
`,
providers: [TrainerService, MatchdayService]
})
export class MatchRowComponent ...
You should use let instead of # in your *ngFor and I'm assuming your match is an input on match-row. So your
<match-row *ngFor="#list of matches" match="list"</match-row>
should be
<match-row *ngFor="let list of matches" [match]="list" ></match-row>
To pass in variables on ()Inputs you must wrap the name in square brackets. Just like outputs are wrapped in parentheses. Inputs can be left without the square brackets when passing in string literals, not variables.
Hope that helps.
EDIT:
With the code for match-row added, there is a much simpler way to do this. There isn't really a reason to make this a separate component. Move your rows into the main component to iterate on it this way:
<table class="table">
<tr *ngFor="let match of matches">
<td>{{match.matchday.number}}</td>
<td>{{match.displayValue}}</td>
</tr>
</table>
If you insist on having the display data in another component, try moving the <tr> back to the main component since it is each row you want separated out. It would look something like this:
<table class="table">
<tr *ngFor="let list of matches">
<match-row [match]="list"></match-row>
</tr>
</table>

Categories