I am using the <b-table> component from BootstrapVue and trying to customize field output by using a formatter callback function. The table data displays fine , but for some reason the callback function method branchName() is not being called and the value is not being formatted to the branch name instead of the branch id.
I set up a codesandbox to demonstrate the problem: code demo
The goal of the method is to return the name of the branch item. However, only the branch_id is being returned. In other words, the record row under the Branch table column should say ACME10 and not 10.
My file named App.vue:
<template>
<div id="app">
<b-table striped hover :items="userProfiles"></b-table>
</div>
</template>
<script>
export default {
data() {
return {
userProfiles: [
{
uid: "3",
branch: 10
},
{
uid: "1",
branch: 20
},
{
uid: "2",
branch: 13
}
],
branches: [
{
branch_id: 13,
branch: "ACME13"
},
{
branch_id: 10,
branch: "ACME10"
},
{
branch_id: 20,
branch: "ACME20"
}
],
fields: [
{
key: "branch",
formatter: "branchName"
}
]
};
},
methods: {
branchName(value) {
const name = this.branches[0].find(branch => value === branch.branch_id);
return name;
}
}
};
</script>
Thanks for your help!
To answer the OPs original question (why the formatter function was not being called):
In your App.vue file you are missing binding your fields definition array to the fields prop of <b-table>. Try this:
<template>
<div id="app">
<b-table striped hover :items="userProfiles" :fields="fields"></b-table>
</div>
</template>
If <b-table> isn't passed a field definition array, it will peek at the first row's item data and grab the field names that it finds, and then generates its own internal fields definition (just the field keys and humanized labels)
You should use slots to implement custom behavior like. For more information https://bootstrap-vue.js.org/docs/components/table#scoped-field-slots
Here is how you could implement your code
<template>
<div id="app">
<b-table striped hover :items="userProfiles">
<template slot="[uid]" slot-scope="data">{{ data.item.uid }}</template>
<template slot="[branch]" slot-scope="data">{{ branchName(data.item.branch) }}</template>
</b-table>
</div>
</template>
Method:
branchName(value) {
const branch = this.branches.find(branch => value === branch.branch_id);
if (branch) {
return branch.branch
}
return ""
}
try this in your template
<template v-slot:cell(name)="data">
<router-link :to="/route/${data.index}">
{{ data.value }}
</router-link>
Related
I am trying to use b-form-checkbox with b-table. Trying to retrieve the selected module Ids.
<b-table
id="module-table"
:items="list.modules"
:fields="fields"
:busy="isBusy">
<template slot="num" slot-scope="row">
{{ row.index + 1 }}
</template>
<template slot="checkbox" slot-scope="row">
<b-form-group>
<b-form-checkbox v-if="!isLoading" v-model="row.item.selected" #change="selection(row.item.moduleId)" variant="outline-secondary" class="mr-1">
</b-form-checkbox>
</b-form-group>
</template>
</b-table>
data: {
fields: [
{ key: 'checkbox', label: '', class: 'd-none d-lg-table-cell' },
{ key: 'num', label: 'Num', class: 'd-none d-lg-table-cell' },
],
selected: []
}
Though I am able to retrieve the selected module Ids but unable to delete them while switching the checkboxes. If anyone can provide an idea on how to track if the checkbox is selected (true) or not selected (false). Thanks in advance.
methods: {
selection(item) {
if (true)
app.selected.push(item);
else
app.selected = app.selected.map(x => x != item);
}
},
The checkbox values are already stored in list.modules[].selected via the v-model, so you could just use a computed prop to get the selected modules instead of using a separate selected array:
Remove the #change="selection(⋯)" from <b-form-checkbox>:
<!--
<b-form-checkbox
v-model="row.item.selected"
#change="selection(row.item.moduleId)" // remove
>
-->
<b-form-checkbox
v-model="row.item.selected"
>
Remove the selection method and selected data property, since they're no longer needed.
export default {
data() {
return {
// selected: [] // remove
}
},
methods: {
// selection() {⋯} // remove
}
}
Add a computed prop that filters list.modules[] for selected modules:
export default {
computed: {
selected() {
return this.list.modules.filter(module => module.selected)
},
},
}
demo
I'm trying to build a simple page builder which has a row elements, its child column elements and the last the component which I need to call. For this I designed and architecture where I'm having the dataset defined to the root component and pushing the data to its child elements via props. So let say I have a root component:
<template>
<div>
<button #click.prevent="addRowField"></button>
<row-element v-if="elements.length" v-for="(row, index) in elements" :key="'row_index_'+index" :attrs="row.attrs" :child_components="row.child_components" :row_index="index"></row-element>
</div>
</template>
<script>
import RowElement from "../../Components/Builder/RowElement";
export default {
name: "edit",
data(){
return{
elements: [],
}
},
created() {
this.listenToEvents();
},
components: {
RowElement,
},
methods:{
addRowField() {
const row_element = {
component: 'row',
attrs: {},
child_components: [
]
}
this.elements.push(row_element)
}
},
}
</script>
Here you can see I've a button where I'm trying to push the element and its elements are being passed to its child elements via props, so this RowElement component is having following code:
<template>
<div>
<column-element v-if="child_components.length" v-for="(column,index) in child_components" :key="'column_index_'+index" :attrs="column.attrs" :child_components="column.child_components" :row_index="row_index" :column_index="index"></column-element>
</div>
<button #click="addColumn"></button>
</template>
<script>
import ColumnElement from "./ColumnElement";
export default {
name: "RowElement",
components: {ColumnElement},
props: {
attrs: Object,
child_components: Array,
row_index: Number
},
methods:{
addColumn(type, index) {
this.selectColumn= false
let column_element = {
component: 'column',
child_components: []
};
let component = {}
//Some logic here then we are emitting event so that it goes to parent element and there it can push the columns
eventBus.$emit('add-columns', {column: column_element, index: index});
}
}
}
</script>
So now I have to listen for event on root page so I'm having:
eventBus.$on('add-columns', (data) => {
if(typeof this.elements[data.index] !== 'undefined')
this.elements[data.index].child_components.push(data.column)
});
Now again I need these data accessible to again ColumnComponent so in columnComponent file I have:
<template>
//some extra div to have extended features
<builder-element
v-if="!loading"
v-for="(item, index) in child_components"
:key="'element_index_'+index" :column_index="column_index"
:element_index="index" class="border bg-white"
:element="item" :row_index="row_index"
>
</builder-element>
</template>
<script>
export default {
name: "ColumnElement",
props: {
attrs: Object,
child_components: Array,
row_index: Number,
column_index: Number
},
}
</script>
And my final BuilderElement
<template>
<div v-if="typeof element.component !== 'undefined'" class="h-10 w-10 mt-1 mb-2 mr-3 cursor-pointer font-bold text-white rounded-lg">
<div>{{element.component}}</div>
<img class="h-10 w-10 mr-3" :src="getDetails(item.component, 'icon')">
</div>
<div v-if="typeof element.component !== 'undefined'" class="flex-col text-left">
<h5 class="text-blue-500 font-bold">{{getDetails(item.component, 'title')}}</h5>
<p class="text-xs text-gray-600 mt-1">{{getDetails(item.component, 'desc')}}</p>
</div>
</template>
<script>
export default {
name: "BuilderElement",
data(){
return{
components:[
{id: 1, title:'Row', icon:'/project-assets/images/row.png', desc:'Place content elements inside the row', component_name: 'row'},
//list of rest all the components available
]
}
},
props: {
element: Object,
row_index: Number,
column_index: Number,
element_index: Number,
},
methods:{
addElement(item,index){
//Some logic to find out details
let component_element = {
component: item.component_name,
attrs: {},
child_components: [
]
}
eventBus.$emit('add-component', {component: component_element, row_index: this.row_index, column_index: this.column_index, element_index: this.element_index});
},
getDetails(component, data) {
let index = _.findIndex(this.components, (a) => {
return a.component_name === component;
})
console.log('Component'+ component);
console.log('Index '+index);
if(index > -1) {
let component_details = this.components[index];
return component_details[data];
}
else
return null;
},
},
}
</script>
As you can see I'm again emitting the event named add-component which is again listened in the root component so for this is made following listener:
eventBus.$on('add-component', (data) => {
this.elements[data.row_index].child_components[data.column_index].child_components[data.element_index] = data.component
});
which shows the data set in my vue-devtools but it is not appearing in the builder element:
Images FYR:
This is my root component:
This is my RowComponent:
This is my ColumnComponent:
This is my builder element:
I don't know why this data not getting passed to its child component, I mean last component is not reactive to props, any better idea is really appreciated.
Thanks
The issue is with the way you're setting your data in the addComponent method.
Vue cannot pick up changes when you change an array by directly modifying it's index, something like,
arr[0] = 10
As defined in their change detection guide for array mutations,
Vue wraps an observed array’s mutation methods so they will also
trigger view updates. The wrapped methods are:
push()
pop()
shift()
unshift()
splice()
sort()
reverse()
So you can change.
this.elements[data.row_index].child_components[data.column_index].child_components[data.element_index] = data.component
To,
this.elements[data.row_index].child_components[data.column_index].child_components.splice(data.element_index, 1, data.component);
If I have a template as such with FruitPrices as a component:
<template>
<div id="app">
<div>
<span #click=SOME_FUNCTION> Change currency </span>
<FruitPrices fruit="apple"></FruitPrices>
<FruitPrices fruit="banana"></FruitPrices>
<FruitPrices fruit="orange"></FruitPrices>
...
....
.....
</template>
...
...
...
import FruitPrices from ./component/FruitPrices.vue
I was wondering would it be possible to have SOME_FUNCTION be a method that alters the currency across all the FruitPrices Components? i.e. if some components are in USD, some are in GBP, can I set them all to Euros via a method in the current main App.?
You could set a property in the data object for currency and use that across all components and have a function alter it.
data() {
return {
currency: "USD",
...
}
},
methods: {
changeCurrency(newCurrency){
this.currency = newCurrency;
}
}
and in the template
<span #click=changeCurrency('Euro')> Change currency </span>
Let's use this component to pass the currency, price and fruit to each component FruitPrices component:
<template>
<div>
<!-- USD is selected by default, once the option change, the prices will be updated -->
<select v-model="currency">
<!-- allows to select one currency at time, it shows all the currencies
that exist in the currencies array data property -->
<option
v-for="(currencyOption, index) in currencies"
:key="index"
:value="currencyOption"
>
<!-- show only the currency name as option -->
{{ currencyOption.name }}
</option>
</select>
<div v-for="(fruit, fruitIndex) in fruitsWithPrices" :key="fruitIndex">
<!-- if everything is working fine, you don't need close tags for empty
custom component, you can use '/' at the end of the first tag to self close it-->
<FruitPrices :name="fruit.name" :convertedPrice="fruit.convertedPrice" />
</div>
</div>
</template>
<script>
import FruitPrices from '../somePlace/FruitPrices'
export default {
name: "FruitPricesContainer",
components: { FruitPrices },
data: () => ({
fruits: [
{
name: 'Apple',
price: 0.2
},
{
name: 'Banana',
price: 0.3
},
{
name: 'Orange',
price: 0.25
}
],
currency: {
// Base currency, exchangeRate = 1
exchangeRate: 1,
name: 'USD'
},
// Used exchange rates only for demo purpose, not are the real and valid exchange rates
currencies: [
{
exchangeRate: 1,
name: 'USD'
},
{
exchangeRate: 1.2,
name: 'EUR'
},
{
exchangeRate: 0.7,
name: 'SGD'
},
{
exchangeRate: 700,
name: 'MXN'
},
{
exchangeRate: 3700,
name: 'COP'
}
]
}),
computed: {
// The fruitsWithPrices listen for changes in both, the fruit and
// the currency, so once you change it by selecting a different currency
// the prices will be updated automatically (nice)
fruitsWithPrices() {
return this.fruits.map((fruit) => {
return {
...fruit, // Add name and original price to the returned object
convertedPrice: fruit.price * this.currency.exchangeRate // Add the price based on the exchange rate
}
})
}
}
}
</script>
Now, let's create the FruitPrices component:
<template>
<div>
<p>{{ name }}: {{ convertedPrice }}</p>
</div>
</template>
<script>
export default {
name: "FruitPrices",
props: {
name: {
type: String,
required: true
},
convertedPrice: {
type: Number,
required: true
}
}
}
</script>
And it's ready! (Tbh, I didn't test it, please let me know if you have errors or problems).
I don't know if I got the logic of your problem but maybe you can use the vue props like this:
<FruitPrices fruit="apple" :currency="newCurrency || 'USD'"></FruitPrices>
<FruitPrices fruit="banana" :currency="newCurrency || 'GBP'"></FruitPrices>
<FruitPrices fruit="orange" :currency="newCurrency || 'USD'"></FruitPrices>
you can define the props currency inside the component FruitPrices. If the currency prop is not defined then the second currency is going to be taken as default (for example "newCurrency || 'GBP'" if newCurrency is null then the currency GBP is taken as default).
Vue docs for props: https://v2.vuejs.org/v2/guide/components-props.html
Then in the main template component, you define the variable newCurrency and you pass that variable to the prop currency of the component FruitPrices:
data: () => {
newCurrency: null
},
methods: {
setNewCurrency() {
this.newCurrency = this.someFieldOfYourForm;
}
}
I have a Vue.js text-input component like the following:
<template>
<input
type="text"
:id="name"
:name="name"
v-model="inputValue"
>
</template>
<script>
export default {
props: ['name', 'value'],
data: function () {
return {
inputValue: this.value
};
},
watch: {
inputValue: function () {
eventBus.$emit('inputChanged', {
type: 'text',
name: this.name,
value: this.inputValue
});
}
}
};
</script>
And I am using that text-input in another component as follows:
<ul>
<li v-for="row in rows" :key="row.id">
<text-input :name="row.name" :value="row.value">
</text-input>
</li>
</ul>
Then, within the JS of the component using text-input, I have code like the following for removing li rows:
this.rows = this.rows.filter((row, i) => i !== idx);
The filter method is properly removing the row that has an index of idx from the rows array, and in the parent component, I can confirm that the row is indeed gone, however, if I have, for example, two rows, the first with a value of 1 and the second with a value of 2, and then I delete the first row, even though the remaining row has a value of 2, I am still seeing 1 in the text input.
Why? I don't understand why Vue.js is not updating the value of the text input, even though the value of value is clearly changing from 1 to 2, and I can confirm that in the parent component.
Maybe I'm just not understanding how Vue.js and v-model work, but it seems like the value of the text input should update. Any advice/explanation would be greatly appreciated. Thank you.
You cannot mutate values between components like that.
Here is a sample snippet on how to properly pass values back and forth. You will need to use computed setter/getter. Added a button to change the value and reflect it back to the instance. It works for both directions.
<template>
<div>
<input type="text" :id="name" v-model="inputValue" />
<button #click="inputValue='value2'">click</button>
</div>
</template>
<script>
export default {
props: ['name', 'value'],
computed: {
inputValue: {
get() {
return this.value;
},
set(val) {
this.$emit('updated', val);
}
}
}
}
</script>
Notice that the "#updated" event updates back the local variable with the updated value:
<text-input :name="row.name" :value="row.value" #updated="item=>row.value=item"></text-input>
From your code you are trying to listen to changes.. in v-model data..
// Your Vue components
<template>
<input
type="text"
:id="name"
:name="name"
v-model="inputValue"
>
</template>
<script>
export default {
props: ['name', 'value'],
data: function () {
return {
inputValue: ""
};
},
};
</script>
If You really want to listen for changes..
<ul>
<li v-for="row in rows" :key="row.id">
<text-input #keyup="_keyUp" :name="row.name" :value="row.value">
</text-input>
</li>
</ul>
in your component file
<template>...</template>
<script>
export default {
props: ['name', 'value'],
data: function () {
return {
inputValue: ""
};
},
methods : {
_keyUp : () => {// handle events here}
};
</script>
check here for events on input here
To bind value from props..
get the props value, then assign it to 'inputValue' variable
it will reflect in tthe input element
Well I hope to explain, I'm generating this data from a component, when I click the checkbox changes in the generated data are not reflected, but when clicking the button with a data already in the instance changes are made, I appreciate if you explain Why or do they have a solution?
this my code
js
Vue.component('fighters', {
template: '#fighters-template',
data() {
return {
items: [
{ name: 'Ryu' },
{ name: 'Ken' },
{ name: 'Akuma' }
],
msg: 'hi'
}
},
methods: {
newData() {
this.items.forEach(item => {
item.id = Math.random().toString(36).substr(2, 9);
item.game = 'Street Figther';
item.show = false;
});
this.items.push()
},
greeting() {
this.msg = 'hola'
}
}
});
new Vue({
el: '#app'
})
html
<main id="app">
<fighters></fighters>
</main>
<template id="fighters-template">
<div class="o-container--sm u-my1">
<ul>
<li v-for="item in items">
<input type="checkbox" v-model="item.show">
{{ item.name }}</li>
</ul>
<button class="c-button c-button--primary" #click="newData()">New Data</button>
<h2>{{ msg }}</h2>
<button class="c-button c-button--primary" #click="greeting()">Greeting</button>
<hr>
<pre>{{ items }}</pre>
</div>
</template>
this live code
https://jsfiddle.net/cqx12a00/1/
Thanks for you help
You don't declare the show variables that your checkboxes are bound to, so they are not reactive – Vue is not aware when one is updated.
It should be initialized like so:
items: [
{ name: 'Ryu', show: false },
{ name: 'Ken', show: false },
{ name: 'Akuma', show: false }
]
You might think that newData would fix it, since it assigns a show member to each item, but Vue cannot detect added properties so they're still not reactive. If you initialized the data as I show above, then the assignment would be reactive.
If you want to add a new reactive property to an object, you should use Vue.set.