How to use new Vue in a single file component? - javascript

I'm still learning Vue.js and I have a minor issue:
I have a single file component with an array of checkboxes, and I have looked at the documentation for using multiple checkboxes, but the example there requires me to declare:
new Vue({
el: '#example-3',
data: {
checkedNames: []
}
})
Edit However I have set this up in my single file component within the <script> tag within data(), but it just checks/unchecks all boxes at once (but feeds back true/false correctly):
<script>
export default {
name: 'PhrasesDetail',
data () {
return {
game: '',
language: '',
checkedPhrasesArr: {
el: '#selection',
data: {
checkedPhrasesArr: []
}
}
}
},
...
</script>
The Question is where and how do I declare the checkbox array so that it reacts/recognises the individual elements?
These are my checkboxes:
<tr v-for="(keyTransPair, index) in this.language.data">
<td id="selection"><input type="checkbox" :id="index" v-model="checkedPhrasesArr"></td>
<td>{{index}}</td>
...
</tr>

I have assembled a complete example. (I don´t know anything about your language.data object so I´m just using fake data).
<template>
<div>
<!-- Your Checkboxes -->
<table>
<tr v-for="(keyTransPair, index) in language.data">
<td id="selection"><input type="checkbox" :value="keyTransPair" :id="index" v-model="checkedPhrasesArr"></td>
<td>{{index}}</td>
</tr>
</table>
<!-- Show the selected boxes -->
{{ checkedPhrasesArr }}
</div>
</template>
<script>
export default {
name: 'HelloWorld',
data () {
return {
// Your language data
language: {
data: {
keyOne: "one",
keyTwo: "two",
keyThree: "three"
}
},
// The Checkbox data
checkedPhrasesArr: []
}
}
}
</script>
Note that the checkbox values are bound with :value="keyTransPair". You can change the value to anything you want. This value will be added to the array if the checkbox is checked.
By the way: You use <td id="selection"> within the v-for loop. So the id "selection" will not be unique. You should better use a class instead of an id here.

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) {
}
}
}

b-table reloads nested components when "unshifting" a new row (but not when "pushing")

I have a vue component which contains this table, which also has a component inside its only row:
<template>
<b-table :items="records">
<template slot="row-details">
<child-component/>
</template>
</b-table>
</template>
I'm keeping the data inside the table very simple, for sample purposes:
data() {
return {
records: [{
name: "Parent Row",
_showDetails: true
}]
};
}
The child-component inside the row is quite simple too:
<template>
<div>
<p>{{number}}</p>
</div>
</template>
<script>
export default {
data() {
return {
number: Math.random(),
isUpdating: console.log("Updating Child Component")
};
},
}
</script>
If I add a new row in the parent table using records.push(newRow) everything works fine, I can see the new row and, in the child component, number does not change.
BUT If I add the new row using records.unshift(newRow) the child component is reloaded, the "Updating child component" message shows and number changes every time.
Is this the expected behaviour? How can I keep the number so it does not change when I unshift a new record?
I have created a working sample here.
To minimize re-renders of child components in a b-table, and if your data has a field that is unique for every row (i.e. an ID or primary key), set the primary-key prop to the name of the field that contains the unique row ID. This unique ID will be used as the Vue key for each <tr> element. Vue will then know if it needs to re-render the children or not.
Otherwise, what is happening is that b-table uses the row's index as the key. By pushing new rows on the table, the previous rows' Vue key stays the same (and hence not re-rendered), but if you shift on the top of the rows, those that previously has indexes of 0, 1, 2, etc have been replaced by new rows... and the entire table and it's contents (children) must be re-rendered.
EDIT/UPDATE:
I've discovered the root cause. The row-details slot was always using the row's index as its :key, and not incorporating the primary key value (if available). PR https://github.com/bootstrap-vue/bootstrap-vue/pull/4025 will fix this issue and will be available in the 2.0.0 stable release (being released today hopefully).
UPDATE 2:
BootstrapVue v2.0.0 stable has just been released (2019-09-06)
I think it has something todo with the child component. If you set _showDetails to true to every element you see what is happening. If you press prepend for the first time the number of the prepend will be the current number of parent and parent gets an new number
<template>
<div>
<input value="Append Row" #click="appendRow" type="button">
<input value="Prepend Row" #click="prependRow" type="button">
<b-table :items="records" :fields="fields">
<template v-slot:row-details="scope">
<child-component :number="scope.item.number"/>
</template>
</b-table>
</div>
</template>
<script>
import ChildComponent from "./ChildComponent";
export default {
data() {
return {
fields: ['name'],
records: [
{
name: "Parent Row",
_showDetails: true,
number: Math.random()
}
]
};
},
components: {
"child-component": ChildComponent
},
methods: {
appendRow() {
let newRow = {
name: "New Row (Append)",
_showDetails: true,
number: Math.random()
};
this.records.push(newRow);
},
prependRow() {
let newRow = {
name: "New Row (Prepend)",
_showDetails: true,
number: Math.random()
};
this.records.unshift(newRow);
}
}
};
</script>
Child-component:
<template>
<div>
<p>test {{number}}</p>
</div>
</template>
<script>
export default {
props: ['number'],
data() {
return {
isUpdating: console.log("Updating Child Component")
};
}
};
</script>

How to reset a component's props in VueJS upon reload?

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.

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")

vue.js How to call a function inside a for loop and pass the current item?

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)
}
},

Categories