Vuejs DOM doesn't update after fetching data - vue.js

I have bound an array events to a component tag <scheduler> containing events to fill in a scheduler app (dhtmlx scheduler). However, the DOM doesn't seeem to refresh itself when data is retrieved by the getEvents methods triggered when vue instance is created.
There is 2 vue files I work with: App.vue containing the main app component and the Scheduler.vue file containing the scheduler component.
The thing is that when I modify something in the Scheduler.vue file and save it, it correctly take the updated events array into account.
Scheduler parse the data in the events prop when DOM is mounted in scheduler component.
Therefore is there something I can do to get the updated array ?
Here is the App.vue:
<template>
<div id="app">
<div class="container">
<scheduler v-bind:events="events"></scheduler>
</div>
</div>
</template>
<script>
import Scheduler from './components/Scheduler.vue';
import auth from './components/auth/index'
import data from './components/data/index'
export default {
name: 'app',
components: {
Scheduler
},
data() {
return {
events: []
}
},
created() {
this.getEvents();
},
watch: {
events: function(value) {
console.log('updated');
}
},
methods: {
async getEvents() {
try {
const token = await auth.getToken(this);
const thattoken = await auth.getThatToken(this, token);
await data.getOgustData(this, token, '/calendar/events', 307310564, this.events);
} catch (e) {
console.log(e);
}
},
}
}
</script>
Here is Scheduler.vue:
<template lang="html">
<div ref="scheduler_here" class="dhx_cal_container" style='width:100%; height:700px;'>
<div class="dhx_cal_navline">
<div class="dhx_cal_prev_button"> </div>
<div class="dhx_cal_next_button"> </div>
<div class="dhx_cal_today_button"></div>
<div class="dhx_cal_date"></div>
<div class="dhx_cal_tab" name="day_tab" style="right:204px;"></div>
<div class="dhx_cal_tab" name="week_tab" style="right:140px;"></div>
<div class="dhx_cal_tab" name="month_tab" style="right:76px;"></div>
</div>
<div class="dhx_cal_header"></div>
<div class="dhx_cal_data"></div>
</div>
</template>
<script>
import 'dhtmlx-scheduler'
import 'dhtmlx-scheduler/codebase/locale/locale_fr';
import 'dhtmlx-scheduler/codebase/ext/dhtmlxscheduler_readonly.js';
export default {
name: 'scheduler',
props: {
events: {
type: Array,
default () {
return [{
id: '',
text: '',
start_date: '',
end_date: '',
}]
}
}
},
mounted() {
scheduler.config.xml_date = '%Y-%m-%d %H:%i';
// disable left buttons on lightbox
scheduler.config.buttons_left = [];
// enable cancel button on lightbox's right wing
scheduler.config.buttons_right = ['dhx_cancel_btn'];
// changing cancel button label
scheduler.locale.labels['icon_cancel'] = 'Fermer';
// hide lightbox in month view
scheduler.config.readonly_form = true;
// hide select bar in day and week views
scheduler.config.select = false;
scheduler.config.lightbox.sections = [
{
name: "description",
height: 20,
map_to: "text",
type: "textarea",
focus: true
}
];
scheduler.init(this.$refs.scheduler_here, new Date(), 'month');
scheduler.parse(this.$props.events, 'json');
},
}
</script>
<style lang="css" scoped>
#import "~dhtmlx-scheduler/codebase/dhtmlxscheduler.css";
</style>

getOgustData can't populate events in a way that Vue can observe. Since you're passing it as an argument, the array itself can be updated, but it's not a reactive array. Try
var newEvents;
await data.getOgustData(this, token, '/calendar/events', 307310564, newEvents);
this.events = newEvents;
Assigning to this.events is something Vue can notice.

Problem is solved. The issue didn't come from Vue but rather from the dhtmlx scheduler which wasn't parsing events when events was updated.
I ended up watching for any changes to events and thus, parsing it when it updates.
Thanks again for the help provided.
App.vue :
<template>
<div id="app">
<div class="container">
<scheduler v-bind:events="events"></scheduler>
</div>
</div>
</template>
<script>
import Scheduler from './components/Scheduler.vue';
import auth from './components/auth/index'
import data from './components/data/index'
import 'dhtmlx-scheduler'
export default {
name: 'app',
components: {
Scheduler
},
data() {
return {
events: []
}
},
created() {
this.getEvents();
},
watch: {
events: function(value) {
scheduler.parse(this.events, 'json');
}
},
methods: {
async getEvents() {
const token = await auth.getToken(this);
const apiToken = await auth.getApiToken(this, token);
this.events = await data.getApiData(this, apiToken, '/calendar/events', 307310564, this.events);
}
},
}
</script>

Related

Passing data from child component to parent and then to another child not working on page load but works after minor potentially unrelated change

I am new to Vuejs and come across this bug which I have no idea what I have done wrong. I am not receiving any console errors. It doesn't work on initial page load but it seems to work after I comment something out (or make a minor change). It will still then continue to work if I reverse the changes I just made and put it back to the original code. But once again on a fresh page load it won't work.
The issue: I am making a to do list and on page load when I add new tasks through the input field, the list does not appear on the page like it should be. I also console log the data array for this and it shows it is getting added to the array but is not getting rendered to the page. No console errors. In my code I will comment out some other data property (there are 2 additional ones below todosList in the TodoList.vue file that are currently not being used yet) and save and then the tasks will automatically appear on the page. So I think oh ok that might be the issue so with this new minor change I decide to refresh the page to see if it works as expected. Nope it doesn't so I then uncomment out what I previously commented out and save and the list appears again. But once again if I refresh the page it doesn't work. It only seems to be if I make a change inside the data function in the TodoList.vue file.
Additional info: The data is stored in the parent todos[] (App.vue), updated/pushed to array in a child (TodoCreate.vue) and sent back to the parent using $emit. This data is then sent through to another child (TodoList.vue) using props so that it can be rendered on the page.
Wondering if there is something that is not quite right in my code which is causing this to bug out like that. I will include everything in case it is something that looks unrelated to me but could be causing it.
Here is also a link to a code sandbox where the issue can be replicated by following the instructions on the page https://codesandbox.io/s/adding-new-todo-not-working-properly-jwwex?file=/src/components/TodoList.vue
main.js
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
App.vue
<template>
<div :class="currentMode">
<the-header #modeToggled="updateMode($event)"></the-header>
<main>
<todo-create #addedTodos="updateTodos"></todo-create>
<todo-list :todos="todos"></todo-list>
</main>
</div>
</template>
<script>
import TheHeader from './components/TheHeader.vue';
import TodoCreate from './components/TodoCreate.vue';
import TodoList from './components/TodoList.vue';
export default {
name: 'App',
components: {
TheHeader,
TodoCreate,
TodoList,
},
data() {
return {
currentMode: {
dark_mode: true,
light_mode: false
},
todos: [],
}
},
methods: {
updateMode(mode) {
this.currentMode = mode;
},
updateTodos(data) {
this.todos = data;
console.log(this.todos);
},
toggleCompleted() {
}
},
// provide() {
// return {
// todos: this.todos,
// };
// }
}
</script>
TheHeader.vue
<template>
<h1>To-do App</h1>
<div>
<label for="toggle-mode" aria-label="Toggle light and dark mode"></label>
<input type="checkbox" id="toggle-mode" #change="toggleMode">
</div>
</template>
<script>
export default {
emits: ['modeToggled'],
data() {
return {
toggleState: false,
}
},
methods: {
toggleMode() {
this.toggleState = !this.toggleState;
this.$emit('modeToggled', this.modeClasses);
}
},
computed: {
modeClasses() {
return {
dark_mode: !this.toggleState,
light_mode: this.toggleState
}
}
}
}
</script>
TodoCreate.vue
<template>
<div>
<label for="newtodo" class="sr-only">Create new to do</label>
<input type="text" id="newtodo" placeholder="Create a new todo..." v-model="todoval" v-on:keyup.enter="addTodo" >
</div>
</template>
<script>
export default {
emits: ['addedTodos'],
data() {
return {
todoval: '',
taskNumber: 0,
todos: [],
};
},
methods: {
addTodo() {
const val = this.todoval;
const taskNumber = this.taskNumber;
this.todos.push({ taskID: taskNumber, value: val, complete : 'not-completed'});
this.todoval = '';
this.taskNumber++;
console.log(this.todos);
this.$emit('addedTodos', this.todos);
},
}
}
</script>
TodoList.vue
<template>
<ul class="todo-items" :class="filterClass">
<li class="drop-zone" v-for="(listItem, index) in todosList" :class="listItem.complete" :key="listItem.taskID"
#drop='onDrop($event, index)'
#dragover.prevent
#dragenter.prevent>
<div class="drag-el" draggable="true"
#dragstart='startDrag($event, index)'>
<label :for="'checkbox-'+index" :aria-label="'Mark task ' + listItem.value + ' as completed'"></label>
<input type="checkbox" :id="'checkbox-'+index" #change="toggleCompleted(index, listItem.value, listItem.complete, listItem.taskID)">
<input type="text" disabled :value="listItem.value">
<img src="../assets/icon-cross.svg" #click="removeTask(index)">
</div>
</li>
</ul>
</template>
<script>
export default {
props: {
todos: Object,
filterClass: String
},
// inject: ['todos'],
data() {
return {
todosList: this.todos,
// completedTodos: [],
// activeTodos: [],
};
},
// watch: {
// todosList(data) {
// data.filter(function(todo) {
// if(todo.completed == 'completed') {
// completedTodos.push(todos);
// }
// });
// }
// },
methods: {
startDrag: (evt, item) => {
evt.dataTransfer.dropEffect = 'move'
evt.dataTransfer.effectAllowed = 'move'
evt.dataTransfer.setData('itemID', item)
},
onDrop (evt, list) {
const itemID = evt.dataTransfer.getData('itemID');
const movedData = this.todosList[itemID];
this.todosList.splice(itemID,1);
this.todosList.splice(list,0, movedData);
},
toggleCompleted() {
// still need to write this method
},
removeTask() {
// still need to write this method
}
}
}
</script>

Vue JS: Show loader initially and hide it after getting response

I want to show loader initially but after the response I want to hide it. I have following code:
<template>
<div id="app">
<Document :loading="loading">
</div>
</template>
<script>
import Document from "./components/Document";
export default {
name: "App",
data() {
return {
loading: true
};
},
components: {
Document
},
methods:{
function(){
let response = await Axios.get(`this-is-url`, {});
if (response.data == null) {
return;
} else {
this.loading = false
}
}
}
};
</script>
The loader will be placed in the document component like below:
<template>
<div>
<div>
<b-spinner type="grow" label="Spinning"></b-spinner>
</div>
</div>
</template>
<script>
export default {
name: "Document",
props: {
loading: null
},
};
</script>
I am writing function in App component because this will come from emit.
i think by changing the value of loading to false at the response it will hide the Document component.
if (response.data == null) {
this.loading= false;
}

How to update data from vue-tables-2 after action from Template?

I'm using a custom component as a column on vue-tables-2, to do that I'm using a vue-component as described here: vue-components
I've created a button that opens a modal to the user confirm some information, and after that I make a request to the backend and the record is changed on the database.
Now I want to refresh the data on the table, but I don't know how to do that. The documentation said about using the $ref, but this is not an option because my component is not the parent.
How can I do that?
Links to the code:
Component using 'vue-tables-2'
<template>
<div>
<div id="payment">
<input type="checkbox" v-model="onlyPending" #change="filterPay()">Apenas pendentes</input>
<v-server-table url="/api/payments" :columns="columns" :options="options" ></v-server-table>
</div>
</div>
</template>
<script>
import pay from './ModalConfirmPay.vue'
import {Event} from 'vue-tables-2';
export default {
name: "AeraListPayment",
props: ['groupId'],
data: function(){
let groupId = this.groupId;
return {
columns: ['name','value','course','due_date','paid','installment','pay'],
options: {
responseAdapter : function(data) {
data.data = data.data.map(payment => {
payment.paid = payment.paid ? "pago" : "pendente";
return payment;
})
return data;
},
headings: {
installment: 'Parcela',
paid: 'Status',
value: 'Valor',
due_date: 'Vencimento',
pay: 'Ação',
course: 'Curso',
name: 'Nome'
},
templates : {
pay
},
customFilters: ['onlyPending','groupId'],
initFilters:{groupId:groupId,onlyPending:true}
},
onlyPending: true
}
},
methods: {
filterPay(){
Event.$emit('vue-tables.filter::onlyPending', this.onlyPending);
}
}
}
</script>
Component that is being used as a custom column:
<template>
<div>
<button #click.prevent="show">Pagar</button>
<modal :name="modalName">
<p>Confirma o pagamento de {{data.value}} ?</p>
<p>Parcela: {{data.installment}}</p>
<p>Vecimento: {{data.due_date}}</p>
<button #click.prevent="pay">Confirmar</button>
<button #click.prevent="hide">Cancelar</button>
</modal>
</div>
</template>
<script>
import PaymentService from '../../services/PaymentService'
let service = new PaymentService();
export default {
name:"ModalConfirmPay",
props: ["data"],
computed: {
modalName: function () {
// `this` aponta para a instância Vue da variável `vm`
return `confirm-pay-${this.data.clientGroup_id}-${this.data.installment}`
}
},
methods: {
show () {
this.$modal.show(this.modalName);
},
pay ( ) {
service.pay(this.data)
.then(this.hide());
},
hide () {
this.$modal.hide(this.modalName);
}
}
}
</script>
First, defined an EventBus if you don't have
EventBus.vue
import Vue from 'vue'
export default new Vue()
In ListPayment.vue, import EventBus and listen for refresh-table event. Note that I add ref="table" to vue-tables-2 element
<template>
<v-server-table ref="table" ... />
</template>
<script>
import EventBus from './EventBus.vue'
export default {
mounted() {
EventBus.$on('refresh-table', this.refreshTable)
},
beforeDestroy() {
EventBus.$off('refresh-table', this.refreshTable)
},
methods: {
refreshTable() {
this.$refs.table.refresh();
}
}
}
</script>
Finally, emit event in modal
pay() {
service.pay(this.data)
.then(() => {
EventBus.$emit('refresh-table')
})
.then(this.hide());
}

How to build a bridge of events between different components in Vue?

I need to solve it
1) click mainMidLeft component
2) after clicked, to move slideLeftTop component
http://joxi.ru/ZrJBvERH1JVa8r
The problem I dont quite understand how to do this in right way..
Is it okay to create in mainMidLeft a method where I will do somethik like this:
move: () => {
document.querySelector(`.slideLeftTop`).style.position .....
}
The best practice is to use Vuex State manager with computed methods (getters) and watchers
I have made a working example for you on jsfiddle.
https://jsfiddle.net/n4e_m16/wujafg5e/4/
For more info on how vuex works please go to Here
Please let me know if you need more help :)
const store = new Vuex.Store({
state: {
mainMidLeftState: false,
},
getters: {
mainMidLeftState: state => state.mainMidLeftState,
},
mutations: {
toggleMainMidLeft: (state, payload) => {
state.mainMidLeftState = !state.mainMidLeftState
},
},
})
Vue.component('main-mid-left', {
data() {
return {
}
},
computed: {
mainMidLeftState() {
return this.$store.state.mainMidLeftState
},
},
methods: {
toggleMainMidLeft() {
this.$store.commit('toggleMainMidLeft')
// alert(this.mainMidLeftState)
},
}
})
Vue.component('slide-left-top', {
data() {
return {
}
},
computed: {
mainMidLeftState() {
return this.$store.state.mainMidLeftState
},
},
watch: {
mainMidLeftState: function(val) {
alert("YES, computed property changed and alert have been triggered by watcher in slide top left component")
}
},
methods: {
}
})
const app = new Vue({
el: '#app',
store,
})
div {
color: black;
}
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<script src="https://unpkg.com/vuex#3.0.1/dist/vuex.js"></script>
<div id="app">
<!-- inlining the template to make things easier to read - all of below is held on the component not the root -->
<main-mid-left inline-template>
<div>
<h4>
main mid left
</h4>
<button v-on:click="toggleMainMidLeft()">toggle Main Mid Left State</button>
<div v-show="mainMidLeftState == true">State is true</div>
<div v-show="mainMidLeftState == false">State is false</div>
</div>
</main-mid-left>
<slide-left-top inline-template>
<div>
<h4>
slide left top
</h4>
<div v-show="mainMidLeftState == true">State is true</div>
<div v-show="mainMidLeftState == false">State is false</div>
</div>
</slide-left-top>
</div>
If you don't want to use vuex, you can create a new Vue instance as an event bus (I believe this is mentioned somewhere in the Vue tutorial):
const EventBus = new Vue()
Import EventBus to where you need it and you can send an event by:
EventBus.$emit('event-name', data)
And add the following script in created() of your receiver component:
EventBus.$on('event-name', ($event) => {
// Do something
})
I hope this helps |´・ω・)ノ

Fetch data in component on initiation using parameters from Vuex store

I am new to Vue and am trying to build a simple movie app, fetching data from an API and rendering the results. I want to have an incremental search feature. I have an input field in my navbar and when the user types, I want to redirect from the dashboard view to the search results view. I am unsure of how to pass the query params from the navbar to the search results view.
Here is my App.vue component
<template>
<div id="app">
<Navbar></Navbar>
<router-view/>
</div>
</template>
<script>
import Navbar from './components/Navbar.vue'
export default {
name: 'App',
components: {
Navbar
},
}
</script>
And here is my navbar component where I have the input field
<template>
<nav class="navbar">
<h1 class="logo" v-on:click="goToHome">Movie App</h1>
<input class="search-input" v-on:keyup="showResults" v-model="query" type="text" placeholder="Search..."/>
</nav>
</template>
<script>
import router from '../router/index'
export default {
data: function () {
return {
query: this.query
}
},
methods: {
goToHome () {
router.push({name: 'Dashboard'})
},
showResults () {
//here on each key press I want to narrow my results in the SearchedMovies component
}
}
}
</script>
If I use router.push to the SearchedMovies component then I am only able to pass the query as a parameter once. I thought about using Vuex to store the query and then access it from the SearchedMovies component, but surely there is a better way of doing it?
I also read about using $emit but since my parent contains all the routes, I'm not sure how to go about this.
You don't need to redirect user anywhere. I've made a small demo to show how one might do it. I used this navbar component as you described and emit an event from it:
const movies = {
data: [
{
id: 0,
title: 'Eraserhead',
},
{
id: 1,
title: 'Erazerhead',
},
{
id: 2,
title: 'Videodrome',
},
{
id: 3,
title: 'Videobrome',
},
{
id: 4,
title: 'Cube',
},
]
};
Vue.component('navbar', {
template: '<input v-model="filter" #input="onInput" placeholder="search">',
data() {
return {
filter: '',
};
},
methods: {
onInput() {
this.$emit('filter', this.filter);
}
}
});
// this is just a request imitation.
// just waiting for a second until we get a response
// from the datasample
function request(title) {
return new Promise((fulfill) => {
toReturn = movies.data.filter(movie => movie.title.toLowerCase().indexOf(title.toLowerCase()) !== -1)
setTimeout(() => fulfill(toReturn), 1000);
});
}
new Vue({
el: '#app',
data: {
movies: undefined,
loading: false,
filter: '',
lastValue: '',
},
methods: {
filterList(payload) {
// a timeout to prevent
// instant request on every input interaction
this.lastValue = payload;
setTimeout(() => this.makeRequest(), 1000);
},
makeRequest() {
if (this.loading) {
return;
}
this.loading = true;
request(this.lastValue).then((response) => {
this.movies = response;
this.loading = false;
});
}
},
mounted() {
this.makeRequest('');
}
})
<script src="https://unpkg.com/vue"></script>
<div id="app">
<navbar v-on:filter="filterList"></navbar>
<ul v-if="!loading">
<li v-for="movie in movies" :key="movie.id">{{ movie.title }}</li>
</ul>
<p v-else>Loading...</p>
</div>
Also jsfiddle: https://jsfiddle.net/oniondomes/rsyys3rp/
If you have any problem to understand the code above let me know.
EDIT: Fixed some bugs and added a couple of comments
EDIT2(after the comment below):
Here's what you can do. Every time user inputs something inside a navbar you call a function:
// template
<navbar v-on:input-inside-nav-bar="atInputInsideNavBar"></navbar>
// script
methods: {
atInputInsideNavBar(userInput) {
this.$router.push({
path: '/filtred-items',
params: {
value: userInput
}
})
}
}
Then inside you 'searched movies' page component you can access this value so:
this.$route.params.value // returns userInput from root component