I have two arrays with custom components in each.
List a is a search result. List b is a list of selected items.
Each component has a template that renders the item in the array.
So... the trouble I'm having is, once I have my list in list a, i want to click a link and have it add to list b. But when I try to add the item, I'm being told that Cannot read property 'push' of undefined
Here's my entire Vue. What am I doing wrong?
new Vue({
el: '#search',
data: {
query: '',
listA: '',
listB: ''
},
methods: {
search: function(event) {
if (this.query != "") {
this.$http({url: '/list-a?search=' + this.query, method: 'GET'}).then(function(response) {
this.listA = response.data
});
};
event.preventDefault();
}
},
components: {
listaitem: {
template: '#listaitem-template',
props: ['lista-item'],
methods: {
selected: function(listaitem) {
// When clicked, this will add this listaitem to listB
this.listB.push(listaitem);
}
}
},
listbitem: {
template: '#listbitem-template',
props: ['listbitem']
}
}
});
You should initialize listA and listB as empty arrays instead of empty strings like
data: {
query: '',
listA: [],
listB: []
}
This will allow you use this.listB.push(listaitem); in the listaitem component without throwing an error
Related
I've always thought that object.assign and spread operator are similar.
But today, I have an error 'You may have an infinite update loop in a component render'
State in advance, the structure of the parent object is like this:
I tried the following three ways:
one: spread operator, got this error 'You may have an infinite update loop in a component render'
const template = { ...parent , path: '', noShowingChildren: true }
this.onlyOneChild = template
two: Deep Copy, got this error 'You may have an infinite update loop in a component render'
const template = JSON.parse(JSON.stringify(parent))
template.path = ''
template.noShowingChildren = true
this.onlyOneChild = template
three: Object.assign, works fine.
const template = Object.assign(parent, { path: '', noShowingChildren: true })
this.onlyOneChild = template
These three situations, the results make me feel very confused.
Can someone answer this question for me?
Here is the code.Thank you for your help.
<template v-if="hasOneShowingChild(item.children,item) && (!onlyOneChild.children||onlyOneChild.noShowingChildren)&&!item.alwaysShow">
<router-link
v-if="onlyOneChild.meta"
:to="resolvePath(onlyOneChild.path)"
>
<el-menu-item
:index="getIndex(onlyOneChild.path)"
:class="{'submenu-title-noDropdown':!isNest}"
>
<item
v-if="onlyOneChild.meta"
:icon="onlyOneChild.meta.icon||item.meta.icon"
:title="onlyOneChild.meta.title"
/>
</el-menu-item>
</router-link>
</template>
export default {
name: 'SidebarItem',
components: { Item, AppLink },
props: {
// route object
item: {
type: Object,
required: true
},
isNest: {
type: Boolean,
default: false
},
basePath: {
type: String,
default: ''
}
},
data () {
return {
onlyOneChild: null
}
},
methods: {
hasOneShowingChild (children, parent) {
const showingChildren = children.filter(item => {
// console.log(item)
if (item.meta.hidden) {
return false
} else {
// Temp set(will be used if only has one showing child)
this.onlyOneChild = item
return true
}
})
// When there is only one child router, the child router is displayed by default
if (showingChildren.length === 1) {
return true
}
// Show parent if there are no child router to display
if (showingChildren.length === 0) {
console.log('this is parent object:', parent)
// const template = { ...parent , path: '', noShowingChildren: true }
// const template = { ...JSON.parse(JSON.stringify(parent)), path: '', noShowingChildren: true }
// const template = JSON.parse(JSON.stringify(parent))
// template.path = ''
// template.noShowingChildren = true
const template = Object.assign(parent, { path: '', noShowingChildren: true })
this.onlyOneChild = template
return true
}
return false
}
}
}
The first two methods you're using (parsing JSON and spreading) will create new objects or references, while the last method (assign) will use the exact same one and just assign new values to it. So I can only assume that the first two will trigger a state update by changing this.onlyOneChild completely, which will in return trigger another render, while the last one will not trigger a state change (since it's changing the values on that object and not the entire reference) and not trigger another rerender.
the expand/collapse part of this works just fine.
Right now I am using javascript startInterval() to reload the table every 2 seconds. Eventually this will be moving to web sockets.
In general, as part of the table load/reload, the system checks to see if it should display the icon " ^ " or " v " in the details column by checking row.detailsShowing, this works fine.
getChevron(row, index) {
if (row.detailsShowing == true) {
return "chevronDown";
}
return "chevronUp";
}
When the user selects the " ^ " icon in the relationship column, #click=row.toggleDetails gets called to expand the row and then the function v-on:click="toggleRow(row)" is called to keep track of which row the user selected. This uses a server side system generated guid to track.
Within 2 seconds the table will reload and the row collapses. On load/reload, in the first column it loads, relationship, I call a function checkChild(row), to check the row guid against my locally stored array, to determine if this is a row that should be expanded on load.
<template #cell(relationship)="row"> {{checkChild(row)}} <\template>
if the row guid matches one in the array I try setting
checkChild(row){
var idx = this.showRows.indexOf( row.item.id);
if(idx > -1){
row.item.detailsShowing = true;
row.rowSelected = true;
row.detailsShowing == true
row._showDetails = true;
}
}
and I am able to see that i have found match, but none of those variables set to true keeps the expanded row open, the row always collapses on reload
anyone have any ideas as to how i can make the row(s) stay open on table reload?
The issue with your code is because of a Vue 2 caveat. Adding properties to objects after they've been added to data will not be reactive. To get around this you have to utilize Vue.set.
You can read more about that here.
However, calling a function like you are doing in the template seems like bad practice.
You should instead do it after fetching your data, or use something like a computed property to do your mapping.
Here's two simplified examples.
Mapping after API call
{
data() {
return {
items: [],
showRows: []
}
},
methods: {
async fetchData() {
const { data } = await axios.get('https://example.api')
foreach(item of data) {
const isRowExpanded = this.showRows.includes(item.id);
item._showDetails = isRowExpanded;
}
this.items = data;
}
}
}
Using a computed
{
computed: {
// Use `computedItems` in `<b-table :items="computedItems">`
computedItems() {
const { items, showRows } = this;
return items.map(item => ({
...item,
_showDetails: .showRows.includes(item.id)
}))
}
},
data() {
return {
items: [],
showRows: []
}
},
methods: {
async fetchData() {
const { data } = await axios.get('https://example.api')
this.items = data;
}
}
}
For a more complete example, check the snippet below.
const {
name,
datatype,
image
} = faker;
const getUser = () => ({
uuid: datatype.uuid(),
personal_info: {
first_name: name.firstName(),
last_name: name.lastName(),
gender: name.gender(),
age: Math.ceil(Math.random() * 75) + 15
},
avatar: image.avatar()
});
const users = new Array(10).fill().map(getUser);
new Vue({
el: "#app",
computed: {
computed_users() {
const {
expanded_rows,
users
} = this;
return users.map((user) => ({
...user,
_showDetails: expanded_rows[user.uuid]
}));
},
total_rows() {
const {
computed_users
} = this;
return computed_users.length;
}
},
created() {
this.users = users;
setInterval(() => {
users.push(getUser());
this.users = [...users];
}, 5000);
},
data() {
return {
per_page: 5,
current_page: 1,
users: [],
fields: [{
key: "avatar",
class: "text-center"
},
{
key: "name",
thClass: "text-center"
},
{
key: "personal_info.gender",
label: "Gender",
thClass: "text-center"
},
{
key: "personal_info.age",
label: "Age",
class: "text-center"
}
],
expanded_rows: {}
};
},
methods: {
onRowClicked(item) {
const {
expanded_rows
} = this;
const {
uuid
} = item;
this.$set(expanded_rows, uuid, !expanded_rows[uuid]);
}
}
});
<link href="https://unpkg.com/bootstrap#4.5.3/dist/css/bootstrap.min.css" rel="stylesheet" />
<link href="https://unpkg.com/bootstrap-vue#2.21.2/dist/bootstrap-vue.css" rel="stylesheet" />
<script src="https://unpkg.com/vue#2.6.12/dist/vue.min.js"></script>
<script src="https://unpkg.com/bootstrap-vue#2.21.2/dist/bootstrap-vue.js"></script>
<script src="https://unpkg.com/faker#5.5.3/dist/faker.min.js"></script>
<div id="app" class="p-3">
<b-pagination v-model="current_page" :per-page="per_page" :total-rows="total_rows">
</b-pagination>
<h4>Table is refresh with a new item every 5 seconds.</h4>
<h6>Click on a row to expand the row</h6>
<b-table :items="computed_users" :fields="fields" bordered hover striped :current-page="current_page" :per-page="per_page" #row-clicked="onRowClicked">
<template #cell(avatar)="{ value }">
<b-avatar :src="value"></b-avatar>
</template>
<template #cell(name)="{ item: { personal_info: { first_name, last_name } }}">
{{ first_name }} {{ last_name }}
</template>
<template #row-details="{ item }">
<pre>{{ item }}</pre>
</template>
</b-table>
</div>
So I'm using Bootstrap Vue with this test app. I'm trying to change the variant of a table cell depending on the value of it. Unfortunately, the variant parameter will not take a function, so I'm out of ideas on how to achieve this.
This is my code:
var app = new Vue({
el: '#app',
data: {
items: [], //Will be populated through AJAX
fields: [
{
key: 'Vendedor',
label: 'Vendedor'
},
{
key: 'OBJETIVO',
label: 'Objetivo',
formatter: (value) => { return parseFloat(value).toFixed(2)},
variant: estiloObjetivo //THIS IS NOT WORKING
}
]
},
methods: {
Cargar: function () {
var salesperson = getCookie('salespersonCode');
var url_servicio = 'http://MywebService/';
var self = this;
$.ajax({
type: 'GET',
url: url_servicio + 'ventas/' + salesperson,
dataType: "json", // data type of response
success: function(data){
self.items = data
}
});
},
estiloObjetivo (value) {
if value > 0 //I need my cell variant to change depeding on this value
return 'danger'
else
return 'success'
}
}
})
This is my HTML part:
<div id="app">
<button v-on:click="Cargar">Cargar</button>
<b-table striped hover :fields="fields" :items="items"></b-table>
</div>
Any ideas on how to style a Bootstrap-vue cell dynamically?
This is the way it's done in the docs, it's actually set in the "items" array, but how is this useful in cases like mine where I get the data from a web service?:
{
salesperson: 'John',
Objetivo: 2000,
_cellVariants: { salesperson: 'success', Objetivo: 'danger'}
},
So I guess what I need is a way to set the I need is to set the _cellVariants parameter of each element in the 'items' array.
You likely need a computed property. Computed properties automatically update on changes to the reactive variables that they depend on.
The following example implements a computed property, styledItems, which you must use in place of items in the template. It returns a 1-deep copy of items, i.e. a new array containing a copy of each item, with the extra _cellVariants property added.
new Vue({
data: {
items: [ /* your data here */ ]
},
methods: {
estiloObjetivo: value => (value > 0) ? 'danger' : 'success'
},
computed: {
styledItems() {
return this.data.map(datum =>
Object.assign({}, datum, {
_cellVariants: {
Objetivo: this.estiloObjetivo(datum.Objetivo)
}
})
}
})
If you want to add variant to items you could use a computed property called cptItems and define it as follows:
computed:{
cptItems(){
return this.items.map((item)=>{
let tmp=item;
item.OBJETIVO>0?tmp.variant='danger':tmp.variant='success';
return tmp;
})
}
and use that property inside your template like :
<b-table .... :items="cptItems"></b-table>
I was sure the answers above would solve my own issue but they did not. I found a different way to color table cells: https://github.com/bootstrap-vue/bootstrap-vue/issues/1793
This is aside from using variants to color a table cell. Instead, we utilize tdclass and a function.
<script>
new Vue({
el: '#itemView',
data() {
return {
fields: [
{
key: 'Objetive',
sortable: true,
thClass: 'text-nowrap',
tdClass: (value, key, item) => {
return 'table-' + this.getColor(item);
}
}
],
};
},
methods: {
getColor(item) {
return item.Objetive > 0 ? 'danger' : 'success';
},
},
});
</script>
For my own use-case, I needed to compare two cells of the same row, then apply a class to one.
...
{
key: 'DEMAND_QTY',
sortable: true,
thClass: 'text-nowrap',
tdClass: (value, key, item) => {
return 'table-' + this.demandStatusColor(item);
},
},
{ key: 'TOTAL_DEMAND', sortable: true, thClass: 'text-nowrap' },
],
};
},
methods: {
demandStatusColor(item) {
return item.DEMAND_QTY < item.TOTAL_DEMAND ? 'danger' : 'success';
},
}
...
Perhaps this will help someone, if not OP.
#John answer worked for me. I don't have enough reputation to make comment or useful
tdClass: (type, key, item) => {
switch (type) {
case "value":
return "bg-warning text-white";
break;
case "value":
return "bg-danger text-white";
break;
case "value":
return "bg-info text-white";
break;
default:
break;
}
},
var vue_app = new Vue({
el: '#id1',
data: {
v1:[],
},
methods:{
pushUnique: function() {
this.v1.push({'id':1,'name':'josh'});
this.v1.push({'id':1,'name':'josh'}); //this should not work.
},
},
});
In above code the second push should not execute. I would like to keep id unique. How can this be done in Vue.
THanks
I would move to storing data in an object (keyed by id) and use a computed property to produce your v1 array. For example
data: {
v1obj: {}
},
computed: {
v1 () {
return Object.keys(this.v1obj).map(id => ({ id, name: this.v1obj[id] }))
}
}
Then you can use methods like Object.prototype.hasOwnProperty() to check for existing keys...
methods: {
pushUnique () {
let id = 1
let name = 'josh'
if (!this.v1obj.hasOwnProperty(id)) {
this.v1obj[id] = name
}
}
}
I'm creating a Page Builder with React.
I have a component which contains the structure of the page.
var LayoutPage = React.createClass({
getInitialState: function getInitialState() {
return {
items: {
'78919613':{
id: '78919613',
component : 'OneColumn',
cols:{
'565920458':{
id: '565920458',
content:{
'788062489':{
id: '788062489',
component : 'Text',
params: 'Lorem ipsum'
},
'640002213':{
id: '640002213',
component : 'Text',
params: 'Lorem ipsum'
}
}
}
}
}
}
};
},
.....
});
I have a system with drag'n drop to put a new element on the page and it works. But when the new element is dropped I want to update the state to add a new item in the array.
So how can I push a new item ? I did a test with that :
this.state.items.push({.....});
But I have an error :
TypeError: this.state.items.push is not a function
Can you help me ?
Thank you.
Instead of using an object in your state you should change it to the array like below :
this.state = {
items: [ // items array
{
id: 1,
name: "Stack"
},
{
id: 2,
name: "Overflow"
}],
count: 3, // another state
textValue : '' // and this is state too
}
Where the items it's an array of objects. And then you will be able to add new items to an array.
const newItem = {
id : this.state.count,
name: this.state.textValue
};
const newArr = this.state.items.concat(newItem);
this.setState({
items: newArr,
textValue: '',
count: this.state.count + 1
})
The whole example is here.
I hope it will help you!
Thanks
You'll start to get headaches if you directly mutate the state of your application. If you forget to call this.setState then it won't re-render!
Assuming you can't use an array (which would be easier), then you'll have to generate a unique key if you want to add another item to the object.
// create a new copy of items based on the current state
var newItems = Object.assign({}, this.state.items),
newItem = { id: '', component: '', cols: {} },
uniqueId = generateUniqueId();
// safely mutate the copy
newItems[uniqueId] = newItem;
// update the items property in state
this.setState({ items: newItems });
This is even easier with ES7/Babel.
const newItem = { id: '', component: '', cols: {} },
uniqueId = generateUniqueId(),
items = { [uniqueId]: newItem, ...this.state.items };
this.setState({ items });
You can generate a similar unique ID to the one you have there using Math.random.
function generateUniqueId() {
// removing leading '0.' from number
return Math.random()
.toString()
.slice(3);
}