Related
I am currently writing my first full-stack app. I am using bootstrap <b-table> to display content. On row-click, I expand the row to display nested data. Is there a way to iterate over the nested data and display it in nested rows within the parent b-table?
Currently, I can display the data, however it displays in a single row.
component.vue:
<template>
<div id="report-table" class="report-table">
<b-container>
<b-table striped hover sticky-header="100%"
:items="reports"
:fields="fields"
responsive="xl"
#click="clearRowClick"
#row-clicked="reports=>$set(reports, '_showDetails', !reports._showDetails)"
>
<template slot="row-details" slot-scope="row">
<template v-for="(proc, index) in row.item.Processes">
<b-tr :key=index>
<td>{{ proc.Name }}</td>
<td>{{ proc.Id }}</td>
</b-tr>
</template>
</template>
</b-table>
</b-container>
</div>
</template>
example
In the attached image, the bottom row has been clicked. The content is displayed within a single row, but I would like it to be separate rows, so later I can further click on them to display even more nested content.
data example:
{"_id": <id>, "Hostname": <hostname>, "Address": <address>, "Processes": [{"Name": ApplicationHost, ...}, {"Name": svchost, ...}]
If this is not possible, is there some other Bootstrap element that makes more sense to achieve what I want?
To strictly answer your question: no, a BootstrapVue <b-table>'s row-details row can't be expanded into more than one row.
The row-details row has severe limitations:
it's only one row
it's actually only one cell which, through use of colspan is expanded to the full width of the row (which means you can't really use the table columns to align the content of the row-details row).
But... this is web. In web, because it's virtual, virtually anything is possible. When it's not, you're doing-it-wrong™.
What you want is achievable by replacing rows entirely when a row is expanded, using a computed and concatenating the children to their parent row when the parent is in expanded state. Proof of concept:
Vue.config.productionTip = false;
Vue.config.devtools = false;
new Vue({
el: '#app',
data: () => ({
rows: [
{id: '1', name: 'one', expanded: false, children: [
{id: '1.1', name: 'one-one'},
{id: '1.2', name: 'one-two'},
{id: '1.3', name: 'one-three'}
]},
{id: '2', name: 'two', expanded: false, children: [
{id: '2.1', name: 'two-one'},
{id: '2.2', name: 'two-two'},
{id: '2.3', name: 'two-three'}
]}
]
}),
computed: {
renderedRows() {
return [].concat([...this.rows.map(row => row.expanded
? [row].concat(row.children)
: [row]
)]).flat()
}
}
})
tr.parent { cursor: pointer }
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<table>
<tr v-for="row in renderedRows" :key="row.id"
#click="row.children && (row.expanded = !row.expanded)"
:class="{parent: row.children}">
<td>{{row.id}}</td>
<td>{{row.name}}</td>
</tr>
</table>
</div>
The example is rather basic (I haven't added BootstrapVue to it, nor have I used its fancy <b-table>), but it demonstrates the principle. Apply it to <b-table>'s :items.
One could even take it a step further and make it recursive, by moving the expansion logic into a method:
new Vue({
el: '#app',
data: () => ({
fields: ['id', { key: 'expanded', label: ''}, 'name'],
rows: [{
id: '1',
name: 'one',
expanded: false,
children: [
{ id: '1.1', name: 'one-one' },
{ id: '1.2', name: 'one-two' },
{
id: '1.3',
name: 'one-three',
expanded: false,
children: [
{ id: '1.3.1', name: 'one-three-one' },
{ id: '1.3.2', name: 'one-three-two' }
]
}
]
},
{
id: '2',
name: 'two',
expanded: false,
children: [
{ id: '2.1', name: 'two-one' },
{ id: '2.2', name: 'two-two' },
{ id: '2.3', name: 'two-three' }
]
}
]
}),
computed: {
items() {
return [].concat(this.rows.map(row => this.unwrapRow(row))).flat()
}
},
methods: {
unwrapRow(row) {
return row.children && row.expanded
? [row].concat(...row.children.map(child => this.unwrapRow(child)))
: [row]
},
tbodyTrClass(row) {
return { parent: row.children?.length, child: row.id.includes('.') }
}
}
})
.table td:not(:last-child) { width: 80px; }
.table .bi { cursor: pointer }
tr.child {
background-color: #f5f5f5;
font-style: italic;
}
<link type="text/css" rel="stylesheet" href="//unpkg.com/bootstrap/dist/css/bootstrap.min.css" />
<link type="text/css" rel="stylesheet" href="//unpkg.com/bootstrap-vue#latest/dist/bootstrap-vue.min.css" />
<script src="//cdn.jsdelivr.net/npm/vue#2.6.14"></script>
<script src="//unpkg.com/bootstrap-vue#latest/dist/bootstrap-vue.min.js"></script>
<script src="//unpkg.com/bootstrap-vue#latest/dist/bootstrap-vue-icons.min.js"></script>
<div id="app">
<b-table :items="items"
:fields="fields"
:tbody-tr-class="tbodyTrClass">
<template #cell(expanded)="{item}">
<b-icon v-if="item.children"
:icon="item.expanded ? 'chevron-up' : 'chevron-down'"
#click="item.expanded = !item.expanded" />
</template>
</b-table>
</div>
One approach (that I've personally used in the past) is simply to put a nested <b-table> inside your child row-details for child data, instead of trying to add them to the outer table.
It's also worth noting that adding child data rows to the outer table could be visually confusing if they don't look distinct enough from their parents.
Example:
new Vue({
el: '#app',
data() {
return {
reports: [{_id: 'ID#1', Hostname: 'Host1', Address: 'Addr1', Processes: [{Name: 'ApplicationHost', Id: '1'}, {Name: 'svchost', Id: '2'}]},
{_id: 'ID#2', Hostname: 'Host2', Address: 'Addr2', Processes: [{Name: 'ApplicationHost', Id: '3'}, {Name: 'svchost', Id: '4'}]},],
fields: ['Hostname', 'Address'],
}
},
});
<!-- Import Vue and Bootstrap-Vue -->
<link type="text/css" rel="stylesheet" href="//unpkg.com/bootstrap#4/dist/css/bootstrap.min.css" /><link type="text/css" rel="stylesheet" href="//unpkg.com/bootstrap-vue#latest/dist/bootstrap-vue.min.css" /><script src="//unpkg.com/vue#latest/dist/vue.min.js"></script><script src="//unpkg.com/bootstrap-vue#latest/dist/bootstrap-vue.min.js"></script>
<div id="app">
<b-table
bordered
striped
hover
:items="reports"
:fields="fields"
#row-clicked="reports=>$set(reports, '_showDetails', !reports._showDetails)"
>
<!-- <b-table> nested inside 'row-details' slot: -->
<template #row-details="row">
<b-table
bordered
:items="row.item.Processes"
:fields="['Name', 'Id']"
></b-table>
</template>
</b-table>
</div>
I'm building a list that users can choose items.
It would be a nested list with at least 3 layers.
As can only offer one layer of sub-option, I would like to build it as <ul> and <li>.
But I can't figure out how to change my code with two <ul> and <li>.
Hope someone could give me some ideas.
Here are what I have
<div id="applyApp" class="container">
<div class="pool">
<ul>
<h3 #click.prevent="isShow = !isShow">Category</h3>
<li v-for="items in filterData" :value="items.id">
{{items.id}} {{items.ame}}
</li>
</ul>
<ul class="selected-item">
<li v-for="items in secondLayer" :value="items.id">
{{items.id}} {{items.name}}
</li>
</ul>
</div>
Vue
new Vue({
el: "#applyApp",
data: {
firstLayer: [
{
name: "name1",
id: "0101",
},
{
name: "name2",
id: "010101",
},
{
name: "name3",
id: "010101001B",
},
],
secondLayer: [],
firstLayerValue: [],
secondLayerValue: [],
},
methods: {
moveHelper(value, arrFrom, arrTo) {
const index = arrFrom.findIndex(function (el) {
return el.id == value;
});
const item = arrFrom[index];
arrFrom.splice(index, 1);
arrTo.push(item);
},
addItems() {
const selected = this.firstLayerValue.slice(0);
for (const i = 0; i < selected.length; ++i) {
this.moveHelper(selected[i], this.firstLayer, this.secondLayer);
}
},
removeItems() {
const selected = this.secondLayerValue.slice(0);
for (const i = 0; i < selected.length; ++i) {
this.moveHelper(selected[i], this.secondLayer, this.firstLayer);
}
}
},
});
If you want nested items in your list, you might have to use a recursion component, which means adding child data to your items. And calling the component inside of itself until it exhausts the list of children, by taking the data props of the child in every call.
Vue.component("listRecursion", {
props: ['listData'],
name: "listRecursion",
template: `
<div>
<ul v-for="item in listData">
<li>{{item.name}}</li>
<list-recursion v-if="item.children.length" :list-data="item.children"/>
</ul>
</div>`
})
new Vue({
el: "#app",
data: {
todos: [{
name: "name1",
id: "0101",
children: [
{
name: "sub item 1",
id: "0d201",
children: []
},
{
name: "sub item 2",
id: "020g1",
children: []
},
{
name: "sub item 3",
id: "20201",
children: [
{
name: "subsub item 1",
id: "40201",
children: [
{
name: "subsub item 1",
id: "40201",
children: [
{
name: "subsubsub item 1",
id: "40201",
children: []
}]
}]
},
{
name: "subsub item 2",
id: "50201",
children: []
},
]
},
]
},
{
name: "name2",
id: "010101",
children: []
},
{
name: "name3",
id: "010101001B",
children: []
},
]
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<h2>Recursive list:</h2>
<list-recursion :list-data="todos"/>
</div>
As you can see, this saves you from manually adding new levels, just add to the data to the child nodes
I see #procoib which solves your first case and for the li rearrange, you can do the same approach similar to 'select' which is shown below.
new Vue({
el: "#app",
data() {
return {
first: [1, 2, 3, 4, 5],
second: []
}
},
methods: {
alter: function (index, src, desc) {
this[desc].push(this[src].splice(index, 1)[0])
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.11/vue.min.js"></script>
<div id="app">
<h1>First</h1>
<ul id="firstList">
<li v-for="(_, i) in first" v-on:click="alter(i, 'first', 'second')">{{_}}</li>
</ul>
<h1>Second</h1>
<ul id="secondList">
<li v-for="(_, i) in second" v-on:click="alter(i, 'second', 'first')">{{_}}</li>
</ul>
</div>
If you click any number for a list, it is added to the other list.
I'm trying to build a custom checkbox component with options that are generated with a v-for loop from an array with options and values. How can I bind the v-model correctly to the checkbox component so that it's correctly updated?
The problem now is that the model only updates to the latest checkbox that is checked and does not give an array with all checked options.
Vue.component('ui-checkbox', {
props: {
label: {
type: String,
required: true,
},
index: {
type: Number
},
inputValue: {
type: String
}
},
methods: {
onChange(e) {
this.$emit('input', e.target.value);
},
},
template: `<div>
<input
:id="index"
type="checkbox"
:value="inputValue"
#change="onChange" />
<label :for="index">
{{ label }}
</label>
</div>`,
})
new Vue({
el: "#app",
data: {
checkOptions: [
{
label: 'Option 1',
value: 'value of option 1',
},
{
label: 'Option 2',
value: 'value of option 2',
},
{
label: 'Option 3',
value: 'value of option 3',
},
{
label: 'Option 4',
value: 'value of option 4',
},
],
myCheckBoxModel: []
},
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<span>checked Checkboxes: {{ myCheckBoxModel }} </span>
<ui-checkbox
v-for="(option, index) in checkOptions"
v-model="myCheckBoxModel"
:key="index"
:index="index"
:input-value="option.value"
:label="option.label" />
</div>
When you do
this.$emit('input', e.target.value);
it works like
myCheckBoxModel = e.target.value
So it just assigns the value of the last checkbox you clicked to myCheckBoxModel.
If you want to keep all checked items in myCheckBoxModel, you need to do the following:
add value property to ui-checkbox component to have access to the current value of myCheckBoxModel. Value is default property name for this goal (see vue guide).
in your onChange method copy the current value to the variable, because it's not good to mutate value property directly
if your checkbox is checked, push the correspondent value to the array. If the checkbox is not checked, delete to correspondent value from the array
emit input event with the resulting array as value
Vue.component('ui-checkbox', {
props: {
label: {
type: String,
required: true,
},
index: {
type: Number
},
inputValue: {
type: String
},
value: {
type: Array
}
},
methods: {
onChange(e) {
let currentValue = [...this.value]
if (e.target.checked) {
currentValue.push(e.target.value)
} else {
currentValue = currentValue.filter(item => item !== e.target.value)
}
this.$emit('input', currentValue);
},
},
template: `<div>
<input
:id="index"
type="checkbox"
:value="inputValue"
#change="onChange" />
<label :for="index">
{{ label }}
</label>
</div>`,
})
new Vue({
el: "#app",
data: {
checkOptions: [
{
label: 'Option 1',
value: 'value of option 1',
},
{
label: 'Option 2',
value: 'value of option 2',
},
{
label: 'Option 3',
value: 'value of option 3',
},
{
label: 'Option 4',
value: 'value of option 4',
},
],
myCheckBoxModel: []
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
checked Checkboxes:
<span v-for="item in myCheckBoxModel"> {{ item }}; </span>
<ui-checkbox
v-for="(option, index) in checkOptions"
v-model="myCheckBoxModel"
:key="index"
:index="index"
:input-value="option.value"
:label="option.label" />
</div>
I don't know if you need to set checkbox state programmatically, i.e. when you change myCheckBoxModel the state of checkboxes changes correspondently. If you do, you need to watch value property in your ui-checkbox component: set the state of the check box in dependance of if its value is in value array. Do the same also in created hook if you want to initialize the state of checkboxes by myChexkboxModel
The solution presented by #Lana is just too complicated. The onChange method is not needed at all - what you want is to use build-in power of v-model. See below...
Vue.component('ui-checkbox', {
props: {
label: {
type: String,
required: true,
},
index: {
type: Number
},
inputValue: {
type: String
},
value: {
type: Array
}
},
computed: {
model: {
get() {
return this.value
},
set(value) {
this.$emit('input', value)
}
},
},
template: `<div>
<input
:id="index"
type="checkbox"
:value="inputValue"
v-model="model" />
<label :for="index">
{{ label }}
</label>
</div>`,
})
new Vue({
el: "#app",
data: {
checkOptions: [{
label: 'Option 1',
value: 'value of option 1',
},
{
label: 'Option 2',
value: 'value of option 2',
},
{
label: 'Option 3',
value: 'value of option 3',
},
{
label: 'Option 4',
value: 'value of option 4',
},
],
myCheckBoxModel: []
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
model: {{ myCheckBoxModel }}
<ui-checkbox v-for="(option, index) in checkOptions" v-model="myCheckBoxModel" :key="index" :index="index" :input-value="option.value" :label="option.label" />
</div>
NOTE that using v-for index as a key works in this case BUT is not recommended in general, especially if set of checkboxes is dynamic and can change over the lifetime of the component that is rendering them.
It's been hours that I'm trying to get the id of the selected option in v-select, but it returns me the object not the id.
Is there any way to get only the Id (object Id) of the selected option?
I already checked the documentation site:
https://sagalbot.github.io/vue-select/docs/
I also checked the various examples in:
https://codepen.io/collection/nrkgxV/
But so far I have not found the concrete solution to my problem. What is missing or am I doing wrong?
My code:
<template>
<div>
<v-select
v-model="selectedId"
:options="items"
label="name"
></v-select>
</div>
</template>
<script>
export default {
data () {
return {
items: [
{id: 1, name: "User 1", creator_id: 3},
{id: 2, name: "User 2", creator_id: 1},
{id: 4, name: "User 3", creator_id: 3},
],
selectedId: '',
...
}
}
Instead of using v-model , you can listen the event on the select:
Vue.component("v-select", VueSelect.VueSelect);
new Vue({
el: "#app",
data () {
return {
items: [
{id: 1, name: "User 1", creator_id: 3},
{id: 2, name: "User 2", creator_id: 1},
{id: 4, name: "User 3", creator_id: 3},
],
selectedId: ''
}
},
methods: {
selectId(e) {
this.selectedId = e.id
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue-select/2.5.1/vue-select.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.min.js"></script>
<div id="app">
<v-select
#input="selectId($event)"
:options="items"
label="name">
</v-select>
<p>Selected ID: {{ selectedId }}</p>
</div>
how about add a computed props id
<script>
export default {
data () {
return {
items: [
{id: 1, name: "User 1", creator_id: 3},
{id: 2, name: "User 2", creator_id: 1},
{id: 4, name: "User 3", creator_id: 3},
],
selectedId: {}
}
},
computed: {
id: function () {
return (this.selectedId && this.selectedId.id)?this.selectedId.id:'';
}
}
}
</script>
It took me quite a while to figure out but apparently you can use :reduce="item = item.id"
See: https://vue-select.org/guide/values.html#getting-and-setting
A real life saver since the "computed" approach wasn't gonna cut it in my case
Returning a single key with reduce
f you need to return a single key, or transform the selection before it is synced, vue-select provides a reduce callback that allows you to transform a selected option before it is passed to the #input event. Consider this data structure:
let options = [{code: 'CA', country: 'Canada'}];
If we want to display the country, but return the code to v-model, we can use the reduce prop to receive only the data that's required.
<v-select :options="options" :reduce="country => country.code" label="country" />
https://vue-select.org/guide/values.html#transforming-selections
In the v-select add these attributes
item-value="id" item-text="name"
Suppose I want to display a List of Questions. For each question, there is a list of answers, none of which are right or wrong. For each question, the user can choose an answer. I'm wondering how to create two-way binding on the selected answer.
The Vue:
new Vue(
{
el: "#app",
data:
{
questions: [{}]
}
}
Example Question Model:
{
id: 1,
name: "Which color is your favorite?",
selectedAnswerId: null,
selectedAnswerName: null,
answers:
[
{id: 1, name: red, photoUrl: ".../red", selected: false},
{id: 2, name: green, photoUrl: ".../green", selected: false},
{id: 3, name: blue, photoUrl: ".../blue", selected: false},
]
}
Components:
var myAnswer =
{
props: ["id", "name", "url", "selected"],
template:
`
<div class="answer" v-bind:class="{selected: selected}">
<img class="answer-photo" v-bind:src="url">
<div class="answer-name">{{name}}</div>
</div>
`
};
Vue.component("my-question",
{
props: ["id", "name", "answers"],
components:
{
"my-answer": myAnswer
},
template:
`
<div class ="question">
<div class="question-name">{{name}}</div>
<div class="question-answers">
<my-answer v-for="answer in answers" v-bind:id="answer.id" v-bind:name="answer.name" v-bind:url="answer.photoUrl" v-bind:selected="answer.selected"></my-answer>
</div>
</div>
`
});
When the user selects an answer to a question by clicking on the div, I want the Question model's selectedAnswerId/selectedAnswerName along with the answers selected property to be set accordingly. Therefore, what do I need to add to my components in order to accomplish this two-way binding? I believe it requires input elements and v-model, but I couldn't quite figure it out. Also, I am only one day into Vue.js and have no experience with related frameworks. So if I am doing anything blatantly wrong or against best practice, that would be good to know as well. Thanks in advance!
The answer will handle a click event and emit a (custom) selected-answer event. The question will have its own data item to store the selected answer ID; the answer component's selected prop will be based on that. The question will handle the selected-answer event by setting its selectedId.
var myAnswer = {
props: ["id", "name", "url", "selected"],
template: `
<div class="answer" v-bind:class="{selected: selected}"
#click="setSelection()"
>
<img class="answer-photo" :src="url">
<div class="answer-name">{{name}}</div>
</div>
`,
methods: {
setSelection() {
this.$emit('selected-answer', this.id);
}
}
};
Vue.component("my-question", {
props: ["id", "name", "answers"],
data() {
return {
selectedId: null
};
},
components: {
"my-answer": myAnswer
},
template: `
<div class ="question">
<div class="question-name">{{name}}</div>
<div class="question-answers">
<my-answer v-for="answer in answers"
:id="answer.id" :name="answer.name" :url="answer.photoUrl"
:selected="answer.id === selectedId" #selected-answer="selectAnswer"></my-answer>
</div>
</div>
`,
methods: {
selectAnswer(answerId) {
this.selectedId = answerId;
}
}
});
new Vue({
el: '#app',
data: {
questions: [{
id: 1,
name: "Which color is your favorite?",
answers: [{
id: 1,
name: 'red',
photoUrl: ".../red"
},
{
id: 2,
name: 'green',
photoUrl: ".../green"
},
{
id: 3,
name: 'blue',
photoUrl: ".../blue"
},
]
}]
}
});
.answer {
cursor: pointer;
}
.selected {
background-color: #f0f0f0;
}
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.2.6/vue.min.js"></script>
<div id="app">
<my-question v-for="q in questions" :name="q.name" :answers="q.answers"></my-question>
</div>