I have the following code structure (simplified):
<template v-for="tile of layer.data">
<VueDragResize :x="calculatePositionX(tile)">
<h2 :style="{'font-size':calculateFontSize(tile) + 'px'}">Test</h2>
</VueDragResize>
</template>
The :x="calculatePositionX(tile) and :style="{'font-size':calculateFontSize(tile) + 'px'}" values are dynamic. I want to change these values on window resize.
To achieve this I do this: (too keep it simple I want to show you only the calculatePositionX example).
mounted() {
window.addEventListener('resize', this.onResize);
},
beforeDestroy() {
window.removeEventListener('resize', this.onResize)
},
methods: {
onResize(event) {
this.calculatePositionX(null);
},
calculatePositionX(tile) {
if (!tile) {
tile = this.cachedTile;
}
this.cachedTile = tile;
let x = tile.boxPositionX;
let parentWidth = tile.boxPositionParentWidth;
let currentParentWidth = this.$refs.builderLayer.clientWidth;
if (parentWidth == currentParentWidth) {
return x;
}
console.log(currentParentWidth * x / parentWidth); // Returns different result as the initial, but the state is not updating
return currentParentWidth * x / parentWidth;
},
}
data() {
return {
cachedTile: Object,
}
},
The console.log(currentParentWidth * x / parentWidth); returns a different result as the initial, but the state is not updating on the UI.
What can be the issue here? I also tried to save the new value as variable into the store and return the store result. Without success.
What do you think?
You defined calculatePositionX on the Vue instance itself.
Move it inside methods and it will work.
Side note: you might want to replace cachedTile: Object with cachedTile: {} in data.
In the OP code, you call calculatePositionX on resize, but ignore the return. The template won't know to update on the resize, but it will if dependent data changes.
Rather than returning the result of the resize calculation, have your calculatePositionX method change the state, then, let vue reactive getters do their job in the template. Refer to x in data (which is set by the slightly modified calculatePositionX)
<template v-for="tile of layer.data">
<VueDragResize :x="x">
<h2 :style="style(tile)">Test</h2>
</VueDragResize>
</template>
mounted() {
window.addEventListener('resize', this.onResize);
this.onResize();
},
beforeDestroy() {
window.removeEventListener('resize', this.onResize)
},
methods: {
onResize(event) {
this.calculatePositionX(null);
},
calculatePositionX(tile) {
if (!tile) {
tile = this.cachedTile;
}
this.cachedTile = tile;
let x = tile.boxPositionX;
let parentWidth = tile.boxPositionParentWidth;
let currentParentWidth = this.$refs.builderLayer.clientWidth;
if (parentWidth == currentParentWidth) {
this.x = x;
return;
}
console.log(currentParentWidth * x / parentWidth);
this.x = currentParentWidth * x / parentWidth;
},
style(tile) {
return {'font-size':calculateFontSize(tile) + 'px'}
}
}
data() {
return {
x: 0, // will get initialized in mounted
cachedTile: Object,
}
},
Related
I want this select multiple to pre-select one option, and not be able to deselect all options.
Whenever the last selected option is deselected it should be reselected. In other words when the user tries to deselect the last selected option it should visually not be deselected.
<template>
<b-select
if="Object.keys(doc).length !== 0 /* wait until firebase has loaded */"
:options="computedOptions"
v-model="model"
multiple
#input="onChange"
/>
</template>
<script>
//import Vue from 'vue'
import { fb } from "../fbconf";
export default {
name: "MyMultiSelect",
props: {
doc: Object, // firestore document
},
data() {
return {
options: []
};
},
firestore() {
var options = fb.db.collection("options");
return {
options: options
};
},
computed: {
computedOptions: function() {
return this.options.map(function(option) {
return {
text: option.name,
value: option.id
};
});
},
// to make sure mySelectedOptions is an array, before this.doc is loaded
// I use the following custom model
// because not using 'get' below causes a warning:
// [Vue warn]: <select multiple v-model="localValue"> expects an Array value for its binding, but got Undefined
model: {
get: function() {
if (!this.doc.hasOwnProperty('mySelectedOptions')) return []; // empty array before this.doc is loaded
else return this.doc['mySelectedOptions'];
},
set: function(newValue) {
// here I can prevent the empty array from being stored
// but visually the user can deselect all options, which is bad UX
//if (Array.isArray(newValue) && newValue.length > 0) this.doc['mySelectedOptions'] = newValue;
}
},
},
methods: {
onChange: function(newValue){
// I can manually store the array as I want here
// but I cannot in any way prevent the user from deselecting all options
if (Array.isArray(newValue) && newValue.length > 0) this.doc['mySelectedOptions'] = newValue;
else {
// none of these reselects the last selected option
var oldValue = this.doc['mySelectedOptions'];
this.doc['mySelectedOptions'] = this.doc['mySelectedOptions'];
//this.$forceUpdate();
//this.$emit("change", newValue);
//Vue.set(this.doc, 'mySelectedOptions', this.doc['mySelectedOptions']);
}
}
}
};
</script>
You could add watcher and when length becomes 0 just add previous value.
watch: {
model(val, oldVal) {
if(val.length == 0 && oldVal.length > 0) {
// take only one item in case there's clear button or etc.
this.model = [oldval[0]];
}
}
}
I am implementing pagination for a huge list of cards, I display 10 cards at once and wish to show the 10 next (or 10 previous) by clicking on two buttons.
Here's how I do it:
export default {
...
data() {
return {
pois: [], // My list of elements
pageNumber: 0, // Current page number
};
},
props: {
size: {
type: Number,
required: false,
default: 10, // 10 cards per page
},
},
computed: {
pageCount() {
// Counts the number of pages total
const l = this.pois.length;
const s = this.size;
return Math.floor(l / s);
},
paginatedData() {
// Returns the right cards based on the current page
const start = this.pageNumber * this.size;
const end = start + this.size;
return this.pois.slice(start, end);
},
},
methods: {
nextPage() {
this.pageNumber += 1;
},
prevPage() {
this.pageNumber -= 1;
},
}
...
};
And my template:
<div v-for="poi in paginatedData" :key="poi.id">
<card :poi="poi"/>
</div>
Everything should work (and a page change does output the correct cards in the console) but my list is not updated even though the computed method is called on each click.
What is causing this issue? I've read it could be linked to a :key value missing, but it's there, and no data is being updated directly and manually in the array, only sliced out.
First, try this change, just for sure, and let me know in comment it works or not.
export default {
...
computed: {
paginatedData() {
...
const end = start + this.size - 1;
...
},
...
};
And yes: instead of id, try to use index:
<div v-for="(poi, idx) in paginatedData" :key="idx">
<card :poi="poi"/>
</div>
I am new to Vue.js and am trying to create components that will simplify form creation, based on a library I have been using for a while now (PHP).
I have created a component that renders a label + textbox, styled via Bootstrap.
In order to avoid having to pass all the parameters every time, I want to be able to define defaults from within the parent, so that they will stay in effect until changed.
The component looks like this (MyTextBox.vue)
<template>
<div v-bind:class="myDivWidth">
<label v-bind:class="`control-label ${myLabelWidth}`">{{label}}</label>
<div v-bind:class="myControlWidth">
<input class="form-control col-md-12" v-bind:value="value">
</div>
</div>
</template>
<script>
export default {
data: function() {
return {
// trying to use this as 'class' variable but most likely wrong
myDefaultLabelWidth: 4
}
},
props: {
label: String,
labelWidth: String,
controlWidth: String,
divWidth: String,
value: {required: false},
defaultLabelWidth: {type: String}
},
computed: {
myLabelWidth: function () {
let lw;
//debugger;
do {
if (typeof this.defaultLabelWidth !== 'undefined') {
lw = this.defaultLabelWidth;
// ****** Note the call to the parent function
this.$parent.setDefault('defaultLabelWidth', lw);
break;
}
if (typeof this.labelWidth !== 'undefined') {
lw = this.labelWidth;
break;
}
if (typeof this.lw !== 'undefined') {
lw = this.lw;
break;
}
// ****** Note the call to the parent function
lw = this.$parent.getDefault('defaultLabelWidth');
} while (false);
return `col-md-${lw}`;
},
// snip....
}
}
</script>
and it is used like this (I am only showing attributes relating to label, for brevity)
(StoryEditor.vue)
<my-textbox label="LableText1" default-label-width=4></my-textbox>
<my-textbox label="LableText2"></my-textbox>
<my-textbox label="LableText3" label-width=5></my-textbox>
<my-textbox label="LableText4"></my-textbox>
<my-textbox label="LableText5" default-label-width=6></my-textbox>
<my-textbox label="LableText6"></my-textbox>
<my-textbox label="LableText7"></my-textbox>
What this is meant to do, is set the label with to 4, for the first 2 instances
then force a width of 5 for the next instance
then go back to 4
then set a new default of 6 for the remaining 3 components.
This is useful in cases where a lot of components (of the same type) are used, most of which are of the same width.
This mechanism will also used for all other applicable attributes.
Please note that what is important here is that the default is set in the parent and can change between instances of the component.
(I am aware that I can have a default value in the template itself but, as I understand it, that would apply to all instances of that component)
Any help would be greatly appreciated!
[Edit]
I have found one solution:
I added these methods to the parent (StoryEditor.vue).
They are called by the component code, shown above with '******' in the comments
<script>
export default {
created: function () {
// make sure the variable exists
if (typeof window.defaultOptions === 'undefined') {
window.defaultOptions = {
defaultLabelWidth: 3,
defaultControlWidth: 7
};
}
},
data() {
return {
story: {
}
}
},
methods: {
getDefaultOptions: () => {
console.log('getDefaultOptions', window.defaultOptions);
},
setDefaultOptions: (opts) => {
window.defaultOptions = opts;
},
getDefault: (option) => {
console.log(' getDefault', window.defaultOptions);
return window.defaultOptions[option];
},
setDefault: (option, v) => {
window.defaultOptions[option] = v;
console.log('setDefault', window.defaultOptions);
}
}
}
</script>
This uses this.$parent. to call methods in the parent.
The parent then uses a window variable to store/retrieve the relevant parameters.
A window variable is used because I want to have a single variable that will be used by all instances of the component.
My mixin:
export default {
data() {
return {
charges: [],
catCharges: [],
offenses: ['Class I Offenses', 'Class II Offenses', 'Class III Offenses', 'Class IV Offense']
}
},
methods: {
getCharges() {
axios.get('admin/charges').then((response) => {
this.charges = response.data;
for(let offense = 1; offense <= this.offenses.length; offense++) {
this.catCharges[offense - 1] = this.chargesAtOffense(offense);
}
});
},
chargesAtOffense(offense) {
return _.filter(this.charges, { offense_level: offense });
}
},
created() {
this.getCharges();
}
};
Fetching data works, the array 'charges' gets populated with the following:
After populating the array, I start looping over the offenses array and filter all 'charges' from the main array into the 'catCharges' array, so all offenses are split into 4 separated arrays in that array.
Chrome's developer tools shows the array just fine and the charges are properly filtered.
This is my component:
<template>
<div>
<h1>Total charges: {{charges.length}}</h1>
<h1>Total offense categories: {{catCharges.length}}</h1>
<div v-for="(charges, offenseIdx) in catCharges">
{{charges}}
</div>
</div>
</template>
<script>
import chargesMixin from '../mixins/chargesMixin';
export default {
mixins: [chargesMixin],
data() {
return {
}
},
methods: {
},
computed: {
},
mounted() {
console.log('Disciplinary Segregation mounted.')
}
}
</script>
It uses the mixin provided above, and IT works and shows the catCharges array properly, HOWEVER when I remove the following line from the template:
<h1>Total charges: {{charges.length}}</h1>
The catCharges array is displayed as EMPTY, why do I need to use the charges array too along with the filtered array? This is driving me crazy.
I also tried the following method in the mixin which also causes the same issue:
chargesAtOffense(offense) {
var newCharges = [];
for(var i = 0; i < this.charges.length; i++) {
if(this.charges[i].offense_level != offense) continue;
const cloned = _.clone(this.charges[i]);
newCharges.push(cloned);
}
return newCharges;
}
I think your use case is linked to the reactivity system of VueJS.
https://v2.vuejs.org/v2/guide/reactivity.html
If you delete the line
<h1>Total charges: {{charges.length}}</h1>
you tell to VueJS to refresh your template only on catCharges get / set.
catCharges is an array, and so it's not as 'reactive' as a simple variable.
If you read precisely https://v2.vuejs.org/v2/guide/list.html#Caveats, prefer use a push on your catCharges to explain correctly to Vue that your array has changed.
I'll try this code :
getCharges() {
axios.get('admin/charges').then((response) => {
this.charges = response.data;
for(let offense = 1; offense <= this.offenses.length; offense++) {
this.catCharges.push(this.chargesAtOffense(offense));
}
});
},
Hope this will solve your problem.
I have a component to display names. I need to calculate number of letters for each name.
I added nameLength as computed property but vuejs doesn't determine this property in loop.
var listing = Vue.extend({
template: '#users-template',
data: function () {
return {
query: '',
list: [],
user: '',
}
},
computed: {
computedList: function () {
var vm = this;
return this.list.filter(function (item) {
return item.toLowerCase().indexOf(vm.query.toLowerCase()) !== -1
})
},
nameLength: function () {
return this.length; //calculate length of current item
}
},
created: function () {
this.loadItems();
},
methods: {
loadItems: function () {
this.list = ['mike','arnold','tony']
},
}
});
http://jsfiddle.net/apokjqxx/22/
So result expected
mike-4
arnold-6
tony-4
it seems there is some misunderstanding about computed property.
I have created fork from you fiddle, it will work as you needed.
http://jsfiddle.net/6vhjq11v/5/
nameLength: function () {
return this.length; //calculate length of current item
}
in comment it shows that "calculate length of current item"
but js cant get the concept of current item
this.length
this will execute length on Vue component it self not on that value.
computed property work on other property of instance and return value.
but here you are not specifying anything to it and used this so it wont able to use any property.
if you need any more info please comment.