Async Axios data with Vue - javascript

I'm trying to load data from an axios call into a table on my page, but there's no data in the table with my current code. I'm assuming that the page elements are being rendered before the axios call finishes and therefore nothing is loading.
export default {
components: {
BaseTable
},
data() {
return {
table1: {
title: "Scene Objects",
columns: [...tableColumns],
data: [],
type: "hover"
},
};
},
mounted() {
axios.get('http://localhost:4000/api/card/leveldata').then((data) => {
this.table1.data = Object.values(data.data['Card Level Data']);
});
}
};
I've tried a lot of things to fix it, including using v-if to wait for the call to finish but I can't get anything to work. Any help is appreciated.
EDIT:
template:
<template>
<div>
<div class="row">
<card type="plain" title="Scene Preview">
<!-- <div id="map" class="map">
</div> -->
<iframe id="panoramic" src="https://momento360.com/e/u/9a0d3bcfff454d74950b0f9b8ded7e43?utm_campaign=embed&utm_source=other&heading=80.77&pitch=-0.4&field-of-view=75&size=medium"></iframe>
</card>
</div>
<div class="row">
<div class="col-12">
<card :title="table1.title">
<div class="table-responsive">
<base-table :data="table1.data"
:columns="table1.columns"
thead-classes="text-primary">
</base-table>
</div>
</card>
<base-button type="primary">Preview</base-button>
<base-button type="primary">Save</base-button>
</div>
</div>
</div>
</div>
</template>
BaseTable component:
<template>
<table class="table tablesorter" :class="tableClass">
<thead :class="theadClasses">
<tr>
<slot name="columns">
<th v-for="column in columns" :key="column">{{column}}</th>
</slot>
</tr>
</thead>
<tbody :class="tbodyClasses">
<tr v-for="(item, index) in data" :key="index">
<slot :row="item">
<td v-for="(column, index) in columns"
:key="index"
v-if="hasValue(item, column)">
{{itemValue(item, column)}}
</td>
</slot>
</tr>
</tbody>
</table>
</template>
<script>
export default {
name: 'base-table',
props: {
columns: {
type: Array,
default: () => [],
description: "Table columns"
},
data: {
type: Array,
default: () => [],
description: "Table data"
},
type: {
type: String, // striped | hover
default: "",
description: "Whether table is striped or hover type"
},
theadClasses: {
type: String,
default: '',
description: "<thead> css classes"
},
tbodyClasses: {
type: String,
default: '',
description: "<tbody> css classes"
}
},
computed: {
tableClass() {
return this.type && `table-${this.type}`;
}
},
methods: {
hasValue(item, column) {
return item[column.toLowerCase()] !== "undefined";
},
itemValue(item, column) {
return item[column.toLowerCase()];
}
}
};
</script>
<style>
</style>

Related

Action happening for all the items in v-for but I want to run the action on just the item I clicked on

I have an icon in the v-for loop and I want the action to be performed specifically to the one icon but it is happening for all the icons that are repeating because of v-for.
<template>
<div>
<tr
v-for="(randomData, randomIndex) in obj['randomData']"
:key="randomIndex"
class="random-table"
>
<td data-label="Activity Alert">
<el-popover
v-model="remindMeVisibility"
placement="left-start"
width="500"
trigger="manual"
class="popover-form"
>
<RemindMe />
<i
slot="reference"
class="el-icon-message-solid reminder-icon"
#click="remindMeVisibility = true"
></i>
</el-popover>
</td>
</tr>
</div>
</template>
<script>
export default {
data() {
return {
remindMeVisibility: false,
}
},
}
</script>
Here is a working example on how to have a specific popover edit per element for a given list.
<template>
<div>
<pre>{{ cityNames }}</pre>
<section>
<div v-for="city in cityNames" :key="city.id">
<span>City {{ city.name }}</span>
<el-popover v-model="city.popoverOpen" placement="top" width="160" class="popover">
<el-button slot="reference">Update visibility</el-button>
<p>Change the visibility of the City?</p>
<div style="text-align: right; margin: 0">
<el-button size="mini" type="text" #click="toggleCityVisibility({ id: city.id, visibility: false })">
Hide it
</el-button>
<el-button type="primary" size="mini" #click="toggleCityVisibility({ id: city.id, visibility: true })">
Make visible
</el-button>
</div>
</el-popover>
</div>
</section>
</div>
</template>
<script>
export default {
data() {
return {
cityNames: [
{
id: 1,
name: 'beijing',
remindMeVisibility: false,
popoverOpen: false,
},
{
id: 2,
name: 'shanghai',
remindMeVisibility: false,
popoverOpen: false,
},
{
id: 3,
name: 'guangzhou',
remindMeVisibility: false,
popoverOpen: false,
}
]
};
},
methods: {
toggleCityVisibility({ id, visibility }) {
console.log('id', id, 'visibility', visibility)
this.targetedCity = this.cityNames.find(city => city.id === id)
this.targetedCity.remindMeVisibility = visibility
}
},
}
</script>
<style scoped>
.popover {
margin-left: 1rem;
}
</style>
Here, we do have a list of cities, and we want to toggle their visibility (remindMeVisibility).
The id in the list was given for the :key uniqueness + find it later on when editing the visibility of it.
I didn't expected popoverOpen to be required here, but it is (a global popover state is not feasible from what I've tried).
This is how it looks

Vue modal with a router

I am new to Vue. I am building a simple app that will list all countries and when you click on a particular country it shows you more details about the country. Idea is to open country details in a modal.
I'm stuck with displaying that modal. The modal opens, but in the background. It also opens a detail page.
CountryDetail.vue:
<script>
import axios from 'axios';
export default {
name: 'country-detail',
props: [ 'isDarkTheme' ],
data () {
return {
pending: false,
error: null,
countryInfo: null,
alpha3Code: [],
alpha3CodetoString: [],
}
},
mounted () {
this.pending = true;
axios
.get(`https://restcountries.eu/rest/v2/name/${this.$route.params.country}?fullText=true`)
.then((response) => {
(this.countryInfo = response.data)
this.alpha3CodetoString = this.alpha3Code.join(';');
})
.catch(error => (this.error = error ))
.finally( () => { this.pending = false });
},
filters: {
formatNumbers (value) {
return `${value.toLocaleString()}`
}
}
}
</script>
<template>
<modal v-model="show">
<div class="modal-mask" :class="{ darkTheme : isDarkTheme }" name="modal">
<div class="modal-wrapper">
<div class="modal-container">
<div class="modal-header">
<slot name="header">
<h1 v-if="error !== null">Sorry, an error has occurred {{error}}</h1>
<div class="loaderFlex"><div v-if="pending" class="loader"></div></div>
</slot>
</div>
<div v-for="country in countryInfo" class="countryTile modal-body" v-bind:key="country.id">
<slot name="body">
<img v-bind:src="country.flag" alt="Country Flag" class="flag">
<div class="country-details">
<h1>{{country.name}}</h1>
<div class="listDiv">
<ul>
<li><span>Population:</span> {{country.population | formatNumbers }}</li>
<li><span>Capital:</span> {{country.capital}}</li>
<li><span>Iso:</span> {{country.alpha3Code}}</li>
</ul>
<ul>
<li><span>Currencies:</span> {{country.currencies['0'].name}}</li>
<li><span>Languages:</span>
<span
v-for="(language, index) in country.languages"
v-bind:key="index"
class="languages">
{{language.name}}<span v-if="index + 1 < country.languages.length">, </span>
</span>
</li>
</ul>
</div>
</div>
</slot>
</div>
<div class="modal-footer">
<slot name="footer">
<a #click="$router.go(-1)" class="backBtn"><i class="fas fa-arrow-left" />Go Back</a>
</slot>
</div>
</div>
</div>
</div>
</modal>
</template>
Home.vue:
<script>
import axios from 'axios';
export default {
name: 'home',
props: [ 'isDarkTheme' ],
data () {
return {
pending: false,
error: null,
countryInfo: null,
search: '',
darkMode: false,
}
},
mounted () {
this.pending = true;
axios
.get('https://restcountries.eu/rest/v2/all')
.then(response => (this.countryInfo = response.data))
.catch(error => (this.error = error ))
.finally( () => { this.pending = false });
},
filters: {
formatNumbers (value) {
return `${value.toLocaleString()}`
}
},
computed: {
filteredCountries: function () {
return this.countryInfo.filter((country) => {
if (this.region === '' ) {
return country.name.toLowerCase().match(this.search.toLowerCase());
} else if (this.search !== '') {
return country.name.toLowerCase().match(this.search.toLowerCase());
} else {
return ('blbla');
}
})
}
},
}
</script>
<template>
<div class="home" :class="{ darkTheme : isDarkTheme }">
<div class="searchBar">
<div class="searchContainer">
<i class="fas fa-search searchIcon"></i>
<input
class="searchInput"
type="text"
v-model="search"
aria-label="Search for a country..."
placeholder="Search for a country..."
/>
<ul class="searchResults"></ul>
</div>
</div>
<h1 v-if="error !== null">Sorry, an error has occurred {{error}}</h1>
<div class="loaderFlex"><div v-if="pending" class="loader"></div></div>
<div v-if="countryInfo" class="tileGrid" #click="showModal = true">
<div v-for="country in filteredCountries" class="countryTile" v-bind:key="country.id">
<router-link
:to="{ name: 'country-detail', params: {country: country.name }}"
class="linkTile"
>
<img v-bind:src="country.flag" alt="Country Flag" class="flag">
<div class="text">
<h1>{{ country.name }}</h1>
</div>
</router-link>
</div>
</div>
</div>
</template>
The router-link will always redirect you to another page, because its basically <a href="..."> see here. You don't need router if you just want to show the detail on a modal, you could just add the modal component inside the Home.vue component, then bind the modal and the countryName with props, then pass them in when clicking a button.
Home.vue:
<template>
<div>
<button #click="showDetail">
Show Detail
</button>
<CountryDetail :countryName="countryName" :showModal="showModal"/>
<div>
</template>
<script>
import CountryDetail from './CountryDetail.vue'
export default {
name: 'Home',
components: { CountryDetail },
data: () => ({
countryName: '',
showModal: false,
}),
methods: {
showDetail() {
this.showModal = true;
},
},
}
</script>
And instead of making request on mounted, you could use watch to do something like watching for the showModal prop, and make request everytime it has a truthy value. Like this:
CountryDetail.vue:
<template>
<modal v-model="showModal">
<!-- modal content -->
</modal>
</template>
<script>
export default {
name: 'CountryDetail',
props: ['countryName', 'showModal'],
watch: {
'showModal': {
deep: true,
handler(val) {
if (val && this.countryName !== '') {
// Make request
}
}
}
}
}
</script>

Vue.js pass a callback as prop to child component and execute on parent when it is clicked

I am trying to create a dropdown component, in which it receives various data and the list items are built dynamically, but I am having difficulty detecting the click on any item in the list
parent
<Dropdown
:items="[
{
text: 'Edit',
icon: 'fal fa-edit',
click: editFunction(id)
},
{
text: 'Delete',
icon: 'fal fa-trash-alt',
click: deleteFunction(id)
}
]"
/>
child Dropdown.vue
<template>
<div class="dropdown">
<a class="trigger">
<i class="far fa-ellipsis-h"></i>
</a>
<ul class="items">
<li v-for="(item, index) in items" :key="index" class="item">
<a>
<i :class="item.icon"></i>
{{ item.text }}
</a>
</li>
</ul>
</div>
</template>
<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
props: {
items: {
type: Array,
default: () => []
}
}
})
</script>
currently in this way, as soon as the parent component is created, the editFunction(id) and deleteFunction(id) methods are executed.
I know it's possible because vuetifyjs it does that way, but I tried to inspect the source code but I got nowhere.
What you want to achieve can be done by
parent.vue
<child-componet :parent-method="thisIsTheMethod" />
...
methods: {
thisIsTheMethod()
{
//here it does something
}
}
note that the method passed inside the prop does not have the parenthesis () because you are passing a reference to it.
To use it inside the child component add the ()
#click="parentMethod()"
To go back to your example, change this:
<Dropdown
:items="[
{
text: 'Edit',
icon: 'fal fa-edit',
click: editFunction(id)
},
{
text: 'Delete',
icon: 'fal fa-trash-alt',
click: deleteFunction(id)
}
]"
/>
to
<Dropdown
:items="[
{
text: 'Edit',
icon: 'fal fa-edit',
click: () => editFunction(10)
},
{
text: 'Delete',
icon: 'fal fa-trash-alt',
click: () => deleteFunction(20)
}
]"
/>
and leave your editFunction(id) method declaration as is. The argument will be automatically injected.
Despite this solution would work, the best way to achieve this communication between parent and child would be to emit the value in the child and then listen for it
there have betters ways to do it, but about your idea should be as below:
Vue.component('dropdown', {
template: '#dropdown-template',
props: {
items: {
type: Array,
default: () => []
}
}
})
new Vue({
el: '#app',
data() {
return {
users: [
{ id: 1, name: 'foo' },
{ id: 2, name: 'bar' },
]
}
},
methods: {
editFunction(id) {
console.warn('edit item ' + id)
},
deleteFunction(id) {
console.warn('delete item ' + id)
}
}
})
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<div id="app">
<table border="1">
<thead>
<tr>
<th>id</th>
<th>name</th>
<th>actions</tr>
</tr>
</thead>
<tbody>
<tr v-for="user in users" :key="user.id">
<td>{{ user.id }}</td>
<td>{{ user.name }}</td>
<td>
<dropdown
:items="[
{
text: 'Edit',
icon: 'fal fa-edit',
event: 'edit-function'
},
{
text: 'Delete',
icon: 'fal fa-trash-alt',
event: 'delete-function'
}
]"
#edit-function="editFunction(user.id)"
#delete-function="deleteFunction(user.id)"
/>
</td>
</tr>
</tbody>
</table>
</div>
<script type="text/x-template" id="dropdown-template">
<div class="dropdown">
<a class="trigger">
<i class="far fa-ellipsis-h"></i>
</a>
<ul class="items">
<li v-for="(item, index) in items" :key="index" class="item">
<a #click="$emit(item.event)">
<i :class="item.icon"></i>
{{ item.text }}
</a>
</li>
</ul>
</div>
</script>

How can I work around the limitation of multiple root elements in Vue.js? [duplicate]

This question already has answers here:
A way to render multiple root elements on VueJS with v-for directive
(6 answers)
Closed 2 years ago.
hopefully someone here will be able to help me with this problem.
I have the following data:
[
{
title: 'Header',
children: [
{
title: 'Paragraph',
children: [],
},
],
},
{
title: 'Container',
children: [
{
title: 'Paragraph',
children: [],
},
],
},
]
I want to render this in a list of <div> like this:
<div class="sortable-item" data-depth="1" data-index="0">Header</div> <!-- Parent -->
<div class="sortable-item" data-depth="2" data-index="0">Paragraph</div> <!-- Child-->
<div class="sortable-item" data-depth="1" data-index="1">Container</div> <!-- Parent -->
<div class="sortable-item" data-depth="2" data-index="0">Paragraph</div> <!-- Child-->
I have built a component that would be recursive, this is what I have so far:
<template>
<template v-for="(item, index) in tree">
<div
class="sortable-item"
:data-depth="getDepth()"
:data-index="index"
:key="getKey(index)"
>
{{ item.title }}
</div>
<Multi-Level-Sortable
:tree="item.children"
:parent-depth="getDepth()"
:parent-index="index"
:key="getKey(index + 0.5)"
></Multi-Level-Sortable>
</template>
</template>
<script>
export default {
name: 'MultiLevelSortable',
props: {
tree: {
type: Array,
default() {
return [];
},
},
parentDepth: {
type: Number,
},
parentIndex: {
type: Number,
},
},
methods: {
getDepth() {
return typeof this.parentDepth !== 'undefined' ? this.parentDepth + 1 : 1;
},
getKey(index) {
return typeof this.parentIndex !== 'undefined' ? `${this.parentIndex}.${index}` : `${index}`;
},
},
};
</script>
As you can see not only I have a <template> as the root element I also have a v-for, two "no no" for Vue.js. How can I solve this to render the list of elements like I pointed out above?
Note: I have tried vue-fragment and I was able to achieve the structure I wanted, but then when I tried using Sortable.js it didn't work, as if it wouldn't recognise any of the .sortable-item elements.
Any help will be greatly appreciated! Thank you!
Thanks to #AlexMA I was able to solve my problem by using a functional component. Here is what it looks like:
import SortableItemContent from './SortableItemContent.vue';
export default {
functional: true,
props: {
tree: {
type: Array,
default() {
return [];
},
},
},
render(createElement, { props }) {
const flat = [];
function flatten(data, depth) {
const depthRef = typeof depth !== 'undefined' ? depth + 1 : 0;
data.forEach((item, index) => {
const itemCopy = item;
itemCopy.index = index;
itemCopy.depth = depthRef;
itemCopy.indentation = new Array(depthRef);
flat.push(itemCopy);
if (item.children.length) {
flatten(item.children, depthRef);
}
});
}
flatten(props.tree);
return flat.map((element) => createElement('div', {
attrs: {
'data-index': element.index,
'data-depth': element.depth,
class: 'sortable-item',
},
},
[
createElement(SortableItemContent, {
props: {
title: element.title,
indentation: element.indentation,
},
}),
]));
},
};
The SortableItemContent component looks like this:
<template>
<div class="item-content">
<div
v-for="(item, index) in indentation"
:key="index"
class="item-indentation"
></div>
<div class="item-wrapper">
<div class="item-icon"></div>
<div class="item-title">{{ title }}</div>
</div>
</div>
</template>
<script>
export default {
name: 'SortableItemContent',
props: {
title: String,
indentation: Array,
},
};
</script>
Given the data I have posted on my question, it now renders the HTML elements like I wanted:
<div data-index="0" data-depth="0" class="sortable-item">
<div class="item-content">
<div class="item-wrapper">
<div class="item-icon"></div>
<div class="item-title">Header</div>
</div>
</div>
</div>
<div data-index="0" data-depth="1" class="sortable-item">
<div class="item-content">
<div class="item-indentation"></div>
<div class="item-wrapper">
<div class="item-icon"></div>
<div class="item-title">Paragraph</div>
</div>
</div>
</div>
<div data-index="1" data-depth="0" class="sortable-item">
<div class="item-content">
<div class="item-wrapper">
<div class="item-icon"></div>
<div class="item-title">Container</div>
</div>
</div>
</div>
<div data-index="0" data-depth="1" class="sortable-item">
<div class="item-content">
<div class="item-indentation"></div>
<div class="item-wrapper">
<div class="item-icon"></div>
<div class="item-title">Paragraph</div>
</div>
</div>
</div>
Thank you again #AlexMA for the tip on Functional Components.

How to display a datatable through an accordion

I encountered some troubles creating an accordion table. I created an Accordion component and a Table component. While independant from each other it works perfectly fine but I can't get any table appear into accordion.
//js part of accordion component
import Table from "../Table/index.vue"
export default {
name: 'accordion',
components: { Table },
mounted () {
},
data(){
return {
items: [
{ id: "mc", title: "Medical Checkup", text: this.components.Table },
{ id: "ac", title: "Application connected", text:
this.components.Table },
{ id: "p", title: "Programs", text: this.components.Table },
{ id: "pl", title: "Pain list", text: this.components.Table }
]
}
}
}
//html part of accordion component
<div>
<div ref="list" class="list">
<transition-group class="flip-list" name="flip-list" ref="tg1"
tag="div">
<div v-for="(item,index) in items" :key="item.id" class="item">
<slot name="item" :class="{active:item.isOpen}" :item="item"
:index="index">
<v-expansion-panel id="exp1">
<v-expansion-panel-content>
<div slot="header" class="drop-target handle2">
<span>{{item.title}}</span>
</div>
<v-card>
<v-card-text>
<div slot="item">{{Table}}</div>
</v-card-text>
</v-card>
</v-expansion-panel-content>
</v-expansion-panel>
</slot>
</div>
</transition-group>
</div>
</div>
So, the point is : how can I make it so that a datatable appears into accordion ? Like when you click aon one of the titles and it appears instead of some text ?
Thanks a lot
Resolved!
In case of (if people encounter the same problem as I had), here is the code (html / js).
//html goes here
<div id="app">
<v-app id="inspire">
<v-data-table :headers="mainHeaders"
:items="mainItems"
item-key="title"
hide-actions
expand
class="elevation-1">
<template slot="items" scope="props">
<tr #click="props.expanded = !props.expanded">
<td class="text-xs">{{ props.item.title }}</td>
</tr>
</template>
<template slot="expand" scope="props">
<v-data-table :headers="subHeaders"
:items="subItems"
item-key="pain"
hide-actions
class="elevation-10">
<template slot="items" scope="props">
<td class="text-xs">{{ props.item.pain }}</td>
<td class="text-xs">{{ props.item.type }}</td>
</template>
</v-data-table>
</template>
</v-data-table> </v-app>
</div>
//js goes here
export default {
name: 'accordion-table',
mounted () {
},
data () {
return {
mainHeaders: [
{ text: 'Medical informations', value: 'title' }
],
mainItems: [
{ title: 'Medical Checkup' },
{ title: 'Application connected' },
{ title: 'Programs' },
{ title: 'Pain List' }
],
subHeaders: [
{ text: 'Pain', value: 'pain' },
{ text: 'Type', value: 'type' }
],
subItems: [
{ pain: 'Knee', type: '1' },
{ pain: 'Ankle', type: '2' },
{ pain: 'Back', type: '3' },
{ pain: 'Neck', type: '4' }
]
}
}
}
(I used random values)
Hope it helps some of you

Categories