I'm currently using vue-router to manage the differents Vue of my project.
My main.js
import Vue from 'vue'
import App from './App.vue'
import jQuery from 'jquery'
import 'bootstrap'
import 'bootstrap/dist/css/bootstrap.css'
global.jQuery = jQuery
global.$ = jQuery
import './assets/css/animate.css'
import router from './router'
import store from './vuex'
Vue.config.productionTip = false
new Vue({
store,
router,
render: h => h(App)
}).$mount('#app')
When I'm on my dashboard ('/dashboard') for the first time, the 'created' methods is called. Data are retrieved from the API and shows up in my array.
After that I click on one element of my array that route me to '/details/:id' (with id the id of my element). Everything works well and then I click on a 'Go back' button.
I finish again on my dashboard page, i see that the 'create' methods is called again, data are well retrived from the API but nothing shows up and my array stays empty.
I really don't understand why.
There is the code of the the 'created' function:
export default {
created: function() {
console.log('created => dashboard');
let store = this.$store;
let q = this.rows;
//get rows
if (store.state.socket.io._callbacks["$rows"] == undefined) {
console.log("Binding rows");
//Where I receive the rows from API
store.state.socket.io.on("rows", data => {
console.log("rows reponse:", data);
if (data.success) {
this.nbrItems = data.rows.length;
q.splice(0, q.length); //Clean the array without replacing the instance
data.rows.map(a => q.push(a));
console.log("Queue length: " + q.length);
}
});
}
//get the queue
this.refresh(); // This send a request to the API to ask it to send us back the datas
},
And I use this.$router.go(-1) to navigate back on the '/dashboard' page.
Edit: Is there a problem of state or something like that? I do not understand why, because in-memory I can access to all data, there is just no more binding anymore...
Do you pop every element of the array before you call the created function?
I'm still an apprentice but it seems to me like you have to pop everything before adding new elements to the array.
I figure it out:
The problem was coming from socket.io. I'm checking if the event is bind already before to subscribe to a function and this function contains 'this' that was still referring to the previous Vue instance.
Simply fixed by replacing this:
//get rows
if (store.state.socket.io._callbacks["$rows"] == undefined) {
console.log("Binding rows");
//Where I receive the rows from API
store.state.socket.io.on("rows", data => {
console.log("rows reponse:", data);
if (data.success) {
this.nbrItems = data.rows.length;
q.splice(0, q.length); //Clean the array without replacing the instance
data.rows.map(a => q.push(a));
console.log("Queue length: " + q.length);
}
});
}
by this:
if (store.state.socket.io._callbacks["$rows"] != undefined) {
store.state.socket.io.off("rows");
}
console.log("Binding rows");
store.state.socket.io.on("rows", data => {
console.log("rows reponse:", data);
if (data.success) {
this.nbrItems = data.rows.length;
q.splice(0, q.length);
data.rows.map(a => q.push(a));
console.log("Queue length: " + q.length);
}
});
But this makes me wonder, if I can still access to a previous Vue instance is it meaning that it will be some kind of memory leak with time?
I suppose that no with the garbage collector but would mean that nothing else refers to the previous instance.
Related
I was using VueJS in browser mode and am now trying to switch my code to a VueJS SPA and vue-router. I've been stuck for hours with a $refs not working anymore.
To interact with my Google Charts, I was using an absolute reference to the graph (this.$refs.villesChart) to get selected data like that:
computed: {
eventsprox() {
let eventsprox = {
select: () => {
var selection = "";
if (this.$refs.villesChart) selection = this.$refs.villesChart1.chartObject.getSelection();
if (selection.length) {
var row = selection0[0].row + 1;
this.code_commune = this.dataprox[row][4];
this.changerville(this.code_commune, this.dataprox[row][0]);
}
return false;
},
};
return eventsprox;
}
HTML code for graph:
<GChart type="BarChart" id="villesChart" ref="villesChart" :data="dataprox" :options="optionsprox" :events="eventsprox"/>
I don't know why, but in browser mode, this.$refs.villesChart is a component:
[1]: https://i.stack.imgur.com/xJ8pV.png
but now it is a proxy object, and lost its chartObject attribute:
[2]: https://i.stack.imgur.com/JyXrL.png
I'm really confused. Do you have an idea why?
And if I use the proxy object, then I get a Vue warning "Avoid app logic that relies on enumerating keys on a component instance" and it is not working in production environment.
Thanks a lot for your help!!
After hours of testing different solutions, I finally found a solution working with Vue3 and Vue-google-chart 1.1.0.
I got rid of "refs" and put the events definition and code in the data section of my Vue 3 app (instead of computed) and accessed the chart data through a component variable I used to populate it.
Here is my event code where this.dataprox is my data table for the chart:
eventsprox: {
'click': (e) => {
const barselect = parseInt(e.targetID.split('#')[2]) + 1;
this.code_commune = this.dataprox[barselect][4];
this.nom_commune = this.dataprox[barselect][0];
this.changerville(this.code_commune, this.nom_commune);
}
},
My Gchart html code:
<GChart type="AreaChart" :data="datag" :options="optionsg" :events="eventsprox"/>
I hope it can help!
I'm using petite-vue as I need to do very basic UI updates in a webpage and have been drawn to its filesize and simplicity. I'd like to control the UI's state of visible / invisible DOM elements and class names and styles of various elements.
I have multiple JavaScript files in my app, I'd like to be able to make these changes from any of them.
In Vue JS it was possible to do things like this...
const vueApp = new Vue({ el: "#vue-app", data(){
return { count: 1}
}})
setTimeout(() => { vueApp.count = 2 }, 1000)
I'm trying the same with Petite Vue but it does nothing.
// Petite Vue
const petiteVueApp = PetiteVue.createApp({
count: 0,
}).mount("#petite-vue");
setTimeout(() => { petiteVueApp.count = 2 }, 1000);
Logging the app gives just a directive and mount attribute, I can't find the count (nb if you log the above app it will show the count, because of that line petiteVueApp.count = 2, that isn't the data)
Demo:
https://codepen.io/EightArmsHQ/pen/YzemBVB
Can anyone shed any light on this?
There is an example which does exactly this in the docs which I overlooked.
https://github.com/vuejs/petite-vue#global-state-management
It requires an import of the #vue/reactivity which can be imported from the petite-vue bundle.
import { createApp, reactive } from 'https://unpkg.com/petite-vue?module'
setTimeout(() => { vueApp.count = 2 }, 1000)
const store = reactive({
count: 0,
inc() {
this.count++
}
})
createApp({
store
}).mount("#petite-vue")
setTimeout(() => { store.count = 2 }, 1000);
Updated working example:
https://codepen.io/EightArmsHQ/pen/ExEYYXQ
Interesting. Looking at the source code it seems that we would want it to return ctx.scope instead of return this.
Your workaround seems like the best choice if using petite-vue as given, or you could fork petite-vue and change that one line (I haven't tested this).
I'm developing a helpdesk tool in which I have a kanban view.
I previously used nested serializers in my backend and I managed to have everything working with a single query but it's not scalable (and it was ugly) so I switched to another schema :
I query my helpdesk team ('test' in the screenshot)
I query the stages of that team ('new', 'in progress')
I query tickets for each stage in stages
So when I mount my component, I do the following :
async mounted () {
if (this.helpdeskTeamId) {
await this.getTeam(this.helpdeskTeamId)
if (this.team) {
await this.getTeamStages(this.helpdeskTeamId)
if (this.stages) {
for (let stage of this.stages) {
await this.getStageTickets(stage)
}
}
}
}
},
where getTeam, getTeamStages and getStageTickets are :
async getTeam (teamId) {
this.team = await HelpdeskTeamService.getTeam(teamId)
},
async getTeamStages (teamId) {
this.stages = await HelpdeskTeamService.getTeamStages(teamId)
for (let stage of this.stages) {
this.$set(stage, 'tickets', [])
}
},
async getStageTickets (stage) {
const tickets = await HelpdeskTeamService.getTeamStageTickets(this.helpdeskTeamId, stage.id)
// tried many things here below but nothing worked.
// stage.tickets = stage.tickets.splice(0, 0, tickets)
// Even if I try to only put one :
// this.$set(this.stages[this.stages.indexOf(stage)].tickets, 0, tickets[0])
// I see it in the data but It doesn't appear in the view...
// Even replacing the whole stage with its tickets :
// stage.tickets = tickets
// this.stages.splice(this.stages.indexOf(stage), 1, stage)
},
In getTeamStages I add an attribute 'tickets' to every stage to an empty list. The problem is when I query all the tickets for every stage. I know how to insert a single object in an array with splice or how to delete one object from an array but I don't know how to assign a whole array to an attribute of an object that is in an array while triggering the Vue reactivity. Here I'd like to put all the tickets (which is a list), to stage.tickets.
Is it possible to achieve this ?
If not, what is the correct design to achieve something similar ?
Thanks in advance !
EDIT:
It turns out that there was an error generated by the template part. I didn't think it was the root cause since a part of the view was rendered. I thought that it would have prevent the whole view from being rendered if it was the case. But finally, in my template I had a part doing stage.tickets.length which was working when using a single query to populate my view. When making my API more granular and querying tickets independently from stages, there is a moment when stage has no tickets attribute until I set it manually with this.$set(stage, 'tickets', []). Because of that, the template stops rendering and raises an issue. But the ways of updating my stage.tickets would have worked without that template issue.
I could update the stages reactively. Here is my full code; I used the push method of an array object and it works:
<template>
<div>
<li v-for="item in stages" :key="item.stageId">
{{ item }}
</li>
</div>
</template>
<script>
export default {
data() {
return {
stages: [],
};
},
methods: {
async getTeamStages() {
this.stages = [{ stageId: 1 }, { stageId: 2 }];
for (let stage of this.stages) {
this.$set(stage, "tickets", []);
}
for (let stage of this.stages) {
await this.getStageTickets(stage);
}
},
async getStageTickets(stage) {
const tickets = ["a", "b", "c"];
for (let ticket of tickets) {
this.stages[this.stages.indexOf(stage)].tickets.push(ticket);
}
},
},
mounted() {
this.getTeamStages();
},
};
</script>
It should be noted that I used the concat method of an array object and also works:
this.stages[this.stages.indexOf(stage)].tickets = this.stages[this.stages.indexOf(stage)].tickets.concat(tickets);
I tried your approaches some of them work correctly:
NOT WORKED
this.$set(this.stages[this.stages.indexOf(stage)].tickets, tickets)
WORKED
this.$set(this.stages[this.stages.indexOf(stage)].tickets, 0, tickets[0]);
WORKED
stage.tickets = tickets
this.stages.splice(this.stages.indexOf(stage), 1, stage)
I'm sure it is XY problem..
A possible solution would be to watch the selected team and load the values from there. You seem to be loading everything from the mounted() hook, and I suspect this won't actually load all the content on demand as you'd expect.
I managed to make it work here without needing to resort to $set magic, just the pure old traditional vue magic. Vue will notice the properties of new objects and automatically make then reactive, so if you assign to them later, everything will respond accordingly.
My setup was something like this (showing just the relevant parts) -- typing from memory here, beware of typos:
data(){
teams: [],
teamId: null,
team: null
},
watch:{
teamId(v){
this.refreshTeam(v)
}
},
methods: {
async refreshTeam(id){
let team = await fetchTeam(id)
if(!team) return
//here, vue will auomaticlly make this.team.stages reactive
this.team = {stages:[], ...team}
let stages = await fetchStages(team.id)
if(!stages) return
//since this.team.stages is reactive, vue will update reactivelly
//turning the {tickets} property of each stage reactive also
this.team.stages = stages.map(v => ({tickets:[], ...v}))
for(let stage of this.team.stages){
let tickets = await fetchTickets(stage.id)
if(!tickets) continue
//since tickets is reactive, vue will update it accordingly
stage.tickets = tickets
}
}
},
async mounted(){
this.teams = fetchTeams()
}
Notice that my 'fetchXXX' methods would just return the data retrieved from the server, without trying to actually set the component data
Edit: typos
In a component in a Vue app the following method runs after a user clicks a Submit button on a form:
execute() {
let message = '';
let type = '';
const response = this.actionMode == 'create' ? this.createResource() : this.updateResource(this.data.accountId);
response.then(() => {
message = 'Account ' + this.actionMode + 'd for ' + this.data.name;
type = 'is-success';
})
.catch(e => {
message = 'Account <i>NOT</i> ' + this.actionMode + 'd<br>' + e.message;
type = 'is-danger';
})
.then(() => {
this.displayOutcome(message, type);
this.closeModal();
});
}
The displayOutcome() method in that same component looks like this:
displayOutcome(message, type) {
this.$buefy.toast.open({
duration: type == 'is-danger' ? 10000 : 3500,
position: 'is-bottom',
message: message,
type: type
});
}
The code is working fine within the component. Now I'm trying to move the displayOutcome() method into a helpers.js file and export that function so any component in the app can import it. This would centralize maintenance of the toast and prevent writing individual toasts within each component that needs one. Anyhow, when displayOutcome() gets moved over to helpers.js, then imported into the component an error appears in the console when the function is triggered:
I suspect it has to do with referring to the Vue instance so I experimented with the main.js file and changed this
new Vue({
router,
render: h => h(App),
}).$mount('#app');
to this
var vm = new Vue({
router,
render: h => h(App),
}).$mount('#app');
then in helpers.js
export function displayOutcome(message, type) {
// this.$buefy.toast.open({
vm.$buefy.toast.open({
duration: type == 'is-danger' ? 10000 : 3500,
position: 'is-bottom',
message: message,
type: type
});
}
but that resulted in a "Failed to compile." error message.
Is it possible to make displayOutcome() in helpers.js work somehow?
displayOutcome() requres a reference to this to work, which is fine if you define it as a method on your component object (the standard way). When you define it externally however, you just supply any function instead of a method, which is a function "targeted" on an object. This "targeting" is done through this. So when you're passing a simple function from an external file, there's no association to a specific object, and thus no this available.
To overcome this, you can use displayOutcome.apply(thisArg, methodArgs), where thisArg will be whatever is the this reference in your function, and methodArgs are the remaining arguments that are being passed to the function.
So displayOutcome.apply(4, ['some', 'thing']) would imply that the this reference in displayOutcome() becomes 4 in this case.
Further reading:
Understanding "This" in JavaScript
this on MDN
import { displayOutcome } from './component-utils'
// when calling displayOutcome() from within your component
displayOutcome.apply(this, ['Hello World', 'is-info'])
// component-utils.js
export function displayOutcome(message, type) {
// this.$buefy.toast.open({
this.$buefy.toast.open({
duration: type == 'is-danger' ? 10000 : 3500,
position: 'is-bottom',
message: message,
type: type
});
}
You can create an action in Vuex store and dispatch it from any component.
Currently I am working on a new UI for a legacy API. Unfortunately, this one delivers HTML source code for a column header. This code usually creates a FontAwesome icon. This library will not be used in the new project.
I found a very similar icon in the Icon Library of CoreUI. Now it is only a matter of rendering the icon at this point. However, no approach has been successful so far. How can I replace the icon in the headerCellTemplate method?
Or maybe there is a completely different, much better approach to do this. I don't know if I am on the right track with this method approach. You can probably use static templates, but I don't know how to do that.
import { CIcon } from '#coreui/vue';
import { cilCheckCircle } from '#coreui/icons';
headerCellTemplate: (element, info) => {
element.innerHTML = curr.ColumnTitle;
if (element.firstChild.nodeName === 'I') {
// WORKS
//element.firstChild.innerHTML = 'Done';
// ANOTHER EXPERIMENT
//const componentClass = Vue.extend(cilCheckCircle);
//const instance = new componentClass();
//instance.$mount();
//element.removeChild(element.firstChild);
//element.appendChild(instance.$el);
// ALSO NOT WORKING
return CIcon.render.call(this, cilCheckCircle);
}
}
I finally found a solution after revisiting this interesting article.
import Vue from 'vue';
import { CIcon } from '#coreui/vue';
import { cilCheckCircle } from '#coreui/icons';
headerCellTemplate: (element, info) => {
element.innerHTML = curr.ColumnTitle;
if (element.firstChild.nodeName === 'I') {
const cIconClass = Vue.extend(CIcon);
const instance = new cIconClass({
propsData: { content: cilCheckCircle }
});
instance.$mount(element.firstChild);
}
}
I don't know, though, if this is the ideal solution. So feel free to tell me, if you have a better, less complex solution.