I have a replies component and newReply component in my page. When a newReply is added, i emit an event on a global vue message bus to notify the replies component that a new reply has been added so that it reloads and re-renders.
methods: {
updateReplyList: function(newReply){
window.vueMessageBus.$emit('notifyingrepliescomponent',newReply);
},
}
i have attached the event listener for the notifyingrepliescomponent event inside the created() hook of replies component.
UPDATED CODE SNIPPET
//file: replies.vue
methods: {
fetch: function(url = null){
let vm = this;
if(!url)
{
axios.get(route('replies.paginated',{ 'thread' : this.thread_id }))
.then(function(serverResponse){
vm.replies_with_pagination = serverResponse.data;
//'replies_with_pagination' holds the paginated collection returned by laravel for the current paginated page
});
}
else
{
var page = url.match(/\?page=(\d+)/i)[1];
axios.get(route('replies.paginated',{ 'thread' : this.thread_id, 'page' : page }))
.then(function(serverResponse){
vm.replies_with_pagination = serverResponse.data;
});
}
},
reloadNew: function(url){
this.fetch(url);
window.scrollTo(0,0);
},
},
created() {
this.fetch();
window.vueMessageBus.$on('notifyingrepliescomponent',newReply => {
console.log('added object: ' + newReply);
this.all_reply_items.push(newReply);
this.reloadNew(route('replies.paginated',{ 'thread' : this.thread_id, 'page' : this.pageForQueryString }));
this.$emit('repliescountchanged',this.all_reply_items.length);
});
},
The whole system works, except for the first time. That is when there are no replies to a thread, and i add a new reply, the replies component does not reload. But for the subsequent replies, this works fine.
I am guessing this is an issue with my event listener? Can anyone please help?
TIA, Yeasir
Related
So I have this nuxt page /pages/:id.
In there, I do load the page content with:
content: function(){
return this.$store.state.pages.find(p => p.id === this.$route.params.id)
},
subcontent: function() {
return this.content.subcontent;
}
But I also have an action in this page to delete it. When the user clicks this button, I need to:
call the server and update the state with the result
redirect to the index: /pages
// 1
const serverCall = async () => {
const remainingPages = await mutateApi({
name: 'deletePage',
params: {id}
});
this.$store.dispatch('applications/updateState', remainingPages)
}
// 2
const redirect = () => {
this.$router.push({
path: '/pages'
});
}
Those two actions happen concurrently and I can't orchestrate those correctly:
I get an error TypeError: Cannot read property 'subcontent' of undefined, which means that the page properties are recalculated before the redirect actually happens.
I tried:
await server call then redirect
set a beforeUpdate() in the component hooks to handle redirect if this.content is empty.
delay of 0ms the server call and redirecting first
subcontent: function() {
if (!this.content.subcontent) return redirect();
return this.content.subcontent;
}
None of those worked. In all cases the current page components are recalculated first.
What worked is:
redirect();
setTimeout(() => {
serverCall();
}, 1000);
But it is obviously ugly.
Can anyone help on this?
As you hinted, using a timeout is not a good practice since you don't know how long it will take for the page to be destroyed, and thus you don't know which event will be executed first by the javascript event loop.
A good practice would be to dynamically register a 'destroyed' hook to your page, like so:
methods: {
deletePage() {
this.$once('hook:destroyed', serverCall)
redirect()
},
},
Note: you can also use the 'beforeDestroy' hook and it should work equally fine.
This is the sequence of events occurring:
serverCall() dispatches an update, modifying $store.state.pages.
content (which depends on $store.state.pages) recomputes, but $route.params.id is equal to the ID of the page just deleted, so Array.prototype.find() returns undefined.
subcontent (which depends on content) recomputes, and dereferences the undefined.
One solution is to check for the undefined before dereferencing:
export default {
computed: {
content() {...},
subcontent() {
return this.content?.subcontent
👆
// OR
return this.content && this.content.subcontent
}
}
}
demo
I'm developing a real time chat app with Vue.js and Firebase realtime database.
When a new message is received, I want the chat window to scroll to the bottom. To achieve this, I created a watcher for the conversation data. However, the function in the watcher is executed before the DOM is updated so the scroll value isn't correct yet. Should I watch another property? How can I detect when the new data has been loaded into the DOM?
HTML Code
<div class="chat" ref="chat">
<div
v-for="(message,key) in conversations" :key="key">
<div class="message">
{{message.text}}
</div>
</div>
</div>
Script (I'm using VueFire)
const conversations = db.ref('conversations');
export default {
data() {
return {
conversations: {},
}
},
watch: {
conversations: function() {
//Scroll to bottom when new message received
this.$refs.chat.scrollTop = this.$refs.chat.scrollHeight;
}
}
}
I can fix this issue by setting a timeout but it's a dirty trick imo...
setTimeout(() => {
this.$refs.chat.scrollTop = this.$refs.chat.scrollHeight;
}, 300);
Thank you for your help.
Edit: DOMNodeInserted => MutationObserver
You could use a MutationObserver
Here is a working example: https://codepen.io/Qumez/pen/WNvOWwp
Create a scrollToBottom method:
...
methods: {
scrollToBottom() {
this.$refs.wrapper.scrollTop = this.$refs.wrapper.scrollHeight;
}
}
...
And call it whenever a new message is added:
...
data() {
return {
mo: {}
}
},
mounted() {
let vm = this;
this.mo = new MutationObserver((mutationList, observer) => {
vm.scrollToBottom();
});
this.mo.observe(this.$el, {childList: true})
}
...
My variable names are a bit different than yours, but it'll work in your code once you update it.
I am trying to get events based on the current view. To track 'prev', 'next' clicks and initial display views, I'm using the CalendarOption viewRender which returns the full view, and invokes itself every click - which gives a new full view.
Problem is - I cannot call the service from which I'm getting my events from, based on the extracted parameter of the viewRender view output.
constructor (public eventService: EventService)
calendarOptions = {
...
...
eventLimit: true,
viewRender: function(view) {
this.eventService.getEvents(view) // <--- code stops here, error printed at bottom
.subscribe(
response => {
this.events = response.events;
},
error => {
console.log(error)
}
);
console.log(this.events);
},
events: this.events,
...
...
}
I'm expecting my service to work and be triggered from within that scenario - on calendar first initializing and each time the 'prev' / 'next' buttons are clicked
currently, the output is
ERROR TypeError: Cannot read property 'getEvents' of undefined
Is there any other way I can invoke this external command?
I also tried using httpClient directly, but HTTP is also a service which can't be accessed as it seems
It is now solved
Managed to get access to my service by appending:
const eventService = this.eventService;
here:
initCalendar() {
const eventService = this.eventService;
var events = []
this.calendarOptions = {
...
...
viewRender: function(view) {
eventService.getEvents(view) // <----- Not accessing with 'this' anymore
.subscribe(
response => {
events = response.events;
},
error => {
console.log(error)
}
);
},
events: events;
...
...
}
}
I have a function that helps filter data. I am using v-on:change when a user changes the selection but I also need the function to be called even before the user selects the data. I have done the same with AngularJS previously using ng-init but I understand that there is no such a directive in vue.js
This is my function:
getUnits: function () {
var input = {block: this.block, floor: this.floor, unit_type: this.unit_type, status: this.status};
this.$http.post('/admin/units', input).then(function (response) {
console.log(response.data);
this.units = response.data;
}, function (response) {
console.log(response)
});
}
In the blade file I use blade forms to perform the filters:
<div class="large-2 columns">
{!! Form::select('floor', $floors,null, ['class'=>'form-control', 'placeholder'=>'All Floors', 'v-model'=>'floor', 'v-on:change'=>'getUnits()' ]) !!}
</div>
<div class="large-3 columns">
{!! Form::select('unit_type', $unit_types,null, ['class'=>'form-control', 'placeholder'=>'All Unit Types', 'v-model'=>'unit_type', 'v-on:change'=>'getUnits()' ]) !!}
</div>
This works fine when I select a specific item. Then if I click on all lets say all floors, it works. What I need is when the page is loaded, it calls the getUnits method which will perform the $http.post with empty input. In the backend I have handled the request in a way that if the input is empty it will give all the data.
How can I do this in vuejs2?
My Code: http://jsfiddle.net/q83bnLrx
You can call this function in the beforeMount section of a Vue component: like following:
// .....
methods: {
getUnits: function() { /* ... */ }
},
beforeMount() {
this.getUnits()
},
// ......
Working fiddle: https://jsfiddle.net/q83bnLrx/1/
There are different lifecycle hooks Vue provide:
I have listed few are :
beforeCreate: Called synchronously after the instance has just been initialized, before data observation and event/watcher setup.
created: Called synchronously after the instance is created. At this stage, the instance has finished processing the options which means the following have been set up: data observation, computed properties, methods, watch/event callbacks. However, the mounting phase has not been started, and the $el property will not be available yet.
beforeMount: Called right before the mounting begins: the render function is about to be called for the first time.
mounted: Called after the instance has just been mounted where el is replaced by the newly created vm.$el.
beforeUpdate: Called when the data changes, before the virtual DOM is re-rendered and patched.
updated: Called after a data change causes the virtual DOM to be re-rendered and patched.
You can have a look at complete list here.
You can choose which hook is most suitable to you and hook it to call you function like the sample code provided above.
You need to do something like this (If you want to call the method on page load):
new Vue({
// ...
methods:{
getUnits: function() {...}
},
created: function(){
this.getUnits()
}
});
you can also do this using mounted
https://v2.vuejs.org/v2/guide/migration.html#ready-replaced
....
methods:{
getUnits: function() {...}
},
mounted: function(){
this.$nextTick(this.getUnits)
}
....
Beware that when the mounted event is fired on a component, not all Vue components are replaced yet, so the DOM may not be final yet.
To really simulate the DOM onload event, i.e. to fire after the DOM is ready but before the page is drawn, use vm.$nextTick from inside mounted:
mounted: function () {
this.$nextTick(function () {
// Will be executed when the DOM is ready
})
}
If you get data in array you can do like below. It's worked for me
<template>
{{ id }}
</template>
<script>
import axios from "axios";
export default {
name: 'HelloWorld',
data () {
return {
id: "",
}
},
mounted() {
axios({ method: "GET", "url": "https://localhost:42/api/getdata" }).then(result => {
console.log(result.data[0].LoginId);
this.id = result.data[0].LoginId;
}, error => {
console.error(error);
});
},
</script>
methods: {
methodName() {
fetch("url").then(async(response) => {
if (response.status === 200) {
const data = await response.json();
this.xy = data.data;
console.log("Success load");
}
})
}
}
you can do it using created() method. it will fire once page fully loaded.
created:function(){
this.fillEditForm();
},
Ok I'll try to explain this the best I can. I have a ResourceInfo component that posts data to the /resources/ path and /users/ + uid + /created-resources path using newPostKeyand update.
I also have a QuizBuilder component. I want to post data from this component to a /resources/ + newPostKey + /quiz/ path. However, I don't know how to get the newPostKeyor key from that particular path I created in ResourceInfo from the QuizBuilder component.
Here are the two components. First the user adds info using the ResourceInfo component. Once they hit submit they go to the QuizBuilder component where they create the quiz.
ResourceInfo.vue
export default {
name: 'resource-info',
data () {
return {
header: 'Before you build your quiz we just need some quick info.',
sharedState: store.state,
resource: {
type: '',
title: '',
url: '',
desc: '',
timesPassed: 0,
authorId: store.state.userInfo.uid,
authorName: store.state.userInfo.displayName,
authorImage: store.state.userInfo.photoURL
},
}
},
methods: {
saveToFirebase () {
var newPostKey = firebase.database().ref().child('resources').push().key;
var updates = {};
updates['/resources/' + newPostKey] = this.resource;
updates['/users/' + store.state.userInfo.uid + '/created-resources/' + newPostKey] = this.resource;
// Clear inputs
this.resource.title = '',
this.resource.type = '',
this.resource.desc = '',
this.resource.url = ''
console.log("Saving resource data...")
return firebase.database().ref().update(updates);
}
}
}
QuizBuilder.vue
export default {
name: "quiz-builder",
data () {
return {
questions: [createNewQuestion()],
showQuestions: false
}
},
methods: {
addQuestion () {
this.questions.push(createNewQuestion())
},
addOption (question) {
question.options.push(createNewOption())
},
saveToFirebase (e) {
e.preventDefault();
var questions = this.questions;
this.firebaseRef = db.ref('a/path/here'); // /resources/ + that resources id + /quiz/
this.firebaseRef.push({ // Should I use set or push here?
questions
})
console.log('Saving quiz data...')
}
}
}
The answer depends on how the transition between the components/pages are made.
If you're building a single page app with vue-router or something, then the transition is replacing the former component with the latter, which all happens on the index.html, with no request sent(simplest situation). To still keep the generated key within our grasp after the first component is gone, you need to save it on a common parent of the two components. To be specific, add a key in the parent's data, and let the ResourceInfo emit a custom event with the generated key to notify the parent to set its key. See http://vuejs.org/guide/components.html#Using-v-on-with-Custom-Events .
If you refreshes the page when jumping from ResourceInfo to to Quiz, with server-side rendering (which should be really rare, since it requires more effort compared to the single-page way, and has an inferior performance), then it's irrelavent to vue and rather simple: redirect the user to Quiz after ResourceInfo is saved, with the key as a url param.
Edit upon OP's using store.js:
Just store the key in LocalStorage(store.js) and retrive it from another component should work since LocalStorage is available globally and even across pages/sessions.
Some thought: main.js just be the parent is in some sense right. There's no real parent vue component here, but our main.js is evaled by the browser in the global scope, so it's true that main.js is the root entry of our app, aka parent.