How to display async data in vue template - javascript

I'm interesting in the case of displaying in vue template data which loaded asynchroniously. In my particular situation I need to show title attribute of product object:
<td class="deals__cell deals__cell_title">{{ getProduct(deal.metal).title }}</td>
But the product isn't currently loaded so that the title isn't rendered at all. I found a working solution: if the products aren't loaded then recall getProduct function after the promise will be resolved:
getProduct (id) {
if (!this.rolledMetal.all.length) {
this.getRolledMetal()
.then(() => {
this.getProduct(id)
})
return {
title: ''
}
} else {
return this.getRolledMetalById(id)
}
}
However maybe you know more elegant solution because I think this one is a little bit sophisticated :)

I always use a loader or a spinner when data is loading!
<template>
<table>
<thead>
<tr>
<th>One</th>
<th>Two</th>
<th>Three</th>
</tr>
</thead>
<tbody>
<template v-if="loading">
<spinner></spinner> <!-- here use a loaded you prefer -->
</template>
<template v-else>
<tr v-for="row in rows">
<td>{{ row.name }}</td>
<td>{{ row.lastName }}</td>
</tr>
</template>
</tbody>
</table>
</template>
And the script:
<script>
import axios from 'axios'
export default {
data() {
return {
loading: false,
rows: []
}
},
created() {
this.getDataFromApi()
},
methods: {
getDataFromApi() {
this.loading = true
axios.get('/youApiUrl')
.then(response => {
this.loading = false
this.rows = response.data
})
.catch(error => {
this.loading = false
console.log(error)
})
}
}
}
</script>

There are a few good methods of handling async data in Vue.
Call a method that fetches the data in the created lifecycle hook that assigns it to a data property. This means that your component has a method for fetching the data and a data property for storing it.
Dispatch a Vuex action that fetches the data. The component has a computed property that gets the data from Vuex. This means that the function for fetching the data is in Vuex and your component has a computed property for accessing it.
In this case, it looks like your component needs to have a RolledMetal and based on that it retrieves a product. To solve this you can add methods that fetch both of them, and call them on the created lifecycle. The second method should be called in a then-block after the first one to ensure it works as expected.

Related

Vue js send data from api to new page

I am new to Vue Js and I would like to know how I can send data between two components. I am building a vue app that gets a list of users from an api and displays them. I would like to know I can move data between two components so that I can view more details on a new page.
here is my html code to display the data in a table
<table class="table table-hover table-striped">
<thead>
<tr>
<th scope="col">ID</th>
<th scope="col">Name</th>
<th scope="col">Occupation</th>
<th scope="col">Email</th>
<th scope="col">Bio</th>
<th scope="col">View</th>
<th scope="col">Edit</th>
</tr>
</thead>
<tbody>
<tr v-for="user in users" :key="user.id">
<th scope="row">{{ user.id }}</th>
<td>{{ user.name }}</td>
<td>{{ user.username}}</td>
<td>{{ user.email }}</td>
<td>{{ user.phonenumber}}</td>
<router-link to="/users" class="btn btn-primary">View</router-link>
</td>
<td>
<router-link #click="shareData" :key="user.id" to="/edit" class="btn btn-primary">Edit</router-link>
</td>
</tr>
</tbody>
</table>
this is my js code
<script>
import axios from "axios";
import moment from "moment";
export default {
name: 'Home',
created() {
this.getUsers();
},
data() {
return {
users: [],
};
},
methods: {
getUsers() {
axios
.get("https://607e868602a23c0017e8b79e.mockapi.io/api/v1/users")
.then((response) => {
console.log(response.data);
this.users = response.data;
})
.catch((error) => {
console.log(error);
});
},
format_date(value) {
if (value) {
return moment(String(value)).format("DD-MM-YYYY");
}
},
shareData(){
this.$router.push({name:"Users", params:{data:this.users}})
},
editData(){
}
},
};
where do how I move the data to the view and edit routes/components
You can use vue-router's dynamic route matching for this kind of problem. You can simply refactor your /user endpoint to /user/:id and in the component you are calling when landing on /user, you can simply make your api call and fill in the details. You need to update your first router link to have some id in the form of: /user/123 and in the component, you can get that id by calling this.$route.params.id.
Depending on the complexity of your Application, you may use VUEX to store all your Application's state, or if you have a small application and do want to spend time learning VUEX, you may simply use props. With props you can pass objects ( data ) to your components or even routes.
If you want to pass data from one sibling component to another sibling component ( components having same parent) then you can use Event Bus.
Here's an article which I used to learn Event Bus Event Bus Article
If you want to pass data from Parent component to child Component , you can simply use a props.
If you want to get data anywhere in your code , then use Vuex.
Vuex sure is very handy. Happy learning.

How to render vuejs component on sibling submit

I have the following code
<body>
<div class="content" id="app">
<file-management></file-management>
<attachment-list></attachment-list>
</div>
<script src="{{ asset('js/app.js') }}"></script>
</body>
FileManagement component code:
<template>
<div>
<button type="button" #click="storeList()">
Save
</button>
</div>
</template>
<script>
export default {
methods: {
storeList: function () {
axios.post('/list', this.data, config)
.then(() => {
// on save I want to be able to load the table again that is found in AttachmentList component
});
},
}
}
</script>
AttachmentList component code:
<template>
<div>
<tr v-for="attachment in attachments" :key="attachment.id">
<td>{{ attachment.name }}</td>
</tr>
</div>
</template>
<script>
export default {
data() {
return {
attachments: []
}
},
methods: {
getList() {
axios.get(`/list`)
.then((data) => {
this.attachments = data;
});
}
}
}
</script>
What I want to do is that I want to be able to load the table of the list when I click save in the other component (after the post request has completed). How will I be able to achieve this?
Easiest way would be to have your FileManagement component emit an event which the parent can listen to, then trigger the AttachmentList#getList() method.
For example
// in AttachmentList
methods: {
async storeList () {
await axios.post('/list', this.data, config)
this.$emit('list-updated')
}
}
and in the parent template
<file-management #list-updated="$refs.list.getList()"></file-management>
<attachment-list ref="list"></attachment-list>
This is how I would proceed.
create a parent component for the siblings.
add a boolean data member (flag) to it with the status of the clicked button.
emit a signal from FileManagement when the button is clicked.
catch this signal in the parent component to set the flag.
pass this flag to the AttachmentList component as a prop.
use this flag inside a v-if to show / hide the the table.

Why variable is not defined in created() method

I've got code like this. I would like to split deals array into 3 separate arrays: dealsCol1, dealsCol2, dealsCol2 and I would like to have it done after the object is created. I'm a beginner in JavaScript. I previously programmed in C++ and the thing that I guess I need is something like a constructor. I've found out that created() function works as a constructor (it's called on object creation). I put the part of the code that splits the array in that function, but I get an error:
vue.esm.js:591 [Vue warn]: Error in created hook: "ReferenceError: deals is not defined"
I have no idea why it's not defined there, because I guess that it should be. Could you give me some hints how can I solve the problem?
<script>
export default {
props: {
deals: Array
},
data() {
return {
dealsCol1: [],
dealsCol2: [],
dealsCol3: []
};
},
created() { // why it doesn't work??
this.dealsCol1 = this.deals.slice(0, this.deals.length/3),
this.dealsCol2 = this.deals.slice(this.deals.length/3, 2*this.deals.length/3),
this.dealsCol3 = this.deals.slice(2*this.deals.length/3, this.deals.length-1)
}
};
</script>
EDIT:
If I use the trick with computed() from one of the answers everything works good. But I'm wondering why the deals are visible in every other method beyond constructed(). It is also visible in the template part. Why is that?
The parent component code looks like this:
<template>
<div>
<editDealsModal ref="editDealsModal" :deals="deals" #editDeals="editDeals" />
<table class="table table-sm color mb-2">
<caption class="caption table-caption">Users</caption>
<thead>
<th class="text-left text-nowrap">Deals</th>
<th></th>
<th></th>
<th></th>
<th></th>
</thead>
<tbody>
<tr v-for="user in users" :key=user.Id v-bind:class="[{ disabled: user.IsEnabled == false }]">
<td class="text-left align-middle">{{user.Username}}</td>
<td class="text-left align-middle">
<div v-for="role in user.Roles" :key=role>{{role}}</div>
</td>
<td class="text-left align-middle">
<div v-for="deal in user.Deals" :key=deal>{{deal}}</div>
</td>
<td class="align-middle">
<b-btn variant="link" #click="showEditDealsModal(user)" v-bind:disabled="!user.IsEnabled">Edit deals</b-btn>
</td>
</tr>
</tbody>
</table>
</div>
</template>
<script>
import service from '../../services/HttpRequestService'
import EditDealsModal from './EditDealsModal'
export default {
props: {
users: Array,
deals: Array
},
methods: {
showEditDealsModal(user) {
this.$refs.editDealsModal.showModal(user, user.Deals || [])
},
async editDeals(user, data) {
try {
await service.editDeals(user.Id, data);
this.$emit("userEdited", { type: "success", msg: "Updated deals for " + user.Username })
} catch (err) {
this.$emit("userEdited", { type: "danger", msg: "Failed to edit deals for " + user.Username })
}
},
},
components: {
EditDealsModal
}
}
</script>
enter code here
Try to defined a default value for your myData array, like this
props: {
myData: {
default: [],
type: Array,
},
},
By the way props are used to pass data from parent to child, I don't think this is the best way to do this.
myDataCol1,2,3 should be computed properties :
props: {
myData: {
default: [],
type: Array,
},
},
computed: {
myDataCol1(){
return this.myData.slice(0, this.myData.length/3)
}
.
.
.
}

VueJS Render VNode

tl;dr:
Given a VueJS VNode object, how do I get the HTML element that would be generated if it were rendered?
e.g.:
> temp1
VNode {tag: "h1", data: undefined, children: Array(1), text: undefined, elm: undefined, …}
> temp1.children[0]
VNode {tag: undefined, data: undefined, children: undefined, text: "Test", elm: undefined, …}
> doSomething(temp1)
<h1>Test</h1>
Goal
I'm attempting to build a small VueJS wrapper around the DataTables.net library.
To mimic the behavior of HTML tables in my markup, I want something like the following:
<datatable>
<thead>
<tr>
<th>Name</th>
<th>Age</th>
<th>Salary</th>
</tr>
</thead>
<tbody>
<datatable-row v-for="person in people">
<td>{{ person.name }}</td>
<td>{{ person.age }}</td>
<td>{{ person.salary }}</td>
</datatable-row>
</tbody>
</datatable>
What I've done so far
I've started to implement this as follows:
DataTable.vue
<template>
<table ref="table" class="display table table-striped" cellspacing="0" width="100%">
<slot></slot>
</table>
</template>
<script>
/* global $ */
export default {
data: () => ({
instance: null
}),
mounted() {
this.instance = $(this.$refs.table).dataTable();
this.$el.addEventListener("dt.row_added", function(e) {
this.addRow(e.detail);
});
},
methods: {
addRow(row) {
// TODO <-----
console.log(row);
}
}
};
</script>
DataTableRow.vue
<script>
/* global CustomEvent */
export default {
mounted() {
this.$nextTick(() => {
this.$el.dispatchEvent(new CustomEvent("dt.row_added", {
bubbles: true,
detail: this.$slots.default.filter(col => col.tag === "td")
}));
});
},
render() { return ""; }
};
What this currently does:
When the page loads, the DataTable is initialized. So the column headers are properly formatted and I see "Showing 0 to 0 of 0 entries" in the bottom left
The CustomEvent is able to bubble up past the <tbody> and be caught by the DataTable element successfully (circumventing the limitation in VueJS that you can't listen to events on slots)
What this does not do:
Actually add the row
My event is giving me an array of VNode objects. There's one VNode per column in my row. The DataTables API has an addRow function which can be called like so:
this.instance.row.add(["col1", "col2", "col3"]);
In my case, I want the resultant element from the rendering of the VNode to be the elements in this array.
var elems = [];
for (var i = 0; i < row.length; i++)
elems[i] = compile(row[i]);
this.instance.row.add(elems);
Unfortunately this compile method eludes me. I tried skimming the VueJS documentation and I tried Googling it, but no dice. I tried manually passing the createElement function (the parameter passed to the render method) but this threw an error. How can I ask VueJS to render a VNode without injecting the result into the DOM?
I ran into the same issue wanting to do basically the same thing with a row details template for DataTables.net.
One solution could be to create a generic component that renders out a VNode and instantiate that programmatically. Here is how my setup for a dynamic detail row that I insert using datatable's row.child() API.
RenderNode.js
export default {
props: ['node'],
render(h, context) {
return this.node ? this.node : ''
}
}
Datatables.vue
Include the renderer component from above
import Vue from 'vue'
import nodeRenderer from './RenderNode'
Instantiate and mount the renderer to get the compiled HTML
// Assume we have `myVNode` and want its compiled HTML
const DetailConstructor = Vue.extend(nodeRenderer)
const detailRenderer = new DetailConstructor({
propsData: {
node: myVNode
}
})
detailRenderer.$mount()
// detailRenderer.$el is now a compiled DOM element
row.child(detailRenderer.$el).show()
You should define your components like with:
import {createApp} from 'vue';
import {defineAsyncComponent} from "vue";
createApp({
components: {
'top-bar': defineAsyncComponent(() => import('#Partials/top-bar'))
}
}).mount("#app")

Watching array stored in Vuex in VueJS

I have a customer list which is actually an array of objects. I store it in Vuex. I render the list in my component and each row has a checkbox. More precisely I use keen-ui and the checkbox rendering part looks like:
<tr v-for="customer in customers" :class="{ selected: customer.selected }">
<td>
<ui-checkbox :value.sync="customer.selected"></ui-checkbox>
</td>
<td>{{ customer.name }}</td>
<td>{{ customer.email }}</td>
</tr>
So the checkbox directly changes customers array which is bad: I use strict mode in Vuex and it throws me an error.
I want to track when the array is changed and call an action in order to change the vuex state:
watch: {
'customers': {
handler() {
// ...
},
deep: true
}
However it still changes the customer directly. How can I fix this?
First and foremost, be careful when using .sync: it will be deprecated in 2.0.
Take a look at this: http://vuex.vuejs.org/en/forms.html, as this problem is solved in here. Basically, this checkbox should trigger a vuex action on input or change. Taken from the docs:
<input :value="message" #input="updateMessage">
Where updateMessage is:
vuex: {
getters: {
message: state => state.obj.message
},
actions: {
updateMessage: ({ dispatch }, e) => {
dispatch('UPDATE_MESSAGE', e.target.value)
}
}
}
If you do not wish to track the mutations, you can move the state of this component away from vuex, to be able to use v-model in all its glory.
You just have to make a custom getter and setter:
<template>
<ui-checkbox :value.sync="thisCustomer"></ui-checkbox>
</template>
<script>
//this is using vuex 2.0 syntax
export default {
thisCustomer: {
get() {
return this.$store.state.customer;
},
set(val) {
this.$store.commit('SET_CUSTOMER', val);
// instead of performing the mutation here,
// you could also use an action:
// this.$store.disptach('updateCustomer')
}
},
}
</script>
In your store:
import {
SET_CUSTOMER,
} from '../mutation-types';
const state = {
customer: null,
};
const mutations = {
[SET_CUSTOMER](state, value) {
state.customer = value;
},
}
I'm not exactly sure what your store looks like, but hopefully this gives you the idea :)
if your customers are in the root state, you can try this:
watch: {
'$store.state.customers'{
handler() {
// ...
},
deep: true
}
}
try using mapState in your component and watch the customers like you have done above.worked for me

Categories