exporting bootstrap table to excel or pdf - vue.js

I'm using bootstrap vue table and I've want to export a bootstrap table to excel or pdf. It's like a table inside a table and I'm finding it difficult as to how to export it for downloading as excel or pdf as the other solutions I have come around are all involve just giving the json data and it does the work but can't find any solution regarding my problem.
Codesandbox demonstration
<template>
<div id="app">
<b-button #click="exportTable" class="mb-3">Export</b-button>
<b-table-simple outlined id="htmltable">
<b-thead class="b-table__head">
<b-tr>
<b-th class="small-tab">Goods</b-th>
<b-th>Units</b-th>
<b-th>Price Per Unit</b-th>
<b-th>Total Price</b-th>
</b-tr>
</b-thead>
<b-tbody v-for="(service, index) in goodsGroupedByCategory" :key="index">
<b-tr class="category-line">
<b-th class="small-tab cs-textstyle-paragraph-small-bold">{{
index
}}</b-th>
<b-td></b-td>
<b-td></b-td>
<b-th class="cs-textstyle-paragraph-small-bold">{{
service.reduce(function (prev, curr) {
return prev + curr.total_units * curr.price_per_unit;
}, 0)
}}</b-th>
</b-tr>
<b-tr
v-for="serviceItem in service"
:key="serviceItem.id"
class="item-line"
>
<b-td class="big-tab cs-textstyle-paragraph-small">{{
serviceItem.billing_sku_name
}}</b-td>
<b-td class="cs-textstyle-paragraph-small">{{
serviceItem.total_units
}}</b-td>
<b-td class="cs-textstyle-paragraph-small">{{
serviceItem.price_per_unit
}}</b-td>
<b-td class="cs-textstyle-paragraph-small">{{
serviceItem.total_units * serviceItem.price_per_unit
}}</b-td>
</b-tr>
</b-tbody>
</b-table-simple>
</div>
</template>
<script>
import _ from "lodash";
export default {
name: "App",
components: {},
data() {
return {
invoice: [
{
id: "123",
billing_sku_id: "FOOD_ITEMS",
billing_sku_name: "Rice",
total_units: 1,
billing_sku_category: "Food Items",
price_per_unit: 3,
},
{
id: "456",
billing_sku_id: "FOOD_ITEMS",
billing_sku_name: "Wheat",
total_units: 3,
billing_sku_category: "Food Items",
price_per_unit: 5,
},
{
id: "789",
billing_sku_id: "ELECTRICITY_ITEMS",
billing_sku_name: "Bulb",
total_units: 5,
billing_sku_category: "Electricity Items",
price_per_unit: 50,
},
],
};
},
computed: {
goodsGroupedByCategory() {
return _.groupBy(this.invoice, "billing_sku_category");
},
},
methods: {
exportTable() {
console.log("hello");
},
},
};
</script>
<style>
#app {
font-family: "Avenir", Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
padding: 10px;
}
</style>

I found a solution with the help of vue-html2pdf package. First I created a separate component called TableCompo and the code of it comes here:
TableCompo.vue:
<template>
<div id="tableMe">
<b-table-simple outlined id="htmltable">
<b-thead class="b-table__head">
<b-tr>
<b-th class="small-tab">Goods</b-th>
<b-th>Units</b-th>
<b-th>Price Per Unit</b-th>
<b-th>Total Price</b-th>
</b-tr>
</b-thead>
<b-tbody v-for="(service, index) in goodsGroupedByCategory" :key="index">
<b-tr class="category-line">
<b-th class="small-tab cs-textstyle-paragraph-small-bold">{{
index
}}</b-th>
<b-td></b-td>
<b-td></b-td>
<b-th class="cs-textstyle-paragraph-small-bold">{{
service.reduce(function (prev, curr) {
return prev + curr.total_units * curr.price_per_unit;
}, 0)
}}</b-th>
</b-tr>
<b-tr
v-for="serviceItem in service"
:key="serviceItem.id"
class="item-line"
>
<b-td class="big-tab cs-textstyle-paragraph-small">{{
serviceItem.billing_sku_name
}}</b-td>
<b-td class="cs-textstyle-paragraph-small">{{
serviceItem.total_units
}}</b-td>
<b-td class="cs-textstyle-paragraph-small">{{
serviceItem.price_per_unit
}}</b-td>
<b-td class="cs-textstyle-paragraph-small">{{
serviceItem.total_units * serviceItem.price_per_unit
}}</b-td>
</b-tr>
</b-tbody>
</b-table-simple>
</div>
</template>
<script>
import _ from "lodash";
export default {
name: "TableCompo",
data() {
return {
invoice: [
{
id: "123",
billing_sku_id: "FOOD_ITEMS",
billing_sku_name: "Rice",
total_units: 1,
billing_sku_category: "Food Items",
price_per_unit: 3,
},
{
id: "456",
billing_sku_id: "FOOD_ITEMS",
billing_sku_name: "Wheat",
total_units: 3,
billing_sku_category: "Food Items",
price_per_unit: 5,
},
{
id: "789",
billing_sku_id: "ELECTRICITY_ITEMS",
billing_sku_name: "Bulb",
total_units: 5,
billing_sku_category: "Electricity Items",
price_per_unit: 50,
},
],
};
},
computed: {
goodsGroupedByCategory() {
return _.groupBy(this.invoice, "billing_sku_category");
},
},
}
</script>
<style scoped>
#tableMe {
font-family: "Avenir", Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
padding: 10px;
}
</style>
After that I created a parent component (or a view) that using this component and vue-html2pdf package to export the content of table as pdf:
TableView.vue:
<template>
<div>
<b-button #click="generateReport" class="mb-3">Export</b-button>
<!-- first usage of table component: this is for showing the table in browser for users -->
<table-compo></table-compo>
<vue-html2pdf
:show-layout="false"
:float-layout="true"
:enable-download="true"
:preview-modal="false"
:paginate-elements-by-height="1400"
filename="my-table"
:pdf-quality="2"
:manual-pagination="false"
pdf-format="a5"
pdf-orientation="landscape"
pdf-content-width="100%"
ref="html2Pdf"
>
<section slot="pdf-content">
<!-- second usage of table component: this is for putting the contents of table to final pdf -->
<table-compo></table-compo>
</section>
</vue-html2pdf>
</div>
</template>
<script>
import VueHtml2pdf from 'vue-html2pdf';
import TableCompo from '../components/TableCompo'
export default {
name: "TableView",
components: {
VueHtml2pdf,
TableCompo
},
methods: {
/*
Generate Report using refs and calling the
refs function generatePdf()
*/
generateReport () {
this.$refs.html2Pdf.generatePdf();
}
},
}
</script>
<style scoped>
</style>
If you are not happy with my settings, see the documentation of that package to find your desired settings.

You have this package: https://www.npmjs.com/package/xlsx
In your html, create a button and with the #click="exportInvoiceButton()". That's one of the way you can do it in Frontend, but, alternatively, and probably the best way to do this export is from the backend, so you can have more flexibility of what to export.
import XLSX from 'xlsx';
//...
methods: {
//...
exportInvoiceButton() {
const invoices = this.invoices.reduce((ac, invoice) => {
ac.push({
billing_sku_id: invoice.billing_sku_id,
billing_sku_name: invoice.billing_sku_name,
total_units: invoice.total_units,
// ...
});
return ac;
}, [])
var invoicesWS = XLSX.utils.json_to_sheet(invoices)
// A workbook is the name given to an Excel file
var wb = XLSX.utils.book_new() // make Workbook of Excel
// add Worksheet to Workbook
// Workbook contains one or more worksheets
XLSX.utils.book_append_sheet(wb, invoicesWS, 'Invoices list') // invoices list is name of Worksheet
// export Excel file
XLSX.writeFile(wb, 'Invoices.xlsx')
}
//...
}

Related

Why is nuxt-link refreshing the page when used with Bootstrap-vue?

I am using nuxt and bootstrap to build a custom hover dropdown menu for navigation. The issue I have is that my navigation submenu NuxtLinks are refreshing the entire page instead of smoothly changing the app content in my Nuxt block. The nav bar is dynamically generated in the default.vue layout and uses a b-dropdown-hover component where the NuxtLink is wrapped around that content. Why does the page do a full refresh for those links/anchors but my b-navbar-brand image does a smooth transition? I apologize, I am very new to Nuxt. This video # ~1:35:00 demonstrates what I'm trying to do.
components/BDropdownHoverRight.vue
<template>
<nuxt-link :to="aTo">
<div class="ddr-top" #mouseover="onOver1($event.target)" #mouseleave="onLeave1($event.target)">
<b-dropdown ref="dropdown_ddr" :text="cText" class="m-md-2 ddr">
<slot></slot>
</b-dropdown>
</div>
</nuxt-link>
</template>
<script>
export default {
name: 'BDropdownHoverRight',
props: {
cText: {
type: String,
},
aTo: {
type: String,
},
},
methods: {
onOver1(t) {
if (t.nodeName === 'DIV') {
console.log(t)
console.log(t.nodeName)
let num_child_nodes = 0
try {
if (t.querySelectorAll(':scope > ul')[0].getElementsByTagName('div').length >= 0) {
num_child_nodes = t.querySelectorAll(':scope > ul')[0].getElementsByTagName('div').length
}
} catch (e) {
if (t.querySelectorAll(':scope > div > ul')[0].getElementsByTagName('div').length >= 0) {
num_child_nodes = t.querySelectorAll(':scope > div > ul')[0].getElementsByTagName('div').length
}
}
if (num_child_nodes > 0) {
try {
t.querySelectorAll(':scope > div > ul')[0].style.display = 'block'
} catch (e) {
try {
t.querySelectorAll(':scope > ul')[0].style.display = 'block'
} catch (e) {}
}
}
}
},
onLeave1(t) {
try {
t.querySelectorAll(':scope > div > ul')[0].style.display = 'none'
} catch (e) {
try {
t.querySelectorAll(':scope > ul')[0].style.display = 'none'
} catch (e) {}
}
},
},
}
</script>
layouts/default.vue
<template>
<div>
<b-navbar id="top-nav-bar" toggleable="lg" type="light" sticky>
<b-navbar-brand to="/">
<Rabbit id="tl-logo" />
</b-navbar-brand>
<b-navbar-toggle target="nav-collapse"></b-navbar-toggle>
<b-collapse id="nav-collapse" is-nav>
<b-navbar-nav>
<template v-for="dir in navtop_dd">
<b-dropdown-hover
:key="dir.id"
:c-text="dir.name"
:a-to="dir.hasOwnProperty('ato') ? dir.ato : '/nolink'"
>
<template v-if="'submenus' in dir && dir.submenus.length > 0">
<template v-for="dir1 in dir.submenus">
<b-dropdown-hover-right
:key="dir1.id"
:c-text="dir1.name"
:a-to="dir1.hasOwnProperty('ato') ? dir1.ato : '/nolink'"
>
<template v-if="'submenus' in dir1 && dir1.submenus.length > 0">
<template v-for="dir2 in dir1.submenus">
<b-dropdown-hover-right
:key="dir2.id"
:c-text="dir2.name"
:a-to="dir2.hasOwnProperty('ato') ? dir2.ato : '/nolink'"
>
</b-dropdown-hover-right>
</template>
</template>
</b-dropdown-hover-right>
</template>
</template>
</b-dropdown-hover>
</template>
</b-navbar-nav>
<!-- Right aligned nav items -->
<b-navbar-nav class="ml-auto">
<b-nav-form>
<b-form-input size="sm" class="mr-sm-2" placeholder="Search"></b-form-input>
<b-button size="sm" class="my-2 my-sm-0" type="submit">Search</b-button>
</b-nav-form>
<b-nav-item-dropdown right>
<!-- Using 'button-content' slot -->
<template #button-content>
<b-img src="../assets/imgs/account-circle.svg" style="height: 35px"> </b-img>
<!-- <em>User</em> -->
</template>
<b-dropdown-item href="#">Profile</b-dropdown-item>
<b-dropdown-item href="#">Sign Out</b-dropdown-item>
</b-nav-item-dropdown>
</b-navbar-nav>
</b-collapse>
</b-navbar>
<b-container id="app-content">
<Nuxt />
</b-container>
<div id="footer">
<div style="height: 100%; padding: 5px">© 2021</div>
</div>
</div>
</template>
<script>
import BDropdownHover from '#/components/BDropdownHover'
import BDropdownHoverRight from '#/components/BDropdownHoverRight'
export default {
components: {
BDropdownHover,
BDropdownHoverRight,
},
data() {
return {
navtop_dd: [
{
id: 1,
name: 'Transactions',
ato: '/transactions',
submenus: [
{
id: '1a',
name: 'Sales Orders',
ato: '/transactions/salesorders',
submenus: [
{
id: '1b',
name: 'New',
},
{
id: '2b',
name: 'List',
},
],
},
{
id: '2a',
name: 'Item Fulfillments',
ato: '/transactions/itemfulfillments',
submenus: [
{
id: '1b',
name: 'New',
},
{
id: '2b',
name: 'List',
},
],
},
],
},
{
id: 2,
name: 'Inventory',
},
{
id: 3,
name: 'Reports',
},
{
id: 4,
name: 'Setup',
},
{
id: 5,
name: 'Support',
},
],
}
},
mounted() {
var x = document.querySelectorAll('.b-dropdown.navtop-dd')
for (var i = 0; i < x.length; i++) {
if (x[i].querySelectorAll(':scope > ul')[0].getElementsByTagName('div').length == 0) {
var btn = x[i].querySelectorAll(':scope > .btn')[0]
btn.classList += ' no-content-after'
}
}
var x = document.querySelectorAll('.b-dropdown.ddr')
for (var i = 0; i < x.length; i++) {
if (x[i].querySelectorAll(':scope > ul')[0].getElementsByTagName('div').length == 0) {
var btn = x[i].querySelectorAll(':scope > .btn')[0]
btn.classList += ' no-content-after'
}
}
},
}
</script>
<style>
#top-nav-bar {
border-bottom: 1px solid green;
}
#tl-logo {
height: 40px;
margin: 5px;
}
#footer {
height: 40px;
color: black;
border-top: 1px solid green;
margin: auto;
text-align: center;
display: flex;
align-items: center;
justify-content: space-around;
}
.navtop-dd button {
background: none !important;
color: #6c757d !important;
border: none !important;
}
#app-content {
margin: 20px auto;
}
.ddr > button::after {
display: inline-block;
margin-left: 0.555em;
right: 0px;
content: "";
border-top: 0.25em solid transparent;
border-right: 0.3em solid transparent;
border-bottom: 0.25em solid transparent;
border-left: 0.35em solid;
vertical-align: 0.075em;
}
.b-dropdown {
width: 100%;
}
.ddr > button {
text-align: left;
}
.no-content-after::after {
content: none !important;
}
.ddr > ul {
top: -1.2rem;
left: calc(100% - 0.5rem);
}
.dropdown-menu {
min-width: 0 !important;
}
.dropdown-item {
color: #6C757D;
}
.ddr-top:hover {
background-color: #e4ffda;
}
a:hover {
text-decoration: none !important;
}
</style>
There is a LOT of irrelevant code here. I took the time to format it properly. Please make the effort yourself next time (to format and input interesting bits only).
Also, the answer on how to fix the issue was actually given in the video itself. The video is talking about the differences between a and nuxt-link tags.
Which relates to this part of Bootstrap's Vue documentation where you can see that
[to] prop: Denotes the target route of the link. When clicked, the value of the to prop will be passed to router.push() internally, so the value can be either a string or a Location descriptor object
So, you should use something like this
<template>
<b-dropdown>
<template #button-content>
Custom <strong>Content</strong> with <em>HTML</em> via Slot
</template>
<b-dropdown-item to="/test">Go to test page via Vue-router</b-dropdown-item>
</b-dropdown>
</template>
I also saw that your code is rather different from the video. You should not use querySelector, you don't have to import Nuxt components neither and you have several ESlint warning/errors.
I do recommend trying to focus on a single part of learning and not mixing all of them. It's fine to want to go a bit further, but be careful of not being lost with too much abstraction while you do learn a lot of new concepts (Vue/Nuxt).
On a side note, if you want to continue learning Nuxt, you can check this: https://masteringnuxt.com/ (created by a Nuxt ambassador and other well known people in the Vue ecosystem)
Have fun creating projects with Nuxt!

Nuxt / Vuex / Vue Reactivity Issue Increment

Hi everyone I am I having some difficulty when working with Nuxt and Vuex.
I am trying to run through the example Vuex / Nuxt Classic Mode.
https://nuxtjs.org/guide/vuex-store/
After clicking my increment button I dont see the number go up. My page just stays at 0, I can see within the console that the state knows the number is no longer 0 but not on the screen, as if it doesnt know to be reactive.
My assumption is that I have miss configured something somewhere and my 0 is not the actual state, but I created some copy of it somehow.
Here is my button within my template.
<button #click="inc">{{ counter }}</button>
Here is my inc function within my methods.
inc () {
this.$store.commit('increment')
},
Here is my computed
computed: {
counter () {
return this.$store.getters.counter
}
}
Here is my Vuex/index.js file contained within the store folder.
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const createStore = () => {
return new Vuex.Store({
state: () => ({
counter: 0
}),
getters: {
counter: state => state.counter
},
mutations: {
increment (state) {
state.counter++
}
}
})
}
export default createStore
Update: Included more code snippets, and replied to existing comments.
#Boussadjra Brahim #xaviert, thank you both for weighing in, I appreciate the assistance.
#Boussadjra Brahim - Yes, I had tried using an action that called the mutation, that didnt seem to get me there either. I also tried adjusting the state via the action alone, and wasnt able to make any changes, however that seems correct, as I am under the impression that actions call mutations to make state changes and do not themselves do so, please correct me if you know more. I am 100% open to the idea that I did not attempt it correctly. Below is that action that didnt do anything and the one that called the mutation
actions: {
increment (state) {
state.counter++
}
},
And here is the version with the action calling the mutation.
actions: {
incrementCounterUp () {
this.commit('increment')
}
},
#xaviert - I have tried starting the server over, and have also tried to see if an nuxt build followed by a firebase serve, to see if maybe that helped. It did not. My normal server start is 'npm run dev'. In hopes that you/anyone else may be able to find my mistake below is my full _id.vue component and also my nuxt.config.js file as maybe that's it. Its still pretty raw and could use a lot of refactoring so hope you can sort through it well enough.
_.id.vue
<template>
<div class="product">
<div class="product-image">
<div class="product-image-img">
<img v-bind:src="product.image_file" width="450px;"/>
</div>
</div>
<div class="product-details-wrapper">
<div class="product-details">
<h1>
<div v-if="this.$route.query.editPage">
<textarea #input="updateTextArea" ref="textarea2" v-model="product.item_name" type="text" />
</div>
<div v-else>{{product.item_name}}</div>
</h1>
<div class="product-description">
<div class="product-description-text" v-if="this.$route.query.editPage">
<textarea #input="updateTextArea" ref="textarea" v-model="product.description" type="text" />
</div>
<div class="product-description-text" v-else v-html="product.description"></div>
</div>
<p class="product-brand"><strong>Brand - </strong> {{product.brand_name}}</p>
<hr />
<div class="product-price">
<div v-if="this.$route.query.editPage">
<strong>Original Price - </strong>
<input v-model="product.msrp" type="text" />
</div>
<div v-else class="product-msrp">
<strong>Original Price - </strong>
<span class="strike">${{product.msrp}}</span>
</div>
<div v-if="this.$route.query.editPage">
<strong>Sale Price - </strong>
<input v-model="product.price" type="text" />
</div>
<div v-else class="product-sale-price">
<strong>Sale Price - </strong>
<span class="">${{product.price}}</span>
</div>
<div class="product-price">
Quantity x
<input #input="updateQuantity" v-model="quantity" min="1" class="" type="number" value="1" />
</div>
<button #click="inc">{{ counter }}</button>
</div>
</div>
</div>
<div v-if="this.$route.query.editPage" class="update-product"> <button #click="updateProduct(product)">Update</button></div>
<div class="footer">
<router-link to="/privacy-policy" target="_blank">Privacy</router-link> |
<router-link to="/terms" target="_blank">Terms</router-link>
</div>
</div>
</template>
<script>
// # is an alias to /src
import firebase from '#/services/fireinit'
import foo from '#/components/foo'
const db = firebase.firestore()
export default {
name: 'ProductPage',
head () {
return {
title: this.product.item_name
}
},
components: {
foo
},
data: function () {
return {
product: {},
image: '',
name: 'Checkout',
description: '',
currency: 'USD',
amount: '',
msrp: '',
quantity: 1
}
},
methods: {
inc () {
this.$store.dispatch('incrementCounterUp', true)
},
updateProduct: function (product) {
db.collection('products').doc(product.item_id).set(product)
.then(function () {
console.log('Document successfully written!')
})
.catch(function (error) {
console.error('Error writing document: ', error)
})
},
updateQuantity () {
this.product.msrp = (this.quantity * this.product.orgMsrp)
this.product.msrp = Math.round(100 * this.product.msrp) / 100
this.product.price = this.quantity * this.product.orgPrice
this.product.price = Math.round(100 * this.product.price) / 100
},
updateTextArea () {
this.$refs.textarea.style.minHeight = this.$refs.textarea.scrollHeight + 'px'
this.$refs.textarea2.style.minHeight = this.$refs.textarea2.scrollHeight + 'px'
}
},
async asyncData({app, params, error}) {
const ref = db.collection("products").doc(params.id)
let snap
let thisProduct = {}
try {
snap = await ref.get()
thisProduct = snap.data()
thisProduct.orgMsrp = thisProduct.msrp
thisProduct.orgPrice = thisProduct.price
} catch (e) {
// TODO: error handling
console.error(e)
}
return {
product: thisProduct
}
},
mounted () {
if(this.$refs.textarea) {
this.$refs.textarea.style.minHeight = this.$refs.textarea.scrollHeight + 'px'
this.$refs.textarea2.style.minHeight = this.$refs.textarea2.scrollHeight + 'px'
}
},
computed: {
counter () {
return this.$store.getters.counter
}
}
}
</script>
<style lang="less">
body {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #2c3e50;
margin: 0
}
p{
margin-top: 1em;
margin-bottom: 1em;
}
html, body, #__nuxt, #__layout, .default, .product{
height: 100%;
}
.product {
justify-content: center;
display: flex;
max-width: 1150px;
margin: 0 auto;
flex-wrap: wrap;
align-items: center;
padding: 0 24px;
&-price{
input {
border: 1px solid;
padding: 0 .3em;
text-align: center;
width: 50px;
}
}
}
.product-details{
width: 100%;
textarea {
width: 100%;
font-size: inherit;
color: inherit;
font-family: inherit;
font-weight: inherit;
height: initial;
resize: none;
background-color: transparent;
border: none;
}
h1{
font-size: 1.9rem;
margin: 15px 0 20px;
}
hr{
width: 50%;
margin: .5rem 0px;
}
p{
}
}
.product-description-text{
margin: 10px 0;
}
.product-image, .product-details-wrapper{
align-items: center;
display: flex;
justify-content: center;
}
.product-details-wrapper{
flex: 0 1 535px;
}
.product-image{
flex: 0 1 535px;
img{
width: 100%;
}
}
.product-price{
.strike{
text-decoration: line-through;
}
button{
display: flex;
width: 150px;
height: 50px;
border-radius: 5px;
justify-content: center;
font-size: 24px;
margin-top: 20px;
&:hover{
cursor: pointer;
background-color: #f1f1f1;
box-shadow: 3px 3px 11px -1px rgba(0, 0, 0, 0.48);
}
}
}
.product-sale-price{
color: #f30000;
}
.footer {
flex: 1 1 100%;
text-align: center;
color: #ccc;
margin-top: 25px;
padding: 15px;
a {
color: #ccc;
text-decoration: none;
&:hover{
text-decoration: underline;
}
}
}
.update-product{
position: absolute;
top: 0;
text-align: center;
}
</style>
nuxt.confgs.js
const pkg = require('./package')
const { STRIPE_TOKEN } = process.env;
module.exports = {
vue: {
config: {
productionTip: false,
devtools: true
}
},
buildDir: './functions/nuxt',
mode: 'universal',
/*
** Headers of the page
*/
head: {
title: pkg.name,
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ hid: 'description', name: 'description', content: pkg.description }
],
link: [
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
]
},
/*
** Customize the progress-bar color
*/
loading: { color: '#fff' },
/*
** Global CSS
*/
css: [
],
/*
** Plugins to load before mounting the App
*/
/*
** Nuxt.js modules
*/
modules: [
// Doc: https://github.com/nuxt-community/axios-module#usage
'#nuxtjs/axios',
'nuxt-stripe-module'
],
stripe: {
version: 'v3',
publishableKey: 'pk_test_XXX',
},
/*
** Axios module configuration
*/
axios: {
// See https://github.com/nuxt-community/axios-module#options
},
/*
** Build configuration
*/
build: {
/*
** You can extend webpack config here
*/
publicPath: '/public/',
vendor: [],
extractCSS: true,
bable: {
presets: [
'es2015',
'stage-8'
],
plugins: [
['transform-runtime', {
'polyfill': true,
'regenerator': true
}]
]
},
extend (config, { isDev }) {
if (isDev && process.client) {
config.module.rules.push({
enforce: 'pre',
test: /\.(js|vue)$/,
loader: 'eslint-loader',
exclude: /(node_modules)/
})
}
},
router: {
middleware: 'router-auth'
}
},
plugins: [
{ src: '~/plugins/fireauth', ssr: true }
]
}
store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const createStore = () => {
return new Vuex.Store({
state: () => ({
counter: 0
}),
actions: {
incrementCounterUp () {
this.commit('increment')
}
},
getters: {
counter: state => state.counter
},
mutations: {
increment (state) {
state.counter++
}
}
})
}
export default createStore
At the end of the day, I was not able to identify exactly what in my application was the error.
I assume that my during my initialization, configuration or development I touched something that should not have been touched, installed something that should not have be installed, messed with a package I should not have or was to bold in my nuxt.config.js changes.
I created a new nuxt app following the same install instructions. https://nuxtjs.org/guide/installation/
Moved the above _id.vue component exactly as it is and once I got dependencies updated it worked perfectly as seen in the image below.
Thank you very much #Boussadjra Brahim, #xaviert, #Andrew1325 for you assistance.

Unexpected behaviour removing a child component (row)

Description:
I have a table with some products, each row is a custom vue <row> component.
Each element has a closing (removing) button that triggers the custom "remove" event. The main app listens to this event and removes the children (by index)
The row a part from some static text it contains an input with a number.
The problem:
The parent (Vue app) removes the row, but the value of the input is then moved (and replaces its previous value) to the input in the next row.
Expected behaviour:
I want to simply remove the item I do not care about the value of the text input once it's removed. It should not move its value to the next sibling.
I attach an example.
let row = Vue.component('row', {
name: 'row',
props: ['number', 'name', 'sq'],
data: () => ({
quantity: 0
}),
template: '<tr>' +
'<td>{{number}}</td>' +
'<td>{{name}}</td>' +
'<td><button v-on:click="quantity--">-</button><input type="text" :value="quantity"><button v-on:click="quantity++">+</button></td>' +
'<td><button v-on:click="remove">×</button></td>' +
'</tr>',
methods: {
remove: function() {
this.$emit('remove', this.quantity)
}
},
beforeMount() {
this.quantity = this.sq
}
})
new Vue({
el: "#app",
data: {
out: [],
rows: [{
name: "Icecream",
sq: 0
},
{
name: "Sugar cube",
sq: 50
},
{
name: "Peanut butter",
sq: 0
},
{
name: "Heavy cream",
sq: 0
},
{
name: "Cramberry juice",
sq: 0
}
]
},
methods: {
removeRow: function(index, quantity) {
this.out.push(`Removing row ${index} (${this.rows[index].name} | ${quantity} units)`)
this.rows.splice(index, 1)
}
},
computed: {
log: function() {
return this.out.join("\r\n")
}
}
})
body {
background: #20262E;
padding: 20px;
font-family: Helvetica;
}
h2 {
font-weight: bold;
margin-bottom: 10px;
}
#app {
background: #fff;
border-radius: 4px;
padding: 20px;
transition: all 0.2s;
}
td {
padding: 4px 5px;
}
input {
width: 40px;
text-align: center;
}
h4 {
margin-top: 20px;
margin-bottom: 5px;
}
#log {
padding: 10px;
background: #20262E;
color: #fff;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.min.js"></script>
<div id="app">
<h2>Cart:</h2>
<table>
<row v-for="(row, index) in rows" :number="index" :name="row.name" :sq="row.sq" v-on:remove="removeRow(index, $event)"></row>
</table>
<h4>Log</h4>
<pre id="log" v-html="log"></pre>
</div>
As #Bert mentioned in the comments.
The problem was that I was missing a key.
https://v2.vuejs.org/v2/api/#key
Adding it solved the problem
Thanks

VueJS - onclick to make active on new Array entry not working

https://codepen.io/donnieberry97/pen/GGKQRN
var demo = new Vue({
el: '#main',
data: {
services: [
{
name: 'Item 1',
price: 200,
active: true
},
{
name: 'Item 2',
price: 500,
active: false
},
{
name: 'Item 3',
price: 700,
active: false
}
]
},
methods: {
addItem: function() {
var newItem= {
name:this.name,
price:this.price
};
this.services.push(newItem);
this.name="";
this.price="";
toggleActive();
},
toggleActive: function(f) {
f.active = !f.active;
},
total: function(){
var total=0;
this.services.forEach(function(f){
if(f.active){
total+=f.price;
}
});
return total;
}
}
});
When you use the input to add a new entry to the services array, upon clicking it afterwards, the active tag does not get applied to the new entry. It should turn blue and add to the total price but only the hover state works.
I've modified you code at method 'addItem' and use computed property total instead total method,have a look:
var demo = new Vue({
el: '#main',
data: {
services: [
{
name: 'Item 1',
price: 200,
active: true
},
{
name: 'Item 2',
price: 500,
active: false
},
{
name: 'Item 3',
price: 700,
active: false
}
]
},
computed: {
total () {
return this.services.reduce((last,item)=>last + parseInt(item.price) * item.active,0)
}
},
methods: {
addItem: function() {
var newItem= {
name:this.name,
price:this.price,
active: true
};
this.services.push(newItem);
this.name="";
this.price="";
},
toggleActive: function(f) {
f.active = !f.active;
}
}
});
* {
padding: 0;
margin: 0;
}
body{
font-family: 'Roboto', sans-serif !important;
}
h3 {
text-align:center;
padding: 2em 0em;
}
h5 {
padding: 1.5em 0.5em;;
box-sizing:border-box;
}
.container {
width:600px;
margin: 0 auto;
}
ul {
list-style:none;
}
li {
color:black;
border:1px solid #eeeeee;
padding:0.5em;
border-left: 5px solid #2196F3;
height:30px;
line-height:30px;
transition: 0.4s ease;
}
.active {
background-color:#2196F3;
color:white;
transition: 0.3s;
transition: 0.4s ease;
}
.active:hover {
background-color:#2196F3;
}
li:hover {
background-color:#82c4f8;
transition: 0.4s ease;
cursor:pointer;
}
span {
float:right;
}
#main {
box-shadow: 0 19px 38px rgba(0,0,0,0.0), 0 6px 12px rgba(0,0,0,0.22)
}
.text-center {
text-align:center;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/1.0.27/vue.min.js"></script>
<div class="container">
<div id="main">
<div class="header"><h3>Click the services you wish to have:</div>
<ul>
<li class="group-item" v-for="service in services" v-on:click="toggleActive(service)" v-bind:class="{'active': service.active}">{{service.name}} <span>{{service.price | currency}}</span></li>
</ul>
<h5>Total is: {{total | currency}}</h5>
<input type="text" v-model="name" placeholder="name">
<input type="text" v-model="price" placeholder="price">
<button v-on:click="addItem()">Add Item</button>
</div>
</div>
The main issue that you're running into is you're calling an undefined function and not passing a parameter into your toggleActive function.
Since toggleActive is a Vue method, you'll need to use this to reference it correctly and use the function from your Vue instance; once that problem is fixed, you'll need to pass in the item that you're wanting to toggle, because the way that function is written it requires a parameter to update active status.
Here's how you could update your addItem function to get it working:
addItem: function() {
var newItem= {
name:this.name,
price:this.price,
active: false,
};
this.services.push(newItem);
this.name="";
this.price="";
this.toggleActive(this.services[this.services.length - 1]);
},
Also notice that I added the active property during item creation so that Vue treats this as a reactive property. Otherwise, your item will be stuck in the active state (after toggling it) and cannot become inactive on click. You could change this to just be active: true during creation (and remove the call to make it active completely) if all new items are supposed to be active on creation. I didn't do that, though, as I wanted to show how to fix the call to toggleActive.
You can view a forked and updated Codepen here if you'd like to see the code in a fully working state.

Vuejs - How to v-for loop on click

I am learning Vue and trying to complete a task myself.
I would like to find out how to run v-for loop on #click so that on initial state, only "city" template appears, and on each click, tour template renders with related tours.
let EventBus = new Vue();
Vue.component('cities', {
name: 'Listings',
props: ['city','tour'],
template: `
<div>
<div class='city-list box'>
<city v-for='city in cities' :id='city.id' :key='city.id' :city='city' :tour='tour' #click.native='select(city)'></city>
</div>
<div class='tour-list box'>
<tours v-for='tour in filter(tours)' :id='tour.id' :key='tour.id' :tour='tour'></tours>
</div>
</div>
`,
data() {
return {
cities: [
{id:1, name: 'Istanbul'},
{id:2, name: 'Paris'},
{id:3, name: 'Barça'},
{id:4, name: 'Rome'},
{id:5, name: 'Mars'}
],
tours: [
{id:1, cid:1, name: 'Bosphorus'},
{id:2, cid:2, name: 'Eiffel'},
{id:3, cid:3, name: 'La Sagrada Familia'},
{id:4, cid:4, name: 'Colosseum'},
{id:5, cid:5, name: 'Mars Canyon'},
{id:6, cid:1, name: 'Sultanahmet'},
{id:7, cid:2, name: 'Champs-Élysées'},
{id:8, cid:3, name: 'Casa Mila'},
{id:9, cid:4, name: 'Trevi Fountain'},
{id:10, cid:5, name: 'Mars Desert'},
]
};
},
methods: {
select(city) {
console.log('select');
EventBus.$emit('filter', city);
},
filter(tours) {
console.log('filter');
EventBus.$on('select', ()=>{
cid = this.city.id;
return tours.filter(function(tour) {
return tour.cid == cid;
});
});
},
},
components: {
'city': {
name: 'City',
props: ['city'],
template: `
<div :id="[city.name.toLowerCase()]" :class="[city.name.toLowerCase()]">
<h1>{{ city.name }}</h1>
</div>`
},
'tour': {
name: 'Tour',
props: ['city', 'tour'],
template: `
<div :class="['tour-' + tour.id]" :id="[city.name.toLowerCase() + '-tours']" :refs="city.id" :data-id="city.id">
{{ tour.name }}
</div>
`,
},
},
});
new Vue({
el: '#root'
});
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
}
.box {
margin: 48px auto;
max-width: 1024px;
display: flex;
justify-content: center;
align-items: center;
}
.box h1 {
font-size: 1.1rem;
color: #41f;
}
.box > div {
padding: 24px;
margin: 12px;
width: 20%;
min-height: 100px;
border-radius: 2px;
font-size: 1.15rem;
line-height: 1;
}
.box > div:nth-child(1)
{background-color: #ffb3ba;}
.box > div:nth-child(2)
{background-color: #ffdfba;}
.box > div:nth-child(3)
{background-color: #ffffba;}
.box > div:nth-child(4)
{background-color: #baffc9;}
.box > div:nth-child(5)
{background-color: #bae1ff;}
<script src="https://unpkg.com/vue#2.4.4/dist/vue.js"></script>
<div id="root">
<cities></cities>
</div>
I am also interested in the state of art, if it is a good practice to have two templates together (which are related), and connect this model with a db and router (city/tour-list). Or how would you approach to a such case (I guess jsfiddle should be self explanatory).
As a side note I have tried adding tour as a child to parent component [jsfiddle] where I filter results by ID, I am not sure if this way is a better approach both for components and filtering results in the sense of architecture.
https://jsfiddle.net/oy5fdc0r/29/
https://jsfiddle.net/oy5fdc0r/30/
Use a data property to keep track of the selected city, instead of an Eventbus. Then you can use a computed property to show the correct tours, based on the selected city.
computed:{
selectedTours(){
return this.tours.filter(tour=>tour.cid == this.selectedCity.id)
}
},
methods: {
select(city) {
this.selectedCity = city;
},
},