I have an issue with a vue component not rendering data - vue.js

I am retrieving data in two steps. All data can be viewed in the Vue dev tools but data retrieved in the second step is not rendered.
My code is as follows:
data() {
return {
activeEmployees: {},
}
},
methods: {
loadEmployees() {
axios.get("/api/organizations/employees/" + this.$route.params.organizationId)
.then(({data}) => (
this.activeEmployees = data.active_employees
))
.then(() => (
this.getUserServices()
));
},
getUserServices() {
for (const element of this.activeEmployees) {
axios.get("/api/organizations/employees/services/" + element.id + "/" + this.$route.params.organizationId)
.then(response => Object.assign(element, response.data));
}
},
},
mounted() {
this.loadEmployees()
}
The data from the getUserServices method (user_services as per screenshot below) is not rendered yet it appears in the Vue dev tools.
Part of the template is as follows:
<tr :key="employee.id" v-for="employee in activeEmployees">
<td class="text-center">
<Avatar :userData="employee"/>
</td>
<td>
<div>{{employee.name}}</div>
<div class="small text-muted">
Registered: {{employee.created_at | formatDate}}
</div>
</td>
<td>
{{employee.user_services}}
</td>
</tr>
And a snapshot of the Vue dev tools is as follows:
The user_services is empty in the view yet available in the dev tools.
How can this be refactored to render all data?

You should use the $set method somewhere to make your data reactive. For example:
axios.get("/api/organizations/employees/services/" + element.id + "/" + this.$route.params.organizationId)
.then(response => {
Object.keys(response.data).forEach(key => this.$set(element, key, response.data[key])
})

Related

Embeded template in vue

I would like to use a local component in VueJS:
My component file (cleaned up a bit):
<template id="heroValuePair">
<td class="inner label">{{label}}</td>
<td class="inner value">{{c}}</td>
<td class="inner value">
{{t}}
<span v-if="c < t" class="more">(+{{t-c}})</span>
<span v-if="c > t" class="less">({{t-c}})</span>
</td>
</template>
<template id="hero">
<table class="hero card" border="0" cellpadding="0" cellspacing="0">
<tr>
<td>other data...</td>
</tr>
<tr>
<hvp label="Label" v-bind:c="current.level" :t="target.level" :key="hero.id"/>
</tr>
</table>
</template>
<script>
var HeroValuePair = {
template: "#heroValuePair",
props: {
label : String,
c : Number,
t : Number
},
created() {
console.log("HVP: "+this.c+" "+this.t);
}
};
Vue.component("Hero", {
template: "#hero",
props: {
heroId : String
},
components: {
"hvp" : HeroValuePair
},
data: () => ({
hero: {},
current: {},
target: {}
}),
computed: {
},
created() {
fetch("/api/hero/"+this.heroId)
.then(res => res.json())
.then(res => {
this.hero = res.hero
this.current = res.current
this.target = res.target
})
}
});
</script>
<style>
</style>
This outer Hero template is used in a list iterator:
<template id="card-list">
<table>
Card list
<div id="">
<div v-for="card in cards" class="entry">
<Hero :hero-id="card.hero.id" :key="card.hero.id"/>
</div>
</div>
</table>
</template>
<script>
Vue.component("card-list", {
template: "#card-list",
data: () => ({
cards: [],
}),
created() {
fetch("/api/cards")
.then(res => res.json())
.then(res => {
this.cards = res.heroes
})
.catch((e) => alert("Error while fetching cards: "+e));
}
});
</script>
<style>
</style>
However, when I render the card list, it only produces the list of the first td in hvp template:
When I comment out the call of hpv the page is rendered correctly with all the HTML code from Hero template.
I tried to figure out what step I left out, but can't find the clue.
One last info: I used JavalinVue to support the server side, not nodejs-based Vue CLI. I don't know if it has any impact, but may be important.
UPDATE 1
After IVO GELOV spot the problem with multiple root tags, and because I can't move to Vue3, I tried to make it as a functional template, as he suggested. I removed the template and created the render function:
var HeroValuePair = {
template: "#heroValuePair",
functional: true,
props: {
label : String,
c : Number,
t : Number
},
render(createElement, context) {
console.log("HVP: "+context.props.c+" "+context.props.t);
if (typeof context.props.c === undefined) return createElement("td" )
else return [
createElement("td", context.props.label ),
createElement("td", context.props.c ),
createElement("td", context.props.t )
]
}
}
Although the console indicated the render is called correctly, the result is the same: there is neither the rendered nodes, nor the parent Hero component displayed. I tried to move into different file, tried the functional template format, but none worked.

Multiple v-for loops to display cities within countries?

I am using vue.js and axios within visualstudiocode I'm very new at coding in principal and so I'm sorry if my question is worded poorly. I have two arrays I have assembled from this API:
'https://docs.openaq.org/#api-_'
One is a list of countries, with data of how many cities is in the region etc, and another is a list of cities within those countries with corresponding data. I display a list of countries, in a table followed by a corresponding button in the same row using a v-for loop.
What I want to happen, is when this button is clicked for each corresponding country in the array, all the cities within that country will be displayed. I've been told this needs seperate v-for loops, but I don't know how I'd write it out.
<template>
<div>
<table>
<tr>
<th>Countries</th>
<th>Cities</th>
</tr>
<tr>
<td>
<ul>
<li v-for="countries in listofcountries" :key="countries.name">
{{countries.name}}
</li>
</ul>
</td>
<ul v-for="countries in listofcountries" :key="countries.name">
<li v-for="cities in lifeofcities" :key="cities.city"></li>
<button>{{countries.name}}</button>
</li>
</ul>
</tr>
</table>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
data () {
return {
listofcountries: [],
listofcities: [],
}
},
mounted () {
const axios = require('axios');
var self = this;
// Make a request for a user with a given ID
axios.get('https://api.openaq.org/v1/cities')
.then(function (response) {
// handle success
console.log(response.data.results);
self.listofcities=response.data.results;
})
.catch(function (error) {
// handle error
console.log(error);
})
.finally(function () {
// always executed
});
axios.get('https://api.openaq.org/v1/countries')
.then(function (response) {
// handle success
console.log(response.data.results) ;
self.listofcountries=response.data.results;
})
.catch(function (error) {
// handle error
console.log(error);
})
.finally(function () {
// always executed
});
},
methods: {
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style>
ul {
list-style-type: none;
}
table,th,td,tr {
border: 1px solid black;
}
</style>
I'm not completely certain this output is what you want but it's a place to start:
<template>
<table>
<tr>
<th>Countries</th>
<th>Cities</th>
</tr>
<tr v-for="country in countries" :key="country.key">
<td>{{ country.name }}</td>
<td v-if="areCitiesVisible(country.code)">{{ citiesInCountry(country.code) }}</td>
<td v-else>
<button #click="onClickCountry(country.code)">show cities</button>
</td>
</tr>
</table>
</template>
<script>
import axios from 'axios';
export default {
data() {
return {
cities: [],
countries: [],
countriesWithVisibleCities: [],
};
},
mounted() {
axios
.get('https://api.openaq.org/v1/cities')
.then(response => (this.cities = response.data.results))
.catch(console.log);
axios
.get('https://api.openaq.org/v1/countries')
.then(response => (this.countries = response.data.results))
.catch(console.log);
},
methods: {
citiesInCountry(code) {
return this.cities
.filter(c => c.country === code)
.map(c => c.city)
.join(', ');
},
onClickCountry(code) {
this.countriesWithVisibleCities.push(code);
},
areCitiesVisible(code) {
return this.countriesWithVisibleCities.includes(code);
},
},
};
</script>
It renders a table with one row per country. Each row has a name and a button. If you press the button, you'll get a list of cities instead of the button. Note that the API call seems to return only the cities for the first few counties in the list.
Other notes:
Your original post had several typos that could have been caught with eslint. I recommend installing it along with prettier. I promise that will save you a lot of time in the future.
I simplified the axios calls - self isn't necessary here.

Vue.js/Axios - Duplicate results in list. Has unique-keys in v-for

I have two other uses of v-for in separate components. They also sometimes throw errors. All three v-for invocations are wrapped with v-if/else. Here is the code that produces duplicate key errors & renders data twice:
AccountDashboard.vue
<tbody>
<tr v-if="!residents.length" class="table-info">
<td class="text-center">
<p>
No residents on record.
</p>
</td>
</tr>
<template v-else>
<tr is="AccountResidentList"
v-for="resident in residents"
v-bind:key="'resident-list-' + resident.id"
v-bind:first_name="resident.first_name"
v-bind:last_name="resident.last_name"
v-bind:dob="resident.dob | date_formatted"
>
</tr>
</template>
</tbody>
Note the unique id attempt in the binding of key.
Here is a look at the child component
ProviderAccountList.vue
<template>
<tr class="AccountResidentList">
<td>
{{ this.$attrs.id }}
</td>
<td>
{{ this.$attrs.first_name }} {{ this.$attrs.last_name }}
</td>
<td>
{{ this.$attrs.dob }}
</td>
<td>
<button #click="toResidentProfile({account_id, id})" class="btn btn-sm btn-purple btn-with-icon">
<div class="ht-25">
<span class="icon wd-25"><i class="fa fa-eye"></i></span>
<span class="pd-x-10">view</span>
</div>
</button>
</td>
<!--TODO: Add view profile button-->
</tr>
</template>
<script>
import Axios from "axios";
import router from "../../router";
import { mapGetters } from "vuex";
import moment from "moment";
export default {
name: "AccountResidentList",
computed: {
...mapGetters['Resident', {
resident: 'getResident'
}]
},
filters: {
date_formatted: (date) => {
return moment(date).format('MMMM Do, YYYY');
}
},
methods: {
toResidentProfile(account_id, resident_id) {
router.push(`/accounts/${account_id}/residents/${resident_id}`)
}
},
};
</script>
<style scoped></style>
My Axios call looks like:
Account.js (a namespaced vuex-module)
async retrieveAccount(context, account_id) {
// Axios.defaults.headers.common['Authorization'] = 'Bearer ' + window.$cookies.get('jwt')
let response
let valid_id = window.$cookies.get('valid_id');
response = await Axios.get(`http://localhost:3000/api/v1/providers/${valid_id}/accounts/${account_id}`, { headers: { 'Authorization': 'Bearer ' + window.$cookies.get('jwt') } })
.then((response) => {
let account = response.data.locals.account;
let account_address = response.data.locals.account_address;
let residents = response.data.locals.residents;
// set Account
context.dispatch('Account/setId', account.id, {root: true});
context.dispatch('Account/setProviderId', account.provider_id, {root: true});
.
.
.
// set AccountAddress
// !Array.isArray(array) || !array.length
if (account.address) {
context.dispatch('Account/setAddressId', account_address.id, {root: true});
context.dispatch('Address/setId', account_address.id, {root: true});
.
.
.
// set AccountResidents
// !Array.isArray(array) || !array.length
residents.forEach(resident => {
if (resident) {
// Add object to parent's list
context.dispatch('Account/setResidents', resident, {root: true}); // Set attr values for object
context.dispatch('Resident/setId', resident.id, {root: true});
.
.
.
(remaining attrs removed for brevity)
}
})
router.push(`/providers/${account.provider_id}/accounts/${account_id}`);
})
.catch(function(error) {
console.log(error);
})
Note: the Account action #setResidents simply calls the mutator that adds one resident to a list total.
i.e state.list.push(resident)
I logged the response to the console and can confirm that the data isn't being sent twice (or more) from my Axios call.
I have reviewed & attempted the following to no avail:
https://alligator.io/vuejs/iterating-v-for/
https://www.reddit.com/r/vuejs/comments/7n3zi4/vue_warn_duplicate_keys_detected_vfor_with/
https://github.com/hejianxian/vddl/issues/23
https://github.com/hejianxian/vddl#warning
https://medium.com/#chiatsai/vue-js-common-issue-duplicate-keys-stops-components-rendering-df415f31838e
Finally, It should be mentioned that I have tried variations of using/not using template to wrap the list, including/not including the for loop in the template, etc..
Did not anticipate it would be this bothersome to iterate a collection.
Am I overlooking something obvious?
Update: What worked for me
I needed access to the resident.id also the id declared in the paren seems like an index. So here is a look at what removed the duplicate render errors and allow me access to the resident's id even after fixing the duplicate keys error:
<template v-else>
<tr is="AccountResidentList"
v-for="(resident, id) in residents"
v-bind:key="id"
v-bind:id="resident.id"
v-bind:first_name="resident.first_name"
v-bind:last_name="resident.last_name"
v-bind:dob="resident.dob | date_formatted"
>
</tr>
</template>
Thanks again #Billal Begueradj for the assist!
For me, I suspect that in residents there are entries which have the same id. So we have to find out a way to overcome this issue. We can give it an efficient try as follows:
<tr
is="AccountResidentList"
v-for="(resident, id) in residents"
:key="id"
// rest of your code

Update data after change without page refreshing using vue.js

I wrote such code:
<template>
<div class="home">
<HelloWorld tableTitle="Goods:">
<table class="table table-striped">
<tbody>
<tr v-for="(i, index) in items.data" :key="index">
<td>{{ i.id }}</td>
<td>{{ i.name }}</td>
<td>{{ i.producer }}</td>
<td><font-awesome-icon v-if="i.received" icon="check" /><font-awesome-icon v-else icon="times" /><td>
</tr>
</tbody>
</table>
</div>
</template>
<script>
import HelloWorld from "#/components/HelloWorld.vue";
import axios from "axios";
export default {
name: "home",
components: {
HelloWorld
},
data: () => ({
items: {},
errors: []
}),
beforeMount() {
axios
.get("http://40.115.119.247/AgileSelection/tnt/status")
.then(response => {
this.items = response.data;
console.log(this.items);
})
.catch(e => {
this.error.push(e);
});
}
};
<script>
Now, for refreshing information I just use the refresh button.
What and where should I add some lines of code to update information but without refreshing the page? Because, I am updating data every 5 seconds. So I think that manually updating is not so good idea.
do something like this
// data property
data () {
return {
...
interval: null
}
},
// in your created hook
created () {
this.interval = setInterval(this.refreshData, 5000)
},
beforeDestroy () {
clearInterval(this.interval)
},
// in methods property
methods: {
refreshData () {
// fetch data
axios.get("http://40.115.119.247/AgileSelection/tnt/status")
.then(response => {
this.items = response.data
})
}
}
this will fetch your data from your API and update the list automatically. this will update your UI as well.
you can try using location.reload() after the code where you register the update
for example
handleSubmit() {
this.registerServices(this.organization)
location.reload();
}

Laravel echo vue js here method

i have a problem with here method vuejs, laravel echo.
broadcast channel
Broadcast::channel('task.*', function ($user, $taskId) {
$task = Task::find($taskId);
if ($task)
{
return ['id' => $user->id, 'name' => $user->name];
}
});
blade template
<task-show inline-template :task="{{ json_encode($task) }}" :user-id="{{ auth()->user()->id }}" >
<ul>
<li v-for="viewer in viewersExceptMe">
#{{ viewer['name'] }}
</li>
</ul>
<div>
<table class="table table-bordered table-hover">
<tbody>
<tr>
<td>Task Name</td>
<td>#{{ task['name'] }}</td>
</tr>
</tbody>
</table>
</div>
</task-show>
My script
<script>
export default{
props : {
task : Object,
userId : Number
},
data : function () {
return {
viewers : []
}
},
ready : function(){
this.listen();
},
computed: {
viewersExceptMe(){
return _.reject(this.viewers, viewer => viewer.id == this.userId);
}
},
methods : {
listen(){
Echo.join('task.' + this.task.id)
.here(viewers => {
this.viewers = viewers;
});
}
}
}
</script>
In image below, the here method doesn't works, the viewers variable is not updated when user leaves the info page
Joining Presence Channels
To join a presence channel, you may use Echo's join method. The join method will return a PresenceChannel implementation which, along with exposing the listen method, allows you to subscribe to the here, joining, and leaving events.
Echo.join(`chat.${roomId}`)
.here((users) => {
//
})
.joining((user) => {
console.log(user.name);
})
.leaving((user) => {
console.log(user.name);
});
The here callback will be executed immediately once the channel is
joined successfully
The joining method will be executed when a new
user joins a channel
The leaving method will be executed when a user
leaves the channel
In you current situation this method should be like this,
Echo.join('task.' + this.task.id)
.here(viewers => {
this.viewers = viewers;
})
.joining((user) => {
this.viewers.push(user)
})
.leaving((user) => {
this.viewers = _.reject(this.viewers, viewer => viewer.id == user.id);
});
Reference: Laracast discuss , Laravel Documentation for broadcasting