Data becoming a proxy when trying to make a SearchBar with vue3 - javascript

i have an application made with VueJs3, and i trying to make a searchbar based on .filter(), and it seems to be working, but when i try to pass te value from my methods to my template its making a huge error, my data is becoming a proxy, and i cant use the data in a proxy format.
The code:
<template>
<div class="searchBar">
<input type="search" v-on:change="filterList" />
{{ search }}
</div>
<div id="listaDestaque" class="list">
<div class="list-header">
<p>Imagem</p>
<p>Descrição</p>
<p>Categoria</p>
<p>Un</p>
<p>Estoque</p>
<p>Valor</p>
</div>
<div v-for="item in current_data">
<div class="item-list">
<img v-bind:src="item.attributes.image" class="item-image" />
<p>{{ item.attributes.name }}</p>
<p>{{ item.attributes['category-name'].name }}</p>
<p>{{ item.attributes['unit-name'].name }}</p>
<p>{{ item.attributes['quantity-in-stock'] }}</p>
<p>{{ item.attributes.price }}</p>
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import api from '../../services/axios.js';
import headers from '../../services/headers.js';
export default defineComponent({
name: 'listaDestaque',
data() {
return { myList: [], search: '', filter: '', current_data: '' };
},
beforeMount() {
api.get('/products/all', headers).then((response) => {
this.myList = response.data.data;
const filter = this.myList.filter((element) => element.attributes.highlight == true);
this.current_data = filter;
});
},
methods: {
filterList() {
this.$data.search = event.target.value;
console.log(this.search);
const myListArray = JSON.parse(JSON.stringify(this.myList));
const filtered = myListArray.find(
(element) => element.attributes.name == this.search
);
const filteredArray = JSON.parse(JSON.stringify(filtered));
if (filtered) {
this.current_data = filteredArray;
console.log(this.current_data);
console.log(filteredArray);
}
},
},
});
</script>
<style scoped></style>
The code refering to the search bar is between the lines 2-5 and 35-65.
The console.log(filteredArray); in line 64 return this:
Proxy {id: '1', type: 'products', attributes: {…}, relationships: {…}}
[[Handler]]: Object
[[Target]]: Object
attributes: {name: 'Small Marble Chair', description: 'Nihil est dignissimos. Quia officia velit. Est aliquid eos.', quantity-in-stock: 12, price: 58.21, highlight: true, …}
id: "1"
relationships: {category: {…}, unit: {…}}
type: "products"
[[Prototype]]: Object
[[IsRevoked]]: false
And i recieve the error in lines 17-22 after i user the function filterList:
listaDestaque.vue:17 Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'image')
at listaDestaque.vue:17:42
at renderList (runtime-core.esm-bundler.js:2905:26)
at Proxy._sfc_render (listaDestaque.vue:24:11)
at renderComponentRoot (runtime-core.esm-bundler.js:896:44)
at ReactiveEffect.componentUpdateFn [as fn] (runtime-core.esm-bundler.js:5651:34)
at ReactiveEffect.run (reactivity.esm-bundler.js:185:25)
at instance.update (runtime-core.esm-bundler.js:5694:56)
at callWithErrorHandling (runtime-core.esm-bundler.js:155:36)
at flushJobs (runtime-core.esm-bundler.js:396:17)

The fact that a value changes to a proxy should not be causing any issues.
The issue is that you are using const filtered = myListArray.find(...). The result of find is either a null or the first object in the array that matches your criteria, so filteredArray will never have an array as the content. When you pass it to the template though, you use for in so the iterator will go through your objects' properties (ie item at some point will be attributes, so attributes.attributes will be unefined, ergo attributes.attributes.image throws an error.
You probably meant to use filter instead of find
const filtered = myListArray.filter(
(element) => element.attributes.name == this.search
);
I think the solution is a bit overcomplicated here
you can use v-model for the search input and the filtered dataset can use a computed
example: (SFC)
<template>
<div class="searchBar">
<input type="search" v-model="search"/>
{{ search }}
</div>
<table id="listaDestaque" class="list">
<thead>
<tr>
<th>Descrição</th>
<th>Categoria</th>
<th>Estoque</th>
<th>Valor</th>
</tr>
</thead>
<tbody>
<tr v-for="item in current_data">
<td>{{ item.attributes.name }}</td>
<td>{{ item.attributes['category-name'] }}</td>
<td>{{ item.attributes['quantity-in-stock'] }}</td>
<td>{{ item.attributes.price }}</td>
</tr>
</tbody>
</table>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
const dataset = [
{attributes:{name:"apple", "category-name":"fruit", "quantity-in-stock": 0.2, price: "$3.98"}},
{attributes:{name:"pear", "category-name":"fruit", "quantity-in-stock": 2, price: "$1.98"}},
{attributes:{name:"orange", "category-name":"fruit", "quantity-in-stock": 3, price: "$3.98"}},
{attributes:{name:"iPhone", "category-name":"not fruit", "quantity-in-stock": 18, price: "$398.29"}},
]
export default defineComponent({
name: 'listaDestaque',
data() {
return { myList: [], search: ''};
},
beforeMount() {
// api mocked
setTimeout(()=>{
this.myList = dataset
}, 500);
},
computed: {
current_data(){
return this.myList.filter((element) => element.attributes.name.includes(this.search)) || [];
}
},
});
</script>

Related

How to make an intersection between 2 datasets in Nuxt?

I tried to get data from api with params which come from an argument in a v-for.
In findUser method, I can console.log the data I'm looking for. But I can't get it at the end of findUser, why?
I know there is an async method to get it but I don't understand how to manage it to make it work with what I want to do;
I also thought about calling the two API at the same time, but the result is the same, I don't know how to manage it.
<template>
<div>
<h4>Listes Reçues</h4>
<p v-for="element in results" id="flex-list" :key="element.list_id">
{{ element.list_name }} de {{ findUser(element.user_id) }}
</p>
</div>
</template>
<script>
export default {
data() {
return {
results: '',
nickname: '',
}
},
created() {
this.$axios
.get(`/api/listReceived/${this.$auth.user[0].user_id}`)
.then((res) => {
this.results = res.data
console.log(JSON.stringify(this.results))
})
.catch((err) => {
console.error(err)
})
},
methods: {
findUser(id) {
console.log(id)
let data = ''
this.$axios
.get(`http://localhost:3000/api/userdata/${id}`)
.then((res) => {
data = res.data[0].nickname
console.log(data)
})
.catch((err) => {
console.error(err)
})
return data
},
},
}
</script>
On top of my top answer which was quite not on point regarding the question but still relevant, here is an example on how to handle an intersection properly.
I did not used an endpoint but mocked the data locally in data() hence why I keep my post above.
<template>
<div class="flex flex-col items-center">
<h1 class="p-4 bg-green-700 rounded-md">
List of users ordered by their according message
</h1>
<!-- <pre>{{ messages }}</pre> -->
<section>
<div v-for="user in groupedMessages" :key="user.id" class="mt-4">
<p>
User: <b>{{ user.name }}</b>
</p>
<aside>
Messages:
<span v-if="!user.messages.length">No messages actually</span>
</aside>
<p v-for="message in user.messages" :key="message.id">
<span class="italic">- {{ message.text }}</span>
</p>
</div>
</section>
</div>
</template>
<script>
// ES version of lodash, lighter overall
import { cloneDeep } from 'lodash-es'
export default {
name: 'Index',
data() {
return {
messages: [
{
id: 1,
text: 'Hello world',
userId: 1,
},
{
id: 2,
text: 'Nice cool message',
userId: 1,
},
{
id: 3,
text: 'Still for the first user?',
userId: 1,
},
{
id: 4,
text: 'Yep, apparently...',
userId: 1,
},
{
id: 5,
text: "Eh, surprise, I'm a sneaky one...",
userId: 3,
},
{
id: 6,
text: 'Oh, a second one.',
userId: 2,
},
{
id: 7,
text: "You're damn right!!",
userId: 2,
},
],
users: [
{
name: 'Patrick',
id: 1,
messages: [],
},
{
name: 'Pablo',
id: 2,
messages: [],
},
{
name: 'Unkown author',
id: 5,
messages: [],
},
{
name: 'Escobar',
id: 3,
messages: [],
},
],
}
},
computed: {
groupedMessages() {
// we use that to avoid any kind of further mutation to the initial `users` array
const clonedUsers = cloneDeep(this.users)
// we do loop on each message and find a corresponding user for it
this.messages.forEach((message) =>
clonedUsers.forEach((user) => {
if (user.id === message.userId) {
user.messages.push(message)
}
})
)
return clonedUsers
},
},
}
</script>
The github repo is available, please do not pay attention to the unrelated name of it.
This is how it looks on Netlify.
created() is totally fine with Vue but usually you do use fetch() and asyncData() hooks in Nuxt.
Here is the basic idea using JSONplaceholder's API.
Here is a possible /pages/index.vue
<template>
<div class="flex flex-col items-center">
<h1 class="p-4 bg-green-700 rounded-md">
List of users from JSONplaceholder
</h1>
<section class="mt-4">
<p v-for="user in users" :key="user.id">
{{ user.name }} 🚀 {{ user.email }} ~
<nuxt-link
:to="{ name: 'users-id', params: { id: user.id } }"
class="border-b-4 border-green-500"
>
Check details
</nuxt-link>
</p>
</section>
</div>
</template>
<script>
export default {
name: 'Index',
data() {
return {
users: [],
}
},
async fetch() {
this.users = await this.$axios.$get('/users')
},
}
</script>
<style>
* {
#apply bg-gray-800 text-gray-100;
}
</style>
And the detailed page aka /pages/users/_id.vue using the fetch() hook.
<template>
<div class="flex flex-col items-center">
<nuxt-link class="p-4 bg-purple-700 rounded-md" :to="{ name: 'index' }">
Go back
</nuxt-link>
<h2>User details ID: # {{ $route.params.id }}</h2>
<p v-if="$fetchState.pending">Loading user's info...</p>
<p v-else-if="$fetchState.error">Error while fetching user</p>
<div v-else>
<p>{{ user.name }}</p>
<p>{{ user.phone }}</p>
<p>{{ user.website }}</p>
<p>{{ user.company.name }}</p>
</div>
</div>
</template>
<script>
export default {
name: 'UserId',
data() {
return {
user: {},
}
},
async fetch() {
this.user = await this.$axios.$get(`/users/${this.$route.params.id}`)
},
}
</script>
I do prefer this approach because it's not blocking the render, you can add some smooth skeleton to still let the user know that something is happening. On top of that, fetch() is available on both components and pages while asyncData() is only for pages.
It also gives the nice $fetchState helper that can also be quite handy!
Here is the same /pages/users/_id.vue page using the asyncData() hook.
<template>
<div class="flex flex-col items-center">
<nuxt-link class="p-4 bg-purple-700 rounded-md" :to="{ name: 'index' }">
Go back
</nuxt-link>
<p>{{ user.name }}</p>
<p>{{ user.phone }}</p>
<p>{{ user.website }}</p>
<p>{{ user.company.name }}</p>
</div>
</template>
<script>
export default {
name: 'UserId',
async asyncData({ route, $axios }) {
const user = await $axios.$get(`/users/${route.params.id}`)
return { user }
},
}
</script>
Main benefit of using asyncData is the fact that it's more safe and that it's blocking the render (can be either a pro or a con, more of a con for me personally).
Here are some other in-depth answers comparing fetch() vs asyncData().
Check out this handy blog article on the subject and also this dev.to clone example.
Finally, if you want to take the SSG path and optimize the whole thing with the least amount of API calls once on the client-side, you can also check my other answer.

How can I access nested data via an array with parsed json

I am creating a vuetify simple table that is going to display various data elements. The problem is, some of those elements are based on relationships and nested. Getting the top level data is fine, and if I pull the nested data as a standalone, it works fine as well.
However, what I want to do is utilize an array to avoid repetitive html code for the table. Is this possible at all?
Below is the code as constructed for the table itself.
HTML:
<v-simple-table fixed-header height="300px">
<template v-slot:default>
<thead>
<tr>
<th class="text-left">
Attribute
</th>
<th class="text-left">
Value
</th>
</tr>
</thead>
<tbody>
<tr
v-for="(serviceProperty, idx) in serviceProperties"
:key="idx">
<th>{{ serviceProperty.label }}</th>
<td>{{ service[serviceProperty.value] }}</td>
</tr>
</tbody>
</template>
</v-simple-table>
JS:
export default {
name: "Details",
data() {
return {
loading: true,
service: {},
serviceProperties: [
{
label: 'Description',
value: 'description'
},
{
label: 'Location',
value: 'organization.locations[1].streetAddress'
},
{
label: 'EIN',
value: 'organization.EIN'
}
]
};
},
props: ["serviceId"],
async created() {
this.service = await Vue.$serviceService.findOne(this.serviceId);
this.loading = false;
},
};
This seems unnecessarily complicated.
Consider using computed, like this
...
computed: {
mappedData() {
return this.service.map(item => {
Description: item.Description,
Location: item.organization.locations[1].streetAddress,
EIN: item.organization.EIN
})
}
}
...
You can then access the data in the template with:
...
<element v-for="item in mappedData">
{{item.Description}}
{{item.Location}}
{{item.EIN}}
</element>
...

Vue component testing with Jest error, TypeError: Cannot read property 'name' of undefined

I have a Vue component with the following template below. It receives an object as a prop. First, it is pulled from the backend on created() in Admin.vue, passed as an array to OrderList, looped over, then passed as an individual order to Order. The ShowOrder component in <router-link> displays the individual order on its own page.
<template v-if="order ? order : error">
<div class="order-container -shadow">
<div class="order-number">
<p>{{ order.orderId }}</p>
<p>{{ order.daySelected }}</p>
<p>{{ order.timeSelected }}</p>
</div>
<div class="customer-info">
<h2>{{ order.userInfo.name }}</h2>
<p>
{{ order.userInfo.street }},
{{ order.userInfo.city }}
</p>
<p v-if="order.userInfo.apt">
{{ order.userInfo.apt }}
</p>
<p>{{ order.userInfo.phone }}</p>
...
...
<router-link :to="{ name: 'ShowOrder', params: { id: order.orderId, order: order }}"
>Show Details</router-link
>
</div>
</template>
<script>
export default {
name: "Order",
props: {
order: {
type: Object,
default: function() {
return {};
}
}
},
data() {
return {
error: "Error"
};
}
};
</script>
My Order.spec.js is as follows:
const localVue = createLocalVue();
localVue.use(VueRouter);
const router = new VueRouter();
const $route = {
path: '"/show-order/:id"'
};
shallowMount(Order, {
localVue,
router,
stubs: ["router-link", "router-view"]
});
const mockOrder = {
orderId: 123,
chocolateChip: 24,
oatmeal: 0,
campfire: 12,
daySelected: "Wed Jan 27",
timeSelected: "2:30 - 3:00",
userInfo: {
apt: "",
city: "Port ",
email: "k#gmail.com",
street: " Point Road",
phone: "(123)446-1980",
name: "Mr.Smith"
}
};
describe("Order.vue", () => {
it("renders correctly", () => {
const wrapper = shallowMount(Order, {
localVue,
propsData: {
order: mockOrder,
$route
}
});
console.log(wrapper.html());
expect(wrapper.element).toMatchSnapshot();
});
});
The error I receive looks like this:
FAIL tests/unit/Order.spec.js
● Test suite failed to run
TypeError: Cannot read property 'name' of undefined
72 | flex-flow: column wrap;
73 | margin-top: 1em;
> 74 | transition: all 0.2s linear;
| ^
75 | }
76 |
77 | .order-container:hover {
I can't even get a snapshot test to work. I read many of the other similar questions here with this error, and tried the v-if in the template to wait for the data before the component loads. What is going on here? And if this is an object error, why in the console is there a pointer to my scoped css and not order.userInfo.name?
I also, tried changing from created() in Admin.vue to mounted().
In Order.vue, I tried adding the object to make it reactive based on these Vue docs.
data() {
return {
error: "Error",
orderItem: {}
};
},
mounted() {
this.orderItem = Object.assign({}, this.order);
console.log(this.orderItem, "line 65");
}
You're calling shallowMount() twice: once outside of your test without the mockOrder (and this call is not necessary), and again inside your test with the mockOrder.
// Order.spec.js
shallowMount(Order, { localVue }) // ❌ no props, but no need for this call
describe("Order.vue", () => {
shallowMount(Order, {
localVue,
propsData: {
order: mockOrder
}
})
})
The first call is causing the error because its order prop defaults to the empty object, so order.userInfo would be undefined, leading to the error for order.userInfo.name.
The solution is to remove the unnecessary first call to shallowMount().

Are there a way to overwrite increment/decrement-handling for a input of type number?

What i would like to do is manipulate the built-in html input buttons for increment and decrement of numbers. If there is a "vue-way" of doing this, that would of course be preferred.
First of all i'm working on a small vue-app that i've created for learning Vue, what i got now is a Vuex store which contains the state and methods for a shopping cart. I have bound the value item.itemCount seen in the image, to the value of the inputfield. But i would like the increment/decrement buttons to actually update the vuex-state in a proper way.
<input
class="slim"
type="number"
v-model.number="item.itemCount"
/>
I understand that i can just stop using a input-field, and create my own "count-view" + two buttons, but i'm curious if it's possible to do something like this.
UPDATE
Shoppingcart.vue
<template>
<div class="sliding-panel">
<span class="header">Shopping Cart</span>
<table>
<thead>
<th>Item Name</th>
<th>Count</th>
<th>Remove</th>
</thead>
<transition-group name="fade">
<tr v-for="item in items" :key="item.id">
<td>{{ item.name }}</td>
<td>
<input class="slim" type="number" v-model.number="item.itemCount" />
</td>
<td><button #click="removeProductFromCart(item)">Remove</button></td>
</tr>
</transition-group>
<tr>
Current sum:
{{
sum
}}
of
{{
count
}}
products.
</tr>
</table>
</div>
</template>
<script>
import { mapState, mapActions } from "vuex";
export default {
computed: mapState({
items: (state) => state.cart.items,
count: (state) => state.cart.count,
sum: (state) => state.cart.sum,
}),
methods: mapActions("cart", ["removeProductFromCart"]),
};
</script>
<style>
</style>
First you don't need to "overwrite increment/decrement handling" in any way. You have the <input> so you need to handle all user inputs changing value - be it inc/dec buttons or user typing value directly...
Proper way of updating Vuex state is by using mutations. So even it's technically possible to bind v-model to some property of object stored in Vuex (as you do), it's not correct "Vuex way"
If there is only single value, you can use computed prop like this:
computed: {
myValue: {
get() { return this.$store.state.myValue },
set(value) { this.$store.commit('changemyvalue', value )} // "changemyvalue" is defined mutation in the store
}
}
...and bind it to input
<input type="number" v-model="myValue" />
But because you are working with array of values, it is more practical to skip v-model entirely - in the end v-model is just syntactic sugar for :value="myValue" #input="myValue = $event.target.value"
In this case
<input type="number" :value="item.itemCount" min="1" #input="setItemCount({ id: item.id, count: $event.target.value})"/>
...where setItemCount is mutation created to change item count in the cart
Working example:
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
items: [
{ id: 1, name: 'Socks', itemCount: 2},
{ id: 2, name: 'Trousers', itemCount: 1}
]
},
mutations: {
setItemCount(state, { id, count }) {
const index = state.items.findIndex((item) => item.id === id);
if(index > -1) {
const item = state.items[index]
item.itemCount = count;
console.log(`Changing count of item '${item.name}' to ${count}`)
}
}
}
})
const app = new Vue({
store,
template: `
<div>
<span>Shopping Cart</span>
<table>
<thead>
<th>Item Name</th>
<th>Count</th>
<th>Remove</th>
</thead>
<transition-group name="fade">
<tr v-for="item in items" :key="item.id">
<td>{{ item.name }}</td>
<td>
<input type="number" :value="item.itemCount" min="1" #input="setItemCount({ id: item.id, count: $event.target.value})"/>
</td>
<td><button>Remove</button></td>
</tr>
</transition-group>
</table>
</div>
`,
computed: {
...Vuex.mapState({
items: (state) => state.items,
})
},
methods: {
...Vuex.mapMutations(['setItemCount'])
}
})
app.$mount("#app")
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.12/vue.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vuex/3.5.1/vuex.min.js"></script>
<div id="app"> </div>

Remove Item from Firebase Vuex

I'm new to Vue and I wanted to learn Veux by building a simple CRUD application with firebase. So far I've been able to figure things out (though if you see something badly coded, I would appreciate any feedback) but I can't seem to figure out how to remove an item. The main problem is that I can't reference it properly. I'm getting [object Object] in my reference path but when I log it I get the correct id.
Firebase Flow:
So I'm making a reference to 'items', Firebase generates a unique key for each item and I added in an id to be able to reference it, though I could also reference it by the key.
I've been able to do this without using Veux and just component state but I've been trying for hours to figure out what I'm doing wrong.
I'm also getting this error:
Store.js
import Vue from 'vue'
import Vuex from 'vuex'
import database from './firebase'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
items: []
},
mutations: {
RENDER_ITEMS(state) {
database.ref('items').on('value', snapshot => {
state.items = snapshot.val()
})
},
ADD_ITEM(state, payload) {
state.items = payload
database.ref('items').push(payload)
},
REMOVE_ITEM(index, id) {
database.ref(`items/${index}/${id}`).remove()
}
},
// actions: {
// }
})
Main.vue
<template>
<div class="hello">
<input type="text" placeholder="name" v-model="name">
<input type="text" placeholder="age" v-model="age">
<input type="text" placeholder="status" v-model="status">
<input type="submit" #click="addItem" />
<ul>
<li v-for="(item, index) in items" :key="index">
{{ item.name }}
{{ item.age }}
{{ item.status }}
<button #click="remove(index, item.id)">Remove</button>
</li>
</ul>
</div>
</template>
<script>
import { mapState, mapMutations } from 'vuex'
import uuid from 'uuid'
export default {
name: 'HelloWorld',
created() {
this.RENDER_ITEMS(this.items)
},
data() {
return {
name: '',
age: '',
status: '',
id: uuid(),
}
},
computed: {
...mapState([
'items'
])
},
methods: {
...mapMutations([
'RENDER_ITEMS',
'ADD_ITEM',
'REMOVE_ITEM'
]),
addItem() {
const item = {
name: this.name,
age: this.age,
status: this.status,
id: this.id
}
this.ADD_ITEM(item)
this.name = ''
this.age = ''
this.status = ''
},
remove(index, id) {
console.log(index, id)
this.REMOVE_ITEM(index, id)
}
}
}
</script>
The first argument to your mutation is always the state.
In your initial code:
REMOVE_ITEM(index, id) {
database.ref(`items/${index}/${id}`).remove()
}
index is the state object, which is why you are getting [object Object] in the url.
To fix your issue, pass an object to your mutation and change it to:
REMOVE_ITEM(state, {index, id}) {
database.ref(`items/${index}/${id}`).remove()
}
And when you are calling your mutation with the remove method, pass an object as well:
remove(index, id) {
console.log(index, id)
// pass an object as argument
// Note: {index, id} is equivalent to {index: index, id: id}
this.REMOVE_ITEM({index, id})
}

Categories

Resources