Smart filter in Bootstrap Vue - vuejs2

I was wondering if there is any way to implement the Intelligent DataTables filter in the Bootstrap-Vue table, I have searched everywhere, but I have not found any functional solution to implement in my project.
DataTable.net smart filter image

We needed that component so we created it. Hope it helps:
SearchTable.vue
<template>
<div class="search-table h-100 justify-content-center align-items-center"
v-bind:class="{row: data.length === 0}" v-if="isMounted">
<div v-if="data.length > 0">
<div class="d-flex justify-content-between">
<!-- main search -->
<b-input-group size="xs">
<b-form-input v-model="searchInput"></b-form-input>
<b-input-group-append is-text>
<b-icon icon="search"></b-icon>
</b-input-group-append>
</b-input-group>
</div>
<div class="d-flex justify-content-between mt-2 mb-0">
<b-button-group>
<!-- dropdown -->
<b-dropdown id="col-dropdown" class="col-dropdown" no-flip text="Visibilité">
<b-dropdown-item :key="field.key" class="p-0" style="padding: 0" v-for="field in fields"
v-if="field.key !== 'action'">
<div #click.stop="onDropdownClick(field.key)"
class="checkbox-wrapper">
<b-form-checkbox
:checked="isColumnDisplayed(field.key)"
disabled
>
{{ field.label || field.key }}
</b-form-checkbox>
</div>
</b-dropdown-item>
</b-dropdown>
<b-button :variant="noneOfSearchMethodIsUsed ? '' : 'danger'" #click="cancelFilters">Enlever filtre</b-button>
<!-- dropdown action groupées -->
<slot name="groupped-actions"></slot>
</b-button-group>
<div align="right" style="display: inline-flex">
<span style="margin: 4px;">Afficher</span>
<b-form-select
v-model="perPage"
:options="perPageOptions"
size="sm"
></b-form-select>
<span style="margin: 4px;">éléments</span>
</div>
</div>
<div class="d-flex justify-content-between mt-0 mb-2">
<span style="margin-top: 5px;">{{ buildInformationLine }}</span>
<!-- pagination -->
<b-pagination
:per-page="perPage"
:total-rows="formattedData.length"
align="right"
class="my-0 mt-1"
size="sm"
v-model="currentPage"
></b-pagination>
</div>
<!-- TABLE -->
<b-table
:current-page="currentPage"
:fields="fieldsToShow"
:items="formattedData"
:per-page="perPage"
foot-clone
no-footer-sorting
primary-key="id"
:sticky-header="true"
responsive
striped
>
<!-- action col template -->
<template
v-if="!!$scopedSlots.action"
v-slot:cell(action)="row">
<slot name="action" v-bind="row.item"></slot>
</template>
<!-- html escape template -->
<template v-slot:cell()="data">
<span v-html="data.value"></span>
</template>
<!-- footer -->
<template v-slot:foot()="data">
<input :value="getFieldFromKey(data.column).searchVal"
#input="setFieldSearchValue(data.column, $event.target.value)"
v-if="getFieldFromKey(data.column).key !== 'action'"
class="w-100"
placeholder="Recherche">
</template>
</b-table>
<div class="d-flex justify-content-between mt-0">
<span style="margin-top: 5px;">{{ buildInformationLine }}</span>
<!-- pagination -->
<b-pagination
:per-page="perPage"
:total-rows="formattedData.length"
align="right"
class="my-0 mt-1"
size="sm"
v-model="currentPage"
></b-pagination>
</div>
</div>
<div v-else>
<p>Aucun résultat</p>
</div>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator'
import BvTableField from '../../interfaces/BvTableField'
enum SearchFusionMethod {
Union = 'union',
Intersection = 'intersection',
}
interface FieldsInteractiveInterface extends BvTableField {
searchVal: string
stickyColumn: boolean
}
#Component
export default class SearchTable extends Vue {
// The array containing the data objects
#Prop(Array) readonly data!: any[]
// The array containing the info of each column. key must be equal to key in object data
#Prop(Array) readonly fields!: BvTableField[]
#Prop({default: SearchFusionMethod.Intersection}) readonly searchFusionMethod!: SearchFusionMethod
#Prop({default: 'highlight'}) readonly highlighterClass!: string
mainHighlighterClass: string = this.highlighterClass
#Prop({default: 'field-highlight'}) readonly fieldHighlighterClass!: string
currentPage = 1
perPage = 10
perPageOptions = [10, 25, 50, 100]
searchInput = ''
isMounted = false
// Contains the value of each column search field
fieldsInteractive: FieldsInteractiveInterface[] = []
// ---
mainHilightColor: string = 'yellow'
fieldHilightColor: string = 'orange'
get fieldsToShow(): BvTableField[] {
return this.fieldsInteractive.filter(field => {
return field.display
})
}
get noneColumnSearchFieldIsUsed(): boolean {
return this.numberOfSearchFieldsUsed === 0
}
get numberOfSearchFieldsUsed(): number {
return this.fieldsInteractive.reduce((count: number, field) => {
return count + (field.searchVal !== '' ? 1 : 0)
}, 0)
}
// (01), (10)
get exactlyOneSearchMethodIsUsed(): boolean {
return (this.searchInput !== '' && this.noneColumnSearchFieldIsUsed) || (this.searchInput === '' && !this.noneColumnSearchFieldIsUsed)
}
// (00)
get noneOfSearchMethodIsUsed(): boolean {
return (this.searchInput === '' && this.noneColumnSearchFieldIsUsed)
}
// (11)
get bothSearchMethodsAreUsed(): boolean {
return (this.searchInput !== '' && !this.noneColumnSearchFieldIsUsed)
}
get onlyMainSearchIsUsed(): boolean {
return (this.searchInput !== '' && this.noneColumnSearchFieldIsUsed)
}
get onlyFieldSearchIsUsed(): boolean {
return (this.searchInput === '' && !this.noneColumnSearchFieldIsUsed)
}
get buildInformationLine(): string {
const txt: String[] = []
txt.push("Affichage de l'élément")
txt.push(this.formattedData.length === 0 ? '0' : (((this.currentPage-1) * this.perPage)+1).toString())
txt.push('à')
txt.push((this.currentPage * this.perPage < this.formattedData.length ? this.currentPage * this.perPage : this.formattedData.length).toString())
txt.push('sur')
txt.push((this.formattedData.length).toString())
txt.push('éléments')
if (this.formattedData.length < this.data.length) {
txt.push('(filtré de')
txt.push((this.data.length).toString())
txt.push('éléments au total)')
}
return txt.join(' ')
}
// Data with
get formattedData() {
const mapped = this.data
.map((item: any) => {
const itemWithHighlight: any = {}
this.fields.forEach(field => {
itemWithHighlight[field.key] = this.replaceBySearch(field.key, item[field.key])
})
return itemWithHighlight
})
return mapped
.filter((item: any) => {
// (searchInput,columnSearchField)
// If there is no filter at all, return the row (00)
if (this.noneOfSearchMethodIsUsed) return true
let countFromMainHighlight = 0
let countFromFieldHighlight = 0
// loop through each field
for (const [key, col] of Object.entries(item)) {
if (!this.fieldsInteractive[this.fieldsInteractive.findIndex(x => x.key === key)].display) continue // Only search in displayed column
if (typeof col !== 'string') continue // only check in string values
if (this.onlyMainSearchIsUsed) {
// if only one of the search method has been used, return anything having a 'highlight' class (01), (10)
if (col.includes('fromMainSearch') || col.includes(this.fieldHighlighterClass)) {
return true
}
} else {
// if both of the search method have been used, filter according to the searchFusionMethod (11)
if (this.searchFusionMethod === SearchFusionMethod.Intersection) {
// TODO: search only in class attribute of markup (faster)
if (col.includes('fromMainSearch')) {
countFromMainHighlight++
}
if (col.includes('fromFieldSearch')) {
countFromFieldHighlight++
}
} else if (this.searchFusionMethod === SearchFusionMethod.Union) {
if (col.includes(`<span class="${this.highlighterClass}`)) {
// TODO
return true
}
}
}
}
// determine whether we keep the row
if (this.bothSearchMethodsAreUsed) {
return countFromMainHighlight > 0 && countFromFieldHighlight === this.numberOfSearchFieldsUsed
} else {
if (this.onlyMainSearchIsUsed) {
return countFromFieldHighlight > 0
} else if (this.onlyFieldSearchIsUsed) {
return countFromFieldHighlight === this.numberOfSearchFieldsUsed
}
}
})
}
isColumnDisplayed(key: string) {
const field = this.getFieldFromKey(key)
return field.display
}
setFieldSearchValue(key: string, searchVal: string) {
const index = this.fieldsInteractive.findIndex(field => field.key === key)
if (index === -1) throw new DOMException('Key not found')
Vue.set(this.fieldsInteractive, index, {
...this.fieldsInteractive[index],
searchVal: searchVal
})
// this.fieldsInteractive[index].searchVal = searchVal
}
mounted() {
// programatically add action column if slot given
if (!!this.$scopedSlots.action) {
const fieldAction = {key: 'action'}
this.fields.push(fieldAction)
}
// init column search values
this.fields.forEach(field => {
if (field.key === 'action') {
this.fieldsInteractive.unshift({
...field,
searchVal: '',
sortable: false,
display: field.display ?? true,
stickyColumn: true
})
} else {
this.fieldsInteractive.push({
...field,
searchVal: '',
sortable: field.sortable ?? true,
display: field.display ?? true,
stickyColumn: false
})
}
})
this.isMounted = true
}
onDropdownClick(key: string) {
for (const index in this.fieldsInteractive) {
if (this.fieldsInteractive[index].key === key) {
this.fieldsInteractive[index].display = !this.fieldsInteractive[index].display // toggle
return
}
}
}
private cancelFilters(): void {
this.fieldsInteractive = this.fieldsInteractive.map((field) => {
field.searchVal = ''
return field
})
this.searchInput = ''
}
private getFieldFromKey(key: string): FieldsInteractiveInterface {
const f = this.fieldsInteractive.find(field => field.key === key)
if (f === undefined) {
throw new DOMException('Key not found')
}
return f
}
private replaceBySearch(key: string, str: string | any) {
if ((this.searchInput === '' && this.noneColumnSearchFieldIsUsed)
|| str === undefined || str === null) return str
str = String(str)
// main search bar
if (this.exactlyOneSearchMethodIsUsed || this.bothSearchMethodsAreUsed) {
const regexMain: RegExp | undefined = this.searchInput !== '' ? new RegExp(`${this.searchInput}`, 'i') : undefined
const regexField: RegExp | undefined = this.getFieldFromKey(key).searchVal !== '' ? new RegExp(`${this.getFieldFromKey(key).searchVal}`, 'i') : undefined
const matchMain: string[] | null = regexMain ? (str).match(regexMain) : null
const matchField: string[] | null = regexField ? (str).match(regexField) : null
if (matchMain || matchField) {
str = this.surroundWithHilightClass(str, matchMain, matchField)
}
}
return str
}
// https://stackoverflow.com/questions/1144783/how-can-i-replace-all-occurrences-of-a-string
// replace only if not already contains a highlight class
/**
* #param str string to be surrounded
* #param findMain what is matching with main search
* #param findField what is matching with field search
*/
private surroundWithHilightClass(str: string, findMain: string[] | null, findField: string[] | null) {
const main: string | null = findMain && findMain.length > 0 ? findMain[0] : null
const field: string | null = findField && findField.length > 0 ? findField[0] : null
str = String(str)
// if a search is in another search, put two classes
if (field && main?.includes(field)) {
str = str.replace(new RegExp(main, 'g'), `<span class="${this.mainHighlighterClass} fromFieldSearch fromMainSearch">${main}</span>`)
} else if (main && field?.includes(main)) {
str = str.replace(new RegExp(field, 'g'), `<span class="${this.mainHighlighterClass} fromMainSearch fromFieldSearch">${field}</span>`)
} else {
// here we are sur the highlightning will be separated (this prevents having span in span)
if (main) {
str = str.replace(new RegExp(main, 'g'), `<span class="${this.mainHighlighterClass} fromMainSearch">${main}</span>`)
}
if (field) {
str = str.replace(new RegExp(field, 'g'), `<span class="${this.fieldHighlighterClass} fromFieldSearch">${field}</span>`)
}
}
return str
}
}
</script>
<style lang="scss">
.search-table {
div {
p {
color: gray;
text-align: center;
}
}
span.fromFieldSearch {
background-color: orange; // not defined : var(--main-highlighter-class);
}
/* Why this overrides fromFielSearch even if fromFieldSearch appear after in class order ? */
span.fromMainSearch {
background-color: yellow; // not defined : var(--field-highlighter-class);
}
span.field-highlight {
background-color: orange;
}
.col-dropdown {
.dropdown-item {
padding: 0 !important;
}
}
.checkbox-wrapper {
padding: 4px 24px;
width: 100%;
}
.custom-control-input[disabled] ~ .custom-control-label, .custom-control-input:disabled ~ .custom-control-label {
color: #000 !important;
}
.b-table-sticky-header > .table.b-table > thead > tr > th {
top: -2px !important;
}
.b-table-sticky-header {
max-height: calc(125vh - 400px) !important;
}
.b-table-sticky-header > .table.b-table > tfoot > tr > th {
position: sticky;
bottom: 0;
background-color: white;
z-index: 0;
}
th.b-table-sticky-column {
z-index: 4 !important;
}
}
</style>
The code is a bit messy but it works.
Note: we use vue class component with vuw property decorators

Related

How to make increment/decrement cart count in vue

I've created a function for increment and decrement so that I can change the count in the cards.count.
The function for increment count works like this
but the functions for decrement count, it doesn't work.
When I click the function for decrement, this output will appear
Error in v-on handler: "TypeError: Cannot read property 'product_id' of undefined"
I don't know why there is an error in the product_id, even though the increment function also has a product_id.
I hope you can help me figured out the problem
<template>
<div v-if="cartList && cartList.length > 0">
<div
class="item-loop container col"
v-for="(cards, index) in cartList"
:key="generateKey(cards.product_id, cards.count)"
>
<div class="items row">
<div class="image col-4">
<img class="img-fluid pt-2" :src="cards.product_img" />
</div>
<div class="content text-left col-8">
<button
v-on:click="cartList.splice(index, 1)"
type="button"
class="close"
data-dismiss="modal"
aria-label="Close"
>
<span aria-hidden="true">×</span>
</button>
<h5 class="text-left">{{ cards.product_name }}</h5>
<div class="prices" :key="cards.product_id">
<div class="value-cart form-group row">
<font-awesome-icon
:icon="['far', 'minus-square']"
size="2x"
class="minus-button"
#click="min(cards.product_id)"
/>
<input
type="number"
class="input-pm bg-light form-control p-0 text-center angka"
:value="cards.count"
/>
<font-awesome-icon
:icon="['far', 'plus-square']"
size="2x"
class="plus-button"
#click="plus(cards.product_id)"
/>
</div>
<p>Rp. {{ cards.product_price * cards.count }}</p>
</div>
</div>
</div>
</div>
<div class="cart-order">
<div class="order-total">
<div class="row">
<h4 class="col-6 font-weight-bold text-left">Total</h4>
<h5 class="col-6 font-weight-bold text-right">
Rp. {{ totalprice }}
</h5>
</div>
</div>
<p class="text-left"><strong>*Not including tax(10%)</strong></p>
<b-button
class="mt-3"
variant="primary"
#click="invoice()"
v-b-modal="'modal-checkout'"
block
>Checkout</b-button
>
<b-button class="mt-2" variant="danger" #click="resetcart" block>
Cancel </b-button
><br /><br />
</div>
</div>
</template>
export default {
components: {
Features,
Card,
},
data() {
return {
datamenu: {},
cartList: [],
invoiceid: 0,
tax: 0,
searchMenu: null,
menu: null,
formCheck: {
amount: 0,
invoice: "",
cashier: "abiwardani",
menu_name: "",
},
};
},
methods: {
plus(product_id) {
let result = this.cartList.find((res) => {
if (res.product_id == product_id) {
return res.product_id;
}
});
if (result) {
for (let i = 0; i < this.cartList.length; i++) {
if (this.cartList[i].product_id == product_id) {
const newFoodObject = {
...this.cartList[i],
count: this.cartList[i].count + 1,
};
console.log("plus");
this.$set(this.cartList, i, newFoodObject);
}
}
}
},
min(product_id) {
let result = this.cartList.find((res) => {
if (res.product_id == product_id) {
return res.product_id;
}
});
if (result) {
for (let i = this.cartList.length; i > 0; i--) {
if (this.cartList[i].product_id == product_id && this.cartList[i].count > 0){
const newFoodObject = {
...this.cartList[i],
count: this.cartList[i].count - 1,
};
this.$set(this.cartList, i, newFoodObject);
}
}
}
},
generateKey(key1, key2) {
return `${key1}-${key2}`;
},
},
mounted() {
axios
.get(process.env.VUE_APP_URL + "product")
.then((res) => {
this.datamenu = res.data.result;
})
.catch((err) => {
console.log(err);
});
}
Option 1:
i should start with length-1 and should go up to 0
min(product_id) {
let result = this.cartList.find((res) => {
if (res.product_id == product_id) {
return res.product_id;
}
});
if (result) {
for (let i = this.cartList.length-1; i >= 0; i--) {
if (this.cartList[i].product_id == product_id && this.cartList[i].count > 0){
const newFoodObject = {
...this.cartList[i],
count: this.cartList[i].count - 1,
};
this.$set(this.cartList, i, newFoodObject);
}
}
}
Option 2:
You do not need 2 methods.. just have one
updateQty(product_id,mode) {
let result = this.cartList.find((res) => {
if (res.product_id == product_id) {
return res.product_id;
}
});
if (result) {
for (let i = 0; i < this.cartList.length; i++) {
if (this.cartList[i].product_id == product_id) {
const newFoodObject = {
...this.cartList[i],
count: mode === 'INCRE' ? this.cartList[i].count + 1 : this.cartList[i].count - 1,
};
console.log("plus");
this.$set(this.cartList, i, newFoodObject);
}
}
}
},
use it like
<font-awesome-icon
:icon="['far', 'minus-square']"
size="2x"
class="minus-button"
#click="updateQty(cards.product_id,'INCRE')"
/>
and
<font-awesome-icon
:icon="['far', 'minus-square']"
size="2x"
class="minus-button"
#click="updateQty(cards.product_id,'DECRE')"
/>
Your problem is in the final expression of your for statement inside your min() method.
Change 'i--' with 'i++' to increase your index in each iteration. This will avoid accessing the 'cartList' array with a negative index (which is causing the problem, as it gets undefined):
min(product_id) {
let result = this.cartList.find((res) => {
if (res.product_id == product_id) {
return res.product_id
}
})
if (result) {
for (let i = this.cartList.length; i > 0; i++) {
👆
...
Here is how I did that
HTML:
<button #click="increment()">+</button>
<input :value="amount" />
<button #click="decrement()">-</button>
JS:
data() {
return {
amount: 0,
};
},
methods: {
increment() {
this.amount++;
},
decrement() {
this.amount--;
},
},

Error mesage quickly displaying then hiding as expected in VUE

I have a page which I validate the email add input #blur. This works perfectly and displays the error message if it fails validation rules set but the issue I have is that due to the #blur, when I click my reset button the error quickly displays then hides and this is poor UI and I want to stop it but can't figure out how to.
HTML
<div class="card" v-on:click="select($event)">
<div class="card-body">
<div class="form-group row">
<label class="col-sm-3 col-form-label pr-0" for="emailAddField">Email <span class="text-danger">*</span></label>
<div class="col-sm-9">
<div class="input-group">
<input id="emailAddField" ref="pdEmailAdd" class="form-control" type="search" :value="pdEmailAdd" #input="pdEmailAddInput" #blur="emailInputValChecker($event)" placeholder="Enter an email address">
<div class="input-group-append" :class="emailButtonValidation">
<a class="btn input-group-text primaryBtn" :class="emailButtonValidation" type="button" :href="'mailto:' + pdEmailAdd">
<i class="far fa-envelope"></i>
</a>
</div>
</div>
<div v-if="emailFormatErrorMsg" class="text-danger">Incorrect email address format</div>
</div>
<div class="card-footer">
<button id="resetButton" ref="resetButton" class="btn btn-warning col-4" #click="pdInitPageStates($event)" :disabled="resetDetails">
Reset details
</button>
</div>
</div>
I have 'hacked' at the card trying to use #click on the card to get the id but this didn't work so I set the id in my `data but not happy about it and sure there is a lot better way but I just can't figure it out
Code
data() {
return {
pdEmailAdd: '',
reg: /^(([^<>()\[\]\\.,;:\s#"]+(\.[^<>()\[\]\\.,;:\s#"]+)*)|(".+"))#((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,24}))$/,
detailsChanged: false,
emailIncorrectFormat: false,
targetId: 'resetButton', // HACK
targetId2: '', // HACK
}
},
computed: {
emailButtonValidation() {
if (!this.pdEmailAdd || !this.reg.test(this.pdEmailAdd)) {
if (this.pdEmailAdd === '') {
this.emailIncorrectFormat = false;
} else {
this.emailIncorrectFormat = true;
}
return 'disabled'
} else {
this.emailIncorrectFormat = false;
return ''
}
},
resetDetails() {
this.detailsChanged = false;
if (this.pdName != this.$store.state.account.firstname + ' ' + this.$store.state.account.lastname) {
this.detailsChanged = true;
}
if (this.telNoType === 'ddi' && this.pdTelNo != this.$store.state.account.ddi) {
this.detailsChanged = true;
} else if (this.telNoType === 'mobile' && this.pdTelNo != this.$store.state.account.mobile) {
this.detailsChanged = true;
} else if (this.telNoType === 'na' && this.pdTelNo != '') {
this.detailsChanged = true;
}
if (this.pdExtNo != this.$store.state.account.extension) {
this.detailsChanged = true;
}
if (this.pdEmailAdd != this.$store.state.user.adminemail) {
this.detailsChanged = true;
}
return !this.detailsChanged;
}
}
// Another hack to try set it soon as page loads
mounted() {
this.$refs.resetButton.click();
},
methods: {
emailInputValChecker(event) {
this.emailFormatErrorMsg = false;
if (!this.pdEmailAdd || !this.reg.test(this.pdEmailAdd)) {
if (this.pdEmailAdd === '') {
this.emailFormatErrorMsg = false;
} else {
this.select(event)
// Uses the 'dirty hacks'
if (this.targetId !== '' && this.targetId !== 'resetButton' && this.targetId2 !== 'resetButton') {
this.emailFormatErrorMsg = true;
};
}
}
},
select(event) {
this.targetId = event.target.id;
if (this.targetId === 'resetButton') {
this.targetId2 === 'resetButton';
} else if (this.targetId === '') {
this.targetId === 'resetButton';
}
}
}
Basically all I want is the input to check it passes validation when input is left unless the reset button is clicked then ignore it but think I've gone code blind and can't think of a way to do this.
The mousedown event on your reset button is what causes blur on the input to fire. Adding #mousedown.prevent to the reset button will stop that from happening specifically when the reset button is clicked.
This snippet ought to illustrate the solution. Remove #mousedown.prevent from the reset button and you'll see similar behavior to your issue, with the error briefly flashing.
new Vue({
el: '#app',
data: {
email: '',
error: null
},
methods: {
onBlur () {
if (this.email === 'bad') {
this.error = 'bad email!'
} else {
this.error = null
}
},
onReset () {
this.error = null
this.email = ''
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<button #click="onReset" #mousedown.prevent>Reset</button>
<p>
Type in "bad" and leave input to trigger validation error<br>
<input type="text" v-model="email" #blur="onBlur"/>
</p>
<p>{{ email }}</p>
<p v-if="error">error!</p>
</div>

Pattern to pass computed data as prop to child component (Promise problem)

I am wondering how to pass a computed variable to a child-component (in a slot). The main component revieves an array of objects by using a post request. What actually happpens is that the variable "Branches" seems to be filled with an empty promise which represents the result-data. So i get a warning because the list-component expects "Branches" to be an array. I tried to delay the rendering of the slot content by using "v-if="Array.isArray(Branches)" or a flag which is set in the computed-method ("syncedBranches") but none of these seems to do it.
How to delay the rendering of that list till "Branches" is a filled array of objects? Shouldnt i use a computed var and pass the data by a getter?
Main Component
<branches-widget-tabs :items="register" :activeItem="activeRegister">
<template #tabbody_0="Branches" >
<h1>Content Register 1</h1>
<branches-widget-list :items="Branches" v-if="syncedBranches"></branches-widget-list>
</template>
<template #tabbody_1="Branches" v-if="Array.isArray(Branches)">
<h1>Content Register 2</h1>
<branches-widget-list :items="Branches" v-if="syncedBranches"></branches-widget-list>
</template>
</branches-widget-tabs>
</div>
</template>
<style>
#branchesWidget {
min-width: 150px;
min-height: 150px;
background-color: #333;
}
#branchesWidget:hover {
background-color: #666;
}
</style>
<script>
import chroma from 'chroma-js';
//console.log('chroma',chroma);
import HUSL from 'hsluv';
//console.log('HUSL',HUSL);
import BranchesWidgetTabs from './BranchesWidgetTabs';
import BranchesWidgetList from './BranchesWidgetList';
const random = function(min, max){
return Math.floor(Math.random() * (max - min + 1)) + min;
};
var generateColors = function(n, startHex = '#ff6000', padding = 0, step = 5, randomSat = true, randomLight = true){
let colors = [];
const baseHex = HUSL.hexToHsluv(startHex);
const baseHue = baseHex[0];
//console.log('baseHue',baseHue);
var degrees = [];
for (let i = 0; i < n; i++) {
degrees.push( 360 / n * i);
}
//console.log('degrees',degrees);
const hues = degrees.map((offset) => {
return (baseHue + offset) % 360;
});
//console.log('hues',hues);
if(randomSat){
var baseSaturation = random(55, 85);
}else{
var baseSaturation = baseHex[1];
}
if(randomLight){
var baseLightness = random(35, 75);
}else{
var baseLightness = baseHex[2];
}
var subs = Math.min(n,Math.max(step,2));
for(let i = 0; i < subs; i++) {
colors.push( HUSL.hsluvToHex([
hues[i],
baseSaturation,
baseLightness
]));
}
console.log('colors',colors);
return chroma.scale(colors).padding(0).mode('lab').colors(n);
};
export default {
name: 'BranchesWidget',
props : [],
data() {
return {
activeRegister : null,
register : [
{
'title' : 'tab1',
}
,
{
'title' : 'tab2',
}
],
rawBranches : null,
syncedBranches : false
}
},
computed: {
Branches : function(){
if(this.rawBranches !== null){
let colorArr = generateColors(this.rawBranches.length);
console.log('colorArr',colorArr);
// Der Liste der Branchen die Farben zuordnen und als "Branches" bereitstellen
var l = [];
for(var i=0;i<this.rawBranches.length;i++){
var c = JSON.parse(JSON.stringify(this.rawBranches[i]));
c.color = colorArr[i];
l.push(c);
}
console.log('compute Branches',l);
this.syncedBranches = true;
return l;
}
console.log('compute Branches',null);
return null;
}
},
components: {
BranchesWidgetTabs,
BranchesWidgetList
},
mounted () {
axios
.post('/assets/get',{ entity : 'industryBranches' })
.then(response => ( this.rawBranches = response.data.data ))
},
created(){
//console.log('created',this.rawData);
},
methods : {
// das die Componenten eine ref mit der Bezeichnung "bwidget" hat, ist die Methode in der Seite mit app.$refs.bwidget.getBranches() erreichbar.
getBranches : function(){
return this.Branches;
}
}
}
</script>
Tabs-Compoent
<template>
<div class="BranchesWidgetTabs">
<div class="menu">
<div class="item" v-for="(item, index) in list">
<div>
<div class="i">
<div v-if="item.active">active</div>
</div>
<div class="title">
{{ item.title }}
</div>
</div>
</div>
<div class="spacer"></div>
</div>
<div class="tabbody" v-for="(item, index) in list">
<div class="content" v-if="item.active">
<slot :name="`tabbody_${index}`"></slot>
</div>
</div>
</div>
</template>
<style>
div.BranchesWidgetTabs {
background-color: yellow;
min-height: 40px;
}
div.BranchesWidgetTabs > div.menu {
display: flex;
flex-direction: row;
}
div.BranchesWidgetTabs > div.menu > .item {
flex: 0 0 auto;
min-width: 10px;
background-color: blue;
color: white;
}
div.BranchesWidgetTabs > div.menu > .item > div {
display: flex;
flex-direction: column;
padding: 0px 20px;
}
div.BranchesWidgetTabs > div.menu > .item:nth-child(odd) > div {
padding-right: 0;
}
div.BranchesWidgetTabs > div.menu > .item > div > div {
flex: 1;
}
div.BranchesWidgetTabs > div.menu > .item > div > div.i {
background-color: darkgrey;
min-height: 10px;
}
div.BranchesWidgetTabs > div.menu > .item > div > div.title {
background-color: pink;
padding: 10px 20px;
}
div.BranchesWidgetTabs > div.menu > .spacer {
flex: 1;
}
</style>
<script>
export default {
name: 'BranchesWidgetTabs',
props : {
items : Array,
activeItem : {
required : true,
validator: function(i){
return typeof i === 'number' || i === null;
}
},
},
data(){
return {
}
},
computed: {
list: function(){
var l = [];
var s = (this.activeItem !== null)? this.activeItem : 0;
for(var i=0;i<this.items.length;i++){
var c = JSON.parse(JSON.stringify(this.items[i]));
if(s === i){
c.active = true;
}else{
c.active = false;
}
l.push(c);
}
return l;
}
},
created(){
console.log('created',this.activeItem);
}
}
</script>
List-Component which revieves items from main component
<template>
<div class="BranchesWidgetList">
Liste
</div>
</template>
<style>
div.BranchesWidgetList {
}
</style>
<script>
export default {
name: 'BranchesWidgetList',
props : {
items : Array
},
data(){
return {
}
},
computed: {
},
created(){
console.log('created BranchesWidgetList',this.items);
}
}
</script>
EDIT:
I got it! Somehow i got misslead by the v-slot-directive. I thought i would have to pass the Branches-Array down to the child-component. But it seems that the context of template and main component is a shared one. So only thing to make sure of is that the async-call for that array is completed by using "Branches.length" in a v-if - no need for an extra variable like "syncedBranches".
Full main component with working code.
<template>
<div id="branchesWidget">
<branches-widget-tabs :items="register" :activeItem="activeRegister">
<template #tabbody_0 v-if="Branches.length">
<h1>Content Register 1</h1>
<branches-widget-list :items="Branches"></branches-widget-list>
</template>
<template #tabbody_1 v-if="Branches.length">
<h1>Content Register 2</h1>
<branches-widget-list :items="Branches"></branches-widget-list>
</template>
</branches-widget-tabs>
</div>
</template>
<style>
#branchesWidget {
min-width: 150px;
min-height: 150px;
background-color: #333;
}
#branchesWidget:hover {
background-color: #666;
}
</style>
<script>
import chroma from 'chroma-js';
//console.log('chroma',chroma);
import HUSL from 'hsluv';
//console.log('HUSL',HUSL);
import BranchesWidgetTabs from './BranchesWidgetTabs';
import BranchesWidgetList from './BranchesWidgetList';
const random = function(min, max){
return Math.floor(Math.random() * (max - min + 1)) + min;
};
var generateColors = function(n, startHex = '#ff6000', padding = 0, step = 5, randomSat = true, randomLight = true){
let colors = [];
const baseHex = HUSL.hexToHsluv(startHex);
const baseHue = baseHex[0];
//console.log('baseHue',baseHue);
var degrees = [];
for (let i = 0; i < n; i++) {
degrees.push( 360 / n * i);
}
//console.log('degrees',degrees);
const hues = degrees.map((offset) => {
return (baseHue + offset) % 360;
});
//console.log('hues',hues);
if(randomSat){
var baseSaturation = random(55, 85);
}else{
var baseSaturation = baseHex[1];
}
if(randomLight){
var baseLightness = random(35, 75);
}else{
var baseLightness = baseHex[2];
}
var subs = Math.min(n,Math.max(step,2));
for(let i = 0; i < subs; i++) {
colors.push( HUSL.hsluvToHex([
hues[i],
baseSaturation,
baseLightness
]));
}
console.log('colors',colors);
return chroma.scale(colors).padding(0).mode('lab').colors(n);
};
export default {
name: 'BranchesWidget',
props : [],
data() {
return {
activeRegister : null,
register : [
{
'title' : 'tab1',
}
,
{
'title' : 'tab2',
}
],
rawBranches : null
}
},
computed: {
Branches : function(){
var l = [];
if(this.rawBranches !== null){
let colorArr = generateColors(this.rawBranches.length);
//console.log('colorArr',colorArr);
// Der Liste der Branchen die Farben zuordnen und als "Branches" bereitstellen
for(var i=0;i<this.rawBranches.length;i++){
var c = JSON.parse(JSON.stringify(this.rawBranches[i]));
c.color = colorArr[i];
l.push(c);
}
}
console.log('compute Branches',l);
return l;
}
},
components: {
BranchesWidgetTabs,
BranchesWidgetList
},
mounted () {
axios
.post('/assets/get',{ entity : 'industryBranches' })
.then(response => ( this.rawBranches = response.data.data ))
},
created(){
//console.log('created',this.rawData);
},
methods : {
getBranches : function(){
return this.Branches;
}
}
}
</script>
I got it! Somehow i got misslead by the v-slot-directive. I thought i would have to pass the Branches-Array down to the child-component. But it seems that the context of template and main component is a shared one. So only thing to make sure of is that the async-call for that array is completed by using "Branches.length" in a v-if - no need for an extra variable like "syncedBranches".
To pass the variable "Branches" as a prop there is no need for passing it in as a scoped variable. That is only needed if you want to access those data between the template tags in the main component file.
Full main component with working code.
<template>
<div id="branchesWidget">
<branches-widget-tabs :items="register" :activeItem="activeRegister">
<template #tabbody_0 v-if="Branches.length">
<h1>Content Register 1</h1>
<branches-widget-list :items="Branches"></branches-widget-list>
</template>
<template #tabbody_1 v-if="Branches.length">
<h1>Content Register 2</h1>
<branches-widget-list :items="Branches"></branches-widget-list>
</template>
</branches-widget-tabs>
</div>
</template>
<style>
#branchesWidget {
min-width: 150px;
min-height: 150px;
background-color: #333;
}
#branchesWidget:hover {
background-color: #666;
}
</style>
<script>
import chroma from 'chroma-js';
//console.log('chroma',chroma);
import HUSL from 'hsluv';
//console.log('HUSL',HUSL);
import BranchesWidgetTabs from './BranchesWidgetTabs';
import BranchesWidgetList from './BranchesWidgetList';
const random = function(min, max){
return Math.floor(Math.random() * (max - min + 1)) + min;
};
var generateColors = function(n, startHex = '#ff6000', padding = 0, step = 5, randomSat = true, randomLight = true){
let colors = [];
const baseHex = HUSL.hexToHsluv(startHex);
const baseHue = baseHex[0];
//console.log('baseHue',baseHue);
var degrees = [];
for (let i = 0; i < n; i++) {
degrees.push( 360 / n * i);
}
//console.log('degrees',degrees);
const hues = degrees.map((offset) => {
return (baseHue + offset) % 360;
});
//console.log('hues',hues);
if(randomSat){
var baseSaturation = random(55, 85);
}else{
var baseSaturation = baseHex[1];
}
if(randomLight){
var baseLightness = random(35, 75);
}else{
var baseLightness = baseHex[2];
}
var subs = Math.min(n,Math.max(step,2));
for(let i = 0; i < subs; i++) {
colors.push( HUSL.hsluvToHex([
hues[i],
baseSaturation,
baseLightness
]));
}
console.log('colors',colors);
return chroma.scale(colors).padding(0).mode('lab').colors(n);
};
export default {
name: 'BranchesWidget',
props : [],
data() {
return {
activeRegister : null,
register : [
{
'title' : 'tab1',
}
,
{
'title' : 'tab2',
}
],
rawBranches : null
}
},
computed: {
Branches : function(){
var l = [];
if(this.rawBranches !== null){
let colorArr = generateColors(this.rawBranches.length);
//console.log('colorArr',colorArr);
// Der Liste der Branchen die Farben zuordnen und als "Branches" bereitstellen
for(var i=0;i<this.rawBranches.length;i++){
var c = JSON.parse(JSON.stringify(this.rawBranches[i]));
c.color = colorArr[i];
l.push(c);
}
}
console.log('compute Branches',l);
return l;
}
},
components: {
BranchesWidgetTabs,
BranchesWidgetList
},
mounted () {
axios
.post('/assets/get',{ entity : 'industryBranches' })
.then(response => ( this.rawBranches = response.data.data ))
},
created(){
//console.log('created',this.rawData);
},
methods : {
getBranches : function(){
return this.Branches;
}
}
}
</script>

Vue JS animation/transition effect on specific v-for item text on function call

I would like to create a transition/animation effect from a method where the text changes upon firing an event (not created) customTextAnim(key) which works independently for each of the v-for items.
When run, the text appears larger (22px), then shrinks to the normal 14px size after about a .3 second animation.
The text I would like to animate starts out 1t 14px, then jumps to 22px and shrinks back down to 14px. This is the text i would like to animate this.auctions[key].username*
I have literally no idea how to do this, i really need all the help i can get
<template>
<div>
<h1>Live Auctions {{ unixTime }}</h1>
<button #click="tempSetAuction()">set auctions</button>
<button #click="tempClearAuction()">CLEAR ALL</button>
<div style="clear:both;"></div>
<br /><br />
<ul class="row">
<li class="col-lg-4" v-for="(auction, key, index) in auctions" :key="auction.id">
<div><span>{{ auction.name }} ({{ auction.id }})</span><br /></div>
<div>END TIME: <span class="end-time" ref="endTime">{{ auction.endtime }}</span><br /></div>
<div>TIME LEFT: <span class="bid-seconds" ref="bidTimeSeconds">{{ auction.time_left }}</span><br /></div>
<div>BID TIME: <span class="bid-time" ref="bidTime"></span><br /></div>
<br />
<span ref="serverTime">{{ auction.date_now }}</span><br /><!---->
<span ref="totalBids">{{ auction.total_bids }}</span><br />
<span ref="user">{{ auction.username }}</span><br />
<div ref="newBid" class="button">
<button #click="bidOnThis(auction.id, key)">Bid on this item</button>
</div>
<button #click="countDown()">Countdown</button><br /><br />
<hr />
</li>
</ul>
</div>
</template>
<script>
export default {
// Probably remove this
props : {
items: []
},
data() {
return {
auctions: [],
newBid: '',
totalBids: '',
user: [],
bidTimeArray: [],
unixTime: '',
timeToUpdate: '0',
textEnded: 'Ended',
show: true
};
},
created() {
axios.get('/timenow').then(result => {
this.unixTime = result.data;
});
axios.get('/auctions').then(result => {
// Set up the remaining seconds for each auction on load
this.auctions = result.data;
for (let i = 0; i < this.auctions.length; i++){
this.bidTimeArray[i] = this.auctions[i].bid_time -1;
if(this.auctions[i].endtime <= this.unixTime){
this.auctions[i].time_left = this.textEnded;
this.auctions[i].bidTime = this.textEnded;
} else {
this.auctions[i].time_left = this.auctions[i].endtime - this.unixTime;
}
}
});
axios.get('/getuser').then(result => {
this.user = result.data;
});
},
methods: {
_padNumber: number => (number > 9 || number === 0) ? number : "0" + number,
_readableTimeFromSeconds: function(seconds) {
const hours = 3600 > seconds ? 0 : parseInt(seconds / 3600, 10);
return {
hours: this._padNumber(hours),
seconds: this._padNumber(seconds % 60),
minutes: this._padNumber(parseInt(seconds / 60, 10) % 60),
}
},
bidOnThis(id, key) {
if(this.$refs.bidTimeSeconds[key].innerHTML >= 0){
axios.post('/auctions', { id: id, key: key });
//alert(+this.bidTimeArray[key] + +this.unixTime);
this.auctions[key].endtime = +this.bidTimeArray[key] + +this.unixTime;
this.auctions[key].total_bids = parseInt(this.auctions[key].total_bids) + 1;
//this.$refs.totalBids[key].innerHTML = parseInt(this.$refs.totalBids[key].innerHTML) + 1 ;
this.auctions[key].username = this.user.username ;
}
},
countDown(){
this.unixTime = this.unixTime+1;
this.timeToUpdate = this.timeToUpdate+1;
if(this.timeToUpdate >= 60){
this.timeToUpdate = 0;
axios.get('/timenow').then(result => {
this.unixTime = result.data;
//console.log('Just updated the time');
});
}
if(this.auctions.length >0){
for (let i = 0; i < this.auctions.length; i++){
if(typeof this.auctions[i].time_left == 'number' && this.auctions[i].endtime >= this.unixTime){
//if(this.auctions[i].endtime <= this.unixTime){
this.auctions[i].time_left = this.auctions[i].endtime - this.unixTime;
var newTime = parseInt(this.$refs.bidTimeSeconds[i].innerHTML);
this.$refs.bidTime[i].innerHTML = this._readableTimeFromSeconds(newTime).minutes+ ':'+this._readableTimeFromSeconds(newTime).seconds;
} else {
this.$refs.bidTime[i].innerHTML = this.textEnded;
this.$refs.newBid[i].innerHTML = '';
}
}
}
},
tempSetAuction(){
axios.get('/auctions/set').then(result => {
});
},
tempClearAuction(){
axios.get('/auctions/clear').then(result => {
});
}
},
mounted: function () {
window.setInterval(() => {
this.countDown();
},1000);
}
};
Not the complete solution. It's just the idea that I'm providing here. You can add the styles of transition. I hope that can guide you
Template:
<div id="list-demo">
<button v-on:click="add">Add</button>
<transition-group name="list" tag="p">
<span v-for="item in items" v-bind:key="item" class="list-item">{{ item }}</span>
</transition-group>
</div>
ViewModel
data: {
items: [1, 2, 3, 4, 5, 6, 7, 8, 9],
nextNum: 10
},
methods: {
add: function() {
this.items.push(this.nextNum++);
}
}
Style
.list-item {
display: inline-block;
margin-right: 10px;
}
.list-enter-active, .list-leave-active {
transition: all 1s;
}
.list-enter, .list-leave-to
/* .list-leave-active below version 2.1.8 */
{
opacity: 0;
transform: translateY(30px); //Enter your transition transforms here
}

How can I call a method after the loops(more than 1 loop) complete on vue js?

My vue component like this :
<template>
<div class="row">
<div class="col-md-3" v-for="item1 in items1">
...
</div>
<div class="col-md-3" v-for="item2 in items2">
...
</div>
<div class="col-md-3" v-for="item3 in items3">
...
</div>
</div>
</template>
<script>
export default {
...
computed: {
items1() {
const n = ... // this is object
return n
},
items2() {
const n = ... // this is object
return n
},
items3() {
const n = ... // this is object
return n
}
},
...
}
</script>
If the three loop complete, I want to call a method
So the method is executed when the three loop completes
How can I do it?
As promised, here is the example.
var counter = 0
const vm = new Vue({
el: '#app',
computed: {
items1() {
return {item1: 'value1', item2: 'value2'}
},
items2() {
return {item1: 'value3', item2: 'value4'}
},
items3() {
return {item1: 'value5', item2: 'value6'}
}
},
methods: {
callback() {
counter++
console.log('v-for loop finished')
var numberOfLoops = 3
if (counter >= numberOfLoops) {
console.log('All loops have finished executing.')
counter = 0
}
}
},
directives: {
forCallback(el, binding, vnode) {
let element = binding.value
var key = element.key
var len = 0
if (Array.isArray(element.array)) {
len = element.array.length
}
else if (typeof element.array === 'object') {
var keys = Object.keys(element.array)
key = keys.indexOf(key)
len = keys.length
}
if (key == len - 1) {
if (typeof element.callback === 'function') {
(element.callback.bind(vnode.context))()
}
}
}
},
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.3.4/vue.min.js"></script>
<div id="app">
<div class="row">
<div class="col-md-3" v-for="(item, key) in items1" v-for-callback="{key: key, array: items1, callback: callback}">
...
</div>
<div class="col-md-3" v-for="(item, key) in items2" v-for-callback="{key: key, array: items2, callback: callback}">
...
</div>
<div class="col-md-3" v-for="(item, key) in items3" v-for-callback="{key: key, array: items3, callback: callback}">
...
</div>
</div>
</div>