I'm not sure why my props aren't updating when I update the data in my parent component. I've tried it out in a fiddle and it works https://jsfiddle.net/f3w69rr6/1/ but it doesn't work in my app.
parent:
methods: {
addToHand(index) {
let card = this.gameState.playerDeck.splice(index, 1)
if (this.gameState.playerHand.length < 12)
{
// put card into hand
this.$set(this.gameState, 'playerHand', [...this.gameState.playerHand, card])
// this.gameState.playerHand.push(card)
}
// otherwise discard card
},
retrieveDeck() {
let array = []
for (let i = 0; i < 20; i++)
{
array.push(this.src + "?text=card"+i)
}
this.$set(this.gameState, 'playerDeck', array)
},
},
mounted () {
this.retrieveDeck()
for (let i = 0; i < 5; i++)
{
this.addToHand(1)
}
},
putting the data into child via:
<PlayerCards :gameState="gameState" :hand="gameState.playerHand" />
child:
export default {
name: 'PlayerCards',
props: ["gameState", "hand"],
data() {
return {
}
},
computed: {
rows() {
let cards = this.gameState.playerHand
let max = 6;
if (cards.length <= max)
return [cards]
var mid = Math.ceil(cards.length / 2);
let return_value = [cards.slice(0, mid), cards.slice(mid)]
return return_value
}
}
}
but the row content is empty.
Update (Updated fiddle):
The problem is with the compute
https://jsfiddle.net/f3w69rr6/1/
If you're not using string templates then you need to use the kebab-case equivalent to your camelCase prop:
<PlayerCards :game-state="gameState" :hand="gameState.playerHand" />
The reason it works in your fiddle is because you are using a string template (see: https://v2.vuejs.org/v2/guide/components.html#camelCase-vs-kebab-case)
Look at your demo in jsfiddle:
rows() {
let cards = this.gameState.playerHand
let max = 6;
if (cards.length <= max)
return [cards]
var mid = Math.ceil(cards.length / 2);
let return_value = [cards.slice(0, mid), cards.slice(mid)]
return return_value
}
Actually, vue has notified the computed function successfully when gameState.playerHand has updated. But you wrapped the computed property: rows into an array, like: [cards] and [cards.slice(0, mid), cards.slice(mid)]. And obviously, the rows.length will be 1 or 2.
I believe this was due to a misuse of the computed property. Switching it over to data and using this.$set updates as expected. If I were to use computed then setters would be needed. Computed also seems to be more suited for combining/updating data properties rather than being a property in and of itself.
Related
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,
}
},
I'm quite new to vue and right now I'm trying to figure out how to make changes to a computed array and make an element react to this change. When I click the div element (code section 4), I want the div's background color to change. Below is my failed code.
Code section 1: This is my computed array.
computed: {
arrayMake() {
let used = [];
for (let i = 0; i < 5; i++) {
used.push({index: i, check: true});
}
return used;
Code section 2: This is where I send it as a prop to another component.
<test-block v-for="(obj, index) in arrayMake" v-bind:obj="obj" v-on:act="act(obj)"></card-block>
Code section 3: This is a method in the same component as code section 1 and 2.
methods: {
act(obj){
obj.check = true;
}
Code section 4: Another component that uses the three sections above.
props: ["obj"],
template: /*html*/`
<div v-on:click="$emit('act')">
<div v-bind:style="{ backgroundColor: obj.check? 'red': 'blue' }">
</div>
</div>
Easiest way to achieve this, store the object into another data prop in the child component.
child component
data() => {
newObjectContainer: null
},
onMounted(){
this.newObjectContainer = this.obj
},
methods: {
act(){
// you don't need to take any param. because you are not using it.
newObjectContainer.check = !newObjectContainer.check
}
}
watch: {
obj(val){
// updated if there is any changes
newObjectContainer = val
}
}
And if you really want to update the parent component's computed data. then don't use the computed, use the reactive data prop.
child component:
this time you don't need watcher in the child. you directly emit the object from the method
methods: {
act(){
newObjectContainer.check = !newObjectContainer.check
this.emits("update:modelValue", nextObjectContainer)
}
}
parent component:
data() => {
yourDynamicData: [],
},
onMounted(){
this.yourDynamicData = setAnArray()
},
methods(){
setAnArray(){
let used = [];
for (let i = 0; i < 5; i++) {
used.push({index: i, check: true});
}
return used;
}
}
okay above you created a reactive data property. Now you need the update it if there is a change in the child component,
in the parent first you need object prop so, you can update that.
<test-block v-for="(obj, index) in arrayMake" v-model="updatedObject" :obj="obj"></card-block>
data() => {
yourDynamicData: [],
updatedObject: {}
},
watch:{
updatedObject(val){
const idx = val.index
yourDynamicData[idx] = val
}
}
I'm learning Vue and have been struggling to get the data from a computed property. I am retrieving comments from the store and them processing through a function called chunkify() however I'm getting the following error.
Despite the comments being computed correctly.
What am I doing wrong here? Any help would be greatly appreciated.
Home.vue
export default {
name: 'Home',
computed: {
comments() {
return this.$store.state.comments
},
},
methods: {
init() {
const comments = this.chunkify(this.comments, 3);
comments[0] = this.chunkify(comments[0], 3);
comments[1] = this.chunkify(comments[1], 3);
comments[2] = this.chunkify(comments[2], 3);
console.log(comments)
},
chunkify(a, n) {
if (n < 2)
return [a];
const len = a.length;
const out = [];
let i = 0;
let size;
if (len % n === 0) {
size = Math.floor(len / n);
while (i < len) {
out.push(a.slice(i, i += size));
}
} else {
while (i < len) {
size = Math.ceil((len - i) / n--);
out.push(a.slice(i, i += size));
}
}
return out;
},
},
mounted() {
this.init()
}
}
Like I wrote in the comments, the OPs problem is that he's accessing a store property that is not available (probably waiting on an AJAX request to come in) when the component is mounted.
Instead of eagerly assuming the data is present when the component is mounted, I suggested that the store property be watched and this.init() called when the propery is loaded.
However, I think this may not be the right approach, since the watch method will be called every time the property changes, which is not semantic for the case of doing prep work on data. I can suggest two solutions that I think are more elegant.
1. Trigger an event when the data is loaded
It's easy to set up a global messaging bus in Vue (see, for example, this post).
Assuming that the property is being loaded in a Vuex action,the flow would be similar to:
{
...
actions: {
async comments() {
try {
await loadComments()
EventBus.trigger("comments:load:success")
} catch (e) {
EventBus.trigger("comments:load:error", e)
}
}
}
...
}
You can gripe a bit about reactivity and events going agains the reactive philosophy. But this may be an example of a case where events are just more semantic.
2. The reactive approach
I try to keep computation outside of my views. Instead of defining chunkify inside your component, you can instead tie that in to your store.
So, say that I have a JavaScrip module called store that exports the Vuex store. I would define chunkify as a named function in that module
function chunkify (a, n) {
...
}
(This can be defined at the bottom of the JS module, for readability, thanks to function hoisting.)
Then, in your store definition,
const store = new Vuex.Store({
state: { ... },
...
getters: {
chunkedComments (state) {
return function (chunks) {
if (state.comments)
return chunkify(state.comments, chunks);
return state.comments
}
}
}
...
})
In your component, the computed prop would now be
computed: {
comments() {
return this.$store.getters.chunkedComments(3);
},
}
Then the update cascase will flow from the getter, which will update when comments are retrieved, which will update the component's computed prop, which will update the ui.
Use getters, merge chuckify and init function inside the getter.And for computed comment function will return this.$store.getters.YOURFUNC (merge of chuckify and init function). do not add anything inside mounted.
I am trying to make a simple product catalogue using vuejs2.
All my data is stored in an object array, I then have a subset of this data which is what the product catalogue uses to display each product.The subset is used for pagination.
When a user switches pages it clears and pushes the next set of data into the array.
This automatically makes the product component display the new data.
I have issues with the following, each product has multiple colours which are stored as a comma delimited string eg (white, blue, red)
I am trying to make that information appear as a drop down list of options beside each product.
This works until I switch to the next page of data, all other details update except the colour drop downs, which only reflect the previous set of data.
My product list is stored in an object array like:
var obj = {
productID: productID,
product: title,
gender: gender,
colour: colour,
cost: cost,
size: size,
description: description
}
productArray.push(obj);
I then have several components that display this array of data:
Vue.component('product-list', {
template: '<ul id="productList">'+
'<product :productID="product.productID" v-for="product in products">'+
'<h4>Colour</h4><colourSelect :colours="product.colour" :productID="product.productID"></colourSelect>' +
'<h4>Gender</h4><span class="genderSpan"><p v-bind:id="getID(product.productID)">{{product.gender}}</p></span>' +
'</product></ul>',
data: function() {
return {
products:
paginatedArray
};
},
Vue.component('colourSelect', {
props: ['productID', 'colours'],
template: '<select v-bind:id="getID()" class="form-control input-xs"><colourOption v-for="colourItem in colourArray"></colourOption></select>',
data: function () { //split string based into array
var newArray = [];
var optionsArray = this.colours.split(',');
for (i = 0; i < optionsArray.length; i++) {
var obj = {
colour: optionsArray[i]
}
newArray.push(obj)
}
return {
colourArray: newArray
};
},
methods: {
getID: function (test) {
return 'colourSelect' + this.productID;
}
}
});
Vue.component('colourOption', {
props:['options'],
template: '<option><slot></slot></option>'
});
Within the app section of vuejs I have the following methods that do the pagination:
buildPages: function () {
for (i = 1; i < this.listLength() /this.totalPage ; i++) {
this.pages.push(i);
}
var page = this.currentPage * this.totalPage;
for (i = page; i < page + this.totalPage ; i++) {
paginatedArray.push(productArray[i]);
}
},
listLength: function () {
var listTotal = productArray.length;
return listTotal
},
changePage: function (number) {
this.currentPage = number
var page = this.currentPage * this.totalPage;
//paginatedArray = [];
var count = 0;
for (i = page; i < page + this.totalPage ; i++) {
if (typeof productArray[i] !== 'undefined') {
paginatedArray.splice(count, 1, productArray[i])
}
count++
}
},
productArray is the main array storing data, paginatedArray is the subset of data that the product component works off.
The issue appears to be within the colourSelect component, within its "data" section it splits the colour data and returns it as an colourOption component into the select, but won't update when the paginatedArray changes.
The colourSelect component does however appear to actually get passed the correct data, as getID method updates correctly. Its just the data section which is not being re-rerun.
This is my first vuejs site, anyone have any ideas around this?
Thanks
You should make the colourArray as a computed property, as the data block of component gets executed only once and later change of props will not update colourArray .
Vue.component('colourSelect', {
props: ['productID', 'colours'],
template: '<select v-bind:id="getID()" class="form-control input-xs"><colourOption v-for="colourItem in colourArray"></colourOption></select>',
computed:{
colourArray: function () { //split string based into array
var newArray = [];
var optionsArray = this.colours.split(',');
for (i = 0; i < optionsArray.length; i++) {
var obj = {
colour: optionsArray[i]
}
newArray.push(obj)
}
return newArray
}
},
methods: {
getID: function (test) {
return 'colourSelect' + this.productID;
}
}
});
How do I get access to the columns/datastore fields that are part of the sort set.
I am looking to modify the a grid's sort parameters for remote sorting. I need the remote sort param's sort key to match the column's field's mapping property. I need these things to happen though the normal 'column header click sorts the data' functionality.
Remote sorting and field mapping (ExtJS 4.1)
This functionality seems not to be implemented in ExtJS. Here is a solution using the encodeSorters function provided since ExtJS 4. Accessing fields map throught the model's prototype is a bit dirty but it does the job :
var store = Ext.create('Ext.data.Store', {
...,
proxy: {
...,
encodeSorters: function (sorters) {
var model = store.proxy.model,
map = model.prototype.fields.map;
return Ext.encode(Ext.Array.map(sorters, function (sorter) {
return {
property : map[sorter.property].mapping || sorter.property,
direction: sorter.direction
};
}));
}
}
});
However, it would be more relevant to override the original method :
Ext.data.proxy.Server.override({
encodeSorters: function(sorters) {
var min, map = this.model.prototype.fields.map;
min = Ext.Array.map(sorters, function (sorter) {
return {
property : map[sorter.property].mapping || sorter.property,
direction: sorter.direction
};
});
return this.applyEncoding(min);
}
});
Assuming you are using simpleSortMode, you could do something like this in your store.
listeners: {
beforeload: function( store, operation, eOpts ) {
if (store.sorters.length > 0) {
var sorter = store.sorters.getAt(0),
dir = sorter.direction,
prop = sorter.property,
fields = store.model.getFields(),
i,
applyProp = prop;
for (i = 0; i < fields.length; i++) {
if (fields[i].name == prop) {
applyProp = fields[i].mapping || prop;
break;
}
}
//clearing the sorters since the simpleSortMode is true so there will be only one sorter
store.sorters.clear();
store.sorters.insert(0, applyProp, new Ext.util.Sorter({
property : applyProp,
direction: dir
}));
}
}
},