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")
Related
could it be that Inertia.js page components are blocking the reactivity of vue?
I have a Page component, in this component is a normal single file component.
I have a function that adds items to the ItemsManager.items object.
When I'm running this function the single component below doesnt adds this items in the v-for.
But when I'm reload the Page Component it works and the previously added items appear.
Here the single file component:
<template>
<div>
<div v-for="item in items" :key="item.$key">
test
</div>
</div>
</template>
<script>
import { ItemsManager } from "./utils.js";
export default {
name: "test-component",
data: () => ({
items: ItemsManager.items
}),
};
</script>
utils.js:
export const ItemsManager = {
items: [],
add(item) {
item.$key = this.items.length;
this.items.unshift(item);
},
};
function that adds the items (in page component):
addItem(title, options) {
ItemsManager.add({
name: title,
options: options
});
},
Thanks in advance!
Since you're using Vue2, you need to know that there are some caveats when adding/deleting things to Objects/Arrays. You don't show any code relevant to your actual way of adding stuff to your object, but I can still recommend that you'd check this page to understand and fix your issue.
https://v2.vuejs.org/v2/guide/reactivity.html#Change-Detection-Caveats
I have a component in VueJS that displays a data table based off an array of objects. The component looks like this:
<template>
<div id="table">
<p class="title">Data:</p>
<v-data-table :headers="headers" :items="imported_data">
<template slot="items" slot-scope="props">
<td class="text-xs-left">{{props.item.name}}</td>
<td class="text-xs-right">{{props.item.age}}</td>
</template>
</v-data-table>
</div>
</template>
<script>
export default {
props: {
imported_data: {
type: Array,
required: true
}
},
data: () => ({
headers: [
{ text: "Name", value: "name" },
{ text: "Age", value: "age" }
]
})
};
</script>
I can render this component by doing <DataTableView :imported_data="this.dataset"/>, where this.dataset is an array of objects that populates my data table. My issue is when I go to reinitialize my component with another dataset, the new dataset just appends to the old dataset, it doesn't overwrite the old dataset with the new dataset. How can I make it so that my imported_data prop is reset when I put in new data?
Edit:
The data in imported_data is retrieved from the backend via a simple GET request:
axios
.get("http://localhost:8888/getData")
.then((res) => {
this.dataset = res.data
})
I have a button on the page that gets new data from my backend again, but the 'new' data is appending my 'old' data. The DataTableView component is loaded alongside the button, but isn't shown until this.imported_data is populated via a v-if conditional.
I'm trying to write my first Vue app, which will allow a CSV upload and display the data in a table. I can upload the file and render the contents to the console, and parse the CSV with Papa. I cannot seem to bind the data from the file to a table, though. I started here and added Papa parsing for CSV handling.
Here is the pertinent code:
Main.js
import Vue from 'vue'
import App from './App'
Vue.config.productionTip = false
/* eslint-disable no-new */
new Vue({
el: '#app',
components: { App },
template: '<App/>'
})
Upload.vue Component
<template>
<h1>Upload CSV</h1>
<input type='file' #change='loadTextFromFile'>
</template>
<script>
// csv parsing
import Papa from 'papaparse'
export default {
methods: {
loadTextFromFile (event) {
if (!event.target.files[0]) {
alert('Upload a file.')
} else {
let file = event.target.files[0]
if (file) {
let reader = new FileReader()
let config = {
delimiter: ',',
newline: '',
quoteChar: '"',
escapeChar: '"',
header: false,
trimHeaders: false
}
// reader.onload = e => this.$emit('load', e.target.result)
reader.onload = e => this.$emit('load', Papa.parse(event.target.result, config))
// trace what was emitted
reader.onload = function (event) {
let results = Papa.parse(event.target.result, config)
console.log('PAPA RESULT: ', results.data)
console.log('ROWS:', event.target.result)
}
reader.readAsText(file)
} else {
alert('Please select a file to upload.')
}
}
}
}
}
</script>
<style>
</style>
App.vue
<template>
<div id="app">
<h1>My CSV Handler</h1>
<br>
<upload #load="items = $event"></upload>
<br>
<table>
<thead>
</thead>
<tbody>
<!-- eslint-disable-next-line -->
<tr v-for="row in items">
<td>{{ row }}</td>
</tr>
</tbody>
</table>
</div>
</template>
<script>
import Upload from './components/Upload'
export default {
name: 'app',
data: () => ({ items: [] }),
components: {
Upload
}
}
</script>
<style>
</style>
If I emit the raw e.target.result and change 'items' to 'text' everywhere in App.vue, I can successfully render the text data to a text-area just like the tutorial.
Thanks!
You're running into limitations of array reactivity. Have a look at the docs explaining the caveats.
When you modify an Array by directly setting an index (e.g. arr[0] = val) or modifying its length property. Similarly, Vue.js cannot pickup these changes. Always modify arrays by using an Array instance method, or replacing it entirely.
Source
Here's a quick fiddle showing how to use splice to replace the items array in a way such that it will be reactive to updates.
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.
Using vue.js 2, inside a for loop, I need to render a row only if the current iterated item passes some test.
The test is complex so a simple v-if="item.value == x" wont do.
I've written a function named testIssue that accepts the item and returns true or false and tried to use that is av-if like this:
<template v-for="issue in search_results.issues">
<tr v-if="testIssue(issue)">
....
</tr>
</template>
var releaseApp = new Vue({
el: '#release-app',
methods: {
testIssue: function(issue) {
console.log(issue);
console.log('test');
},
},
mounted: function() {},
data: {
search_results: {
issues: []
},
}
});
However, testIssue is never called.
If I change the line to <tr v-if="testIssue">, the function is called but then I dont have the issue variable that I need to test.
I also tried <tr v-if="releaseApp.testIssue(issue)">
How can I call a function in a v-if declaration inside a for loop and pass the current item?
First of all you can't do v-for on a <template> tag as you can have only one element per template.
You can add a v-if on the same element as v-for, and it just won't render the element that doesn't pass the v-if. If it's a spearate component per row then it's better to do v-for in parent component and pass the issue by props to child component.
Parent:
<child-comp v-for="issue in search_results.issues" v-if="testIssue(issue)">
</child-comp>
Child:
<template>
<tr>
{{issue}}
</tr>
</template>
Your scenario works with a similar example in this fiddle.
But you can try it this way also:
You can create a custom directive named v-hide and pass issue to it as its value.
Then in the directive you can test for testIssue() and set the particular element's display to none
<template v-for="issue in search_results.issues">
<tr v-hide="issue">
....
</tr>
</template>
var releaseApp = new Vue({
el: '#release-app',
directive:{
hide: {
bind(el, binding, Vnode){
var vm = Vnode.context;
var issue = binding.value;
if(vm.testIssue(issue)){
el.style.display = 'none';
}
}
}
},
methods: {
testIssue: function(issue) {
console.log(issue);
console.log('test');
},
},
mounted: function() {},
data: {
search_results: {
issues: []
},
}
});
You can also try creating a computed item that makes use of the filter method so that you have an array where all elements pass the test function before actually rendering (in this case, just returning odd numbers):
https://codepen.io/aprouja1/pen/BZxejL
computed:{
compIssues(){
return this.search_results.issues.filter(el => el%2===1)
}
},