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>
Related
I'm working on Vue 3 app where I would like to use multiple instances of one component. Each component should have its instance of D3 for displaying various SVG images. In my case D3 works as intended only on first instance of Vue component.
Dots are random generated by D3. I can see when inspecting elements that none of dots has been appended in second instance of component. Screenshot of the problem may be found here.
My component with D3 looks like this:
<template>
<div class="fill-100">
<svg ref="svgRef" width="400" height=667>
<g></g>
</svg>
</div>
</template>
<script>
import {ref, onMounted} from "#vue/runtime-core";
import {select, zoom} from "d3";
export default {
name: "SldSvgD3",
props: ["id"],
setup() {
const svgRef = ref(null);
onMounted(() =>{
const svg = select(svgRef.value);
svg.append("svg")
let data = [], width = 400, height = 667, numPoints = 100;
let zoom3 = zoom()
.on('zoom', handleZoom);
function handleZoom(e) {
select('svg g')
.attr('transform', e.transform);
}
function initZoom() {
select('svg')
.call(zoom3);
}
function updateData() {
data = [];
for(let i=0; i<numPoints; i++) {
data.push({
id: i,
x: Math.random() * width,
y: Math.random() * height
});
}
}
function update() {
select('svg g')
.selectAll('circle')
.data(data)
.join('circle')
.attr('cx', function(d) { return d.x; })
.attr('cy', function(d) { return d.y; })
.attr('r', 3);
}
initZoom();
updateData();
update();
});
return {svgRef}
}
}
</script>
<style lang="scss">
.fill-100{
width: 100%;
height: 100%;
}
</style>
Implementation of D3 zoom and pan taken from this site
What I didn't know is that scope of d3.select() call is global for the whole app. Solution in my case was just creating unique id for root div and selecting this div before any manipulation.
This question was very helpful to me.
Complete code:
<template>
<div class="fill-100" :id="'sld_div'+this.id">
</div>
</template>
<script>
import {ref, onMounted} from "#vue/runtime-core";
import * as d3 from "d3";
export default {
name: "SldSvgD3",
props: ["id"],
setup(props) {
const svgRef = ref(null);
const svg_width = 400;
const svg_height = 667;
onMounted(() =>{
const svg = d3
.select("#sld_div"+props.id)
svg.append("svg")
.attr("id","sld_root"+props.id)
.attr("width", svg_width)
.attr("height", svg_height)
.append("g")
.attr("id","sld_root_g"+props.id)
let data = [], width = 600, height = 400, numPoints = 100;
let zoom = d3.zoom()
.on('zoom', handleZoom);
function handleZoom(e) {
d3.select("#sld_div"+props.id)
.select('svg g')
.attr('transform', e.transform);
}
function initZoom() {
d3.select("#sld_div"+props.id)
.select('svg')
.call(zoom);
}
function updateData() {
data = [];
for(let i=0; i<numPoints; i++) {
data.push({
id: i,
x: Math.random() * width,
y: Math.random() * height
});
}
}
function update() {
d3.select("#sld_div"+props.id)
.select('svg g')
.selectAll('circle')
.data(data)
.join('circle')
.attr('cx', function(d) { return d.x; })
.attr('cy', function(d) { return d.y; })
.attr('r', 3);
}
initZoom();
updateData();
update();
});
return {svgRef}
}
}
</script>
<style lang="scss">
.fill-100{
width: 100%;
height: 100%;
}
</style>
I have 4 range inputs. Each of them has min number 0, max number 10.
In Total they can't sum to more than 22.
One way to approach this would be to disable all inputs once they hit 22 and add a reset button. I would find it to be more user-friendly to allow the ranges to be decremented after the max is reached instead of a whole reset.
I tried disabling if it's less or equal 0, but the scroller was still under control.
Check the comments on the
sandbox here if it easier , but the parent class is as below:
<template>
<div class="vote">
<div class="vote__title">Left: <span>{{ hmLeft }}</span> votes</div>
<div class="vote__body">
<div v-for="user in activeInnerPoll" :key="user._id">
<userVoteFor :hmLeft="hmLeft" #cntCount="cntCount" :id="user._id"/>
</div>
</div>
</div>
</template>
<script>
import { mapGetters } from "vuex"
import userVoteFor from "#/components/userVoteFor";
export default {
name: "Vote.vue",
components: {
userVoteFor
},
data(){
return {
votes: 22,
objRes: {} // that's where we write what id of a user and how many counts
}
},
computed: {
...mapGetters("polls", ["activeInnerPoll"]), // array of objects {_id : "some_id", cnt: 0}
hmLeft(){ // how much left, counter which tells how many votes left
let sum = 0;
for(let key in this.objRes){
sum += this.objRes[key];
}
return this.votes - sum;
}
},
methods: {
cntCount(id, cnt){ // emit for children, gets id and cnt of input-range and sets to result obj
this.objRes[id] = parseInt(cnt);
}
}
}
</script>
<style scoped lang="scss">
#import "#/assets/vars.scss";
#import "#/assets/base.scss";
.vote{
&__title{
#include center;
margin-top: 15px;
span{
font-size: 20px;
margin: 0 5px;
color: $pink;
}
}
}
</style>
Child class here:
<template>
<div class="vote__component">
<label class="vote__component__label" :for="id">{{ playerNameById( id )}}</label>
<input #input="check($event)" // thought maybe something to do with event ?
:disabled="disable"
class="vote__component__input"
:id="id"
type="range"
min="0"
max="10"
step="1"
v-model="cnt">
<div class="vote__component__res">{{ cnt }}</div>
</div>
</template>
<script>
import { mapGetters } from "vuex";
export default {
name: "userVoteFor.vue",
props: {
id: {
type: String,
required: true
},
hmLeft: {
type: Number,
required: true
}
},
emits: ["cntCount"],
data() {
return {
cnt: 0,
disable: false,
lastVal: 0
}
},
computed: {
...mapGetters("user", ["playerNameById"]) // gets map object which stores names for user by id
},
methods: {
check(e){
console.log(e);
if(this.hmLeft <= 0) { //HERE IS APART WHERE I THINK SHOULD BE WRITTEN LOGIC if hmLeft <= 0 then ... , else write cnt in resObj and computed var will calc how many votes left
this.lastVal = this.cnt;
this.cnt = this.lastVal;
}
else this.$emit("cntCount", this.id, this.cnt);
}
}
}
</script>
<style scoped lang="scss">
.vote__component{
width: 80%;
margin: 10px auto;
position: relative;
display: flex;
justify-content: right;
padding: 10px 0;
font-size: 15px;
&__input{
margin-left: auto;
width: 60%;
margin-right: 20px;
}
&__res{
position: absolute;
top: 20%;
right: 0;
}
&__label{
}
}
</style>
The way I'd implement this is by using a watch and the get and set method of computed.
The array of values would be updated via a computed. This makes it easy to hook into a v-model and allows us to maintain reactivity with the original array.
The watch is then used to compute the total that is available. Then, for bonus points, we can use the total to adjust the width of the input so the step size remains consistent.
Even though this is using the composition Api, you can implement that using data, watch and computed the classical way
const makeRange = (max, vals, index) => {
const defaultMax = 10;
const num = Vue.computed({
get: () => vals[index],
set: value => vals[index] = Number(value)
});
const total = Vue.computed(() => vals.reduce((a, b) => a + b, 0), vals);
const style = Vue.computed(() => {
return `width: ${(numMax.value * 12 + 20)}px`
})
const numMax = Vue.computed(() => {
return Math.min(defaultMax, (num.value + max - total.value))
}, total);
return {num, numMax, style};
};
const app = Vue.createApp({
setup() {
const vals = Vue.reactive([5, 5, 5])
const max = 22;
const ranges = vals.map((v,i)=>makeRange(max, vals, i));
// helpers for visualising
const total = Vue.computed(() => vals.reduce((a, b) => a + b, 0), vals);
const totalLeft = Vue.computed(() => max - total.value , total.value);
return {ranges, vals, totalLeft, total, max};
}
}).mount('#app');
<script src="https://unpkg.com/vue#3.0.2/dist/vue.global.prod.js"></script>
<div id="app">
<li v-for="range in ranges">
<input
:style="range.style.value"
type="range" min="0"
:max="range.numMax.value"
v-model="range.num.value"
>
value: {{range.num.value}}
max: {{range.numMax.value}}
</li>
<li>{{ vals.join(' + ') }} = {{ total }}</li>
<li>max is {{ max }} , minus total {{total }} is {{ totalLeft }}</li>
</div>
in the latest version of ant-design-vue, we're no longer able to upload bigger image.
<template>
<a-upload
name="avatar"
list-type="picture-card"
class="avatar-uploader"
:show-upload-list="false"
action="https://www.mocky.io/v2/5cc8019d300000980a055e76"
:before-upload="beforeUpload"
#change="handleChange"
>
<img v-if="imageUrl" :src="imageUrl" alt="avatar" />
<div v-else>
<a-icon :type="loading ? 'loading' : 'plus'" />
<div class="ant-upload-text">
Upload
</div>
</div>
</a-upload>
</template>
<script>
function getBase64(img, callback) {
const reader = new FileReader();
reader.addEventListener('load', () => callback(reader.result));
reader.readAsDataURL(img);
}
export default {
data() {
return {
loading: false,
imageUrl: '',
};
},
methods: {
handleChange(info) {
if (info.file.status === 'uploading') {
this.loading = true;
return;
}
if (info.file.status === 'done') {
// Get this url from response in real world.
getBase64(info.file.originFileObj, imageUrl => {
this.imageUrl = imageUrl;
this.loading = false;
});
}
},
beforeUpload(file) {
const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png';
if (!isJpgOrPng) {
this.$message.error('You can only upload JPG file!');
}
const isLt2M = file.size / 1024 / 1024 < 2;
if (!isLt2M) {
this.$message.error('Image must smaller than 2MB!');
}
return isJpgOrPng && isLt2M;
},
},
};
</script>
<style>
.avatar-uploader > .ant-upload {
width: 128px;
height: 128px;
}
.ant-upload-select-picture-card i {
font-size: 32px;
color: #999;
}
.ant-upload-select-picture-card .ant-upload-text {
margin-top: 8px;
color: #666;
}
</style>
it'll return
error POST https://www.mocky.io/v2/5cc8019d300000980a055e76 net::ERR_CONNECTION_RESET will produced after upload file. only file around 10-50 mb is allowed.
i've try by leave action as empty but it'll use base domain with path null as post api.
is there any other way to to leave action as empty so that it can run straight to #change instead stuck in action other than create own api?
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
Alright so I am trying to send image data using JSON but no matter what I do I always end up in sending an empty object... I've tried to console log results but no matter what it just sends empty object
CODE:
<body>
<div id="app">
<div v-if="!image">
<h2>Select an image</h2>
<input type="file" #change="onFileChange" multiple>
</div>
<div v-else>
<div v-for="img in image" class="img_overlay">
<img :src="img" class="img_set"/><br/>
<button #click="removeImage(img)">Remove image</button>
</div>
</div>
</div>
<style>
.img_overlay {
width: 25%;
height: 250px;
float: left;
text-align: center;
}
img {
width: 250px;
height: 200px;
}
</style>
<script type="text/javascript">
new Vue({
el: "#app",
data: {
image: "",
file_data: []
},
methods: {
onFileChange(e) {
var files = e.target.files || e.dataTransfer.files;
if (!files.length)
return;
else if(files.length == 1)
this.createImage(files)
else if(files.length >= 2)
this.createImage(files)
this.file_data = e.target.files;
this.uploadImage(e.target.files);
},
createImage(file) {
var tmp = [];
for(let i = 0; i < file.length; i++) {
var image = new Image();
var reader = new FileReader();
var vm = this;
reader.onload = (e) => {
tmp.push(e.target.result);
};
reader.readAsDataURL(file[i]);
}
vm.image = tmp;
},
removeImage: function (img) {
for(let i = 0; i < this.image.length; i++) {
if(this.image[i] == img) {
this.image.splice(i, 1);
}
}
},
uploadImage: function(x_file) {
const config = {
headers: { 'content-type': 'multipart/form-data' }
}
axios.post('/theme/post_new_image', x_file, config).then(function (response) {
console.log(response);
}).catch(e => { console.log(e); });
}
}
});
</script>
</body>
The result I usualy get is empty object with 5 keys. I've tried to stringify the data and such but I've couldn't find the correct solution for it
You are passing an array of files to your uploadImage function. Try iterating over the array to upload each file:
for (var i = 0, f; f = e.target.files[i]; i++) {
uploadImage(f);
}