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.
Related
I've implemented Instascan (https://github.com/schmich/instascan) to allow users to read QR Codes from within my Angular 5 app.
I have to trigger some actions after a successful scan and update my component's view accordingly.
I'm using the following code inside my component to detect my cameras and start scanning:
cameras: Array<any> = []
selectedCamera: any
scanner: any
content: string
ngOnInit () {
let vm = this
Instascan.Camera.getCameras().then(function (cameras) {
if (cameras.length > 0) {
vm.cameras.push(...cameras)
} else {
console.error('No cameras found.')
}
}).catch(function (e) {
console.error(e)
})
}
startScan () {
let vm = this
this.scanner = new Instascan.Scanner({
video: this.videoContainer.nativeElement,
backgroundScan: false,
mirror: false
})
this.scanner.addListener('scan', function (content) {
vm.content = content
})
this.selectedCamera = this.cameras[0]
this.scanner.start(this.selectedCamera)
}
And in my template I have an element that exists only if 'content' exists, and on click emit the scanned content to the parent component through an EventEmitter:
<div *ngIf="content" class="btn" (click)="emitContent()">
PROCEED
</div>
The problem is that in 'scan' event callback the changes in 'content' seems to don't be applied to my view, and my 'PROCEED' button don't become visible. An even stranger behavior happens: after I click anywhere in the screen, those changes are applied to my view.
I've also tried using ChangeDetectorRef.detectChanges() method inside the callback and binding 'this' into the callback, but both are not working.
How can I overcome this issue?
Thanks!
I've managed to solve this problem by using NgZone like this:
import { NgZone } from '#angular/core'
constructor (
private ngZone: NgZone
) {}
startScan () {
this.scanner = new Instascan.Scanner({
video: this.videoContainer.nativeElement,
backgroundScan: false,
mirror: false
})
this.scanner.addListener('scan', function (content) {
this.ngZone.run(() => {
this.content = content
})
}.bind(this))
this.selectedCamera = this.cameras[0]
this.scanner.start(this.selectedCamera)
}
I don't know if this is the best solution, and actually I don't know how NgZone usage affects the application performance/state at all.
If someone know a better solution, it will be welcome!
Thanks!
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
Vue.js version is: 2.x
Hi. I'm sending an ajax request in vue js to another page and getting it's source which contains vue.js syntax such as events. When this source is added to property and property added to a template, the ajax data source (that contains vue.js syntax) can not be rendered and does not work properly.
For example template is:
<div id="app">
{{{ foo }}}
</div>
and app.js is:
var app = new Vue({
el: '#app',
data: {
foo: 'bar'
},
mounted(){
this.$http.get('/media').then(function(response){
data = response.body;
Vue.set(app, 'foo', data);
});
},
methods: {
alertVideoLink: function(event){
alert(event.target.href);
}
}
});
In the above app.js code, ajax request returns this code (that is response.body):
Video Link
but this link can't be rendered and does not work properly! I'm testing the render method and some useful hints, but no way found. Please help... Thanks
Sounds like you want to use an Async Component.
Something like...
components: {
'async-media': () => Vue.http.get('/media').then(res => ({
template: res.body,
methods: {
alertVideoLink (event) {
this.$emit('click', event)
}
}
}))
}
Then in your template...
<async-media #click="handleClickEventFromChildComponent" />
Here's an example using a timeout to fake "load" a template
var app = new Vue({
el: '#app',
data: {},
components: {
'async-media': () => new Promise(resolve => {
setTimeout(() => {
resolve({
template: 'Video Link',
methods: {
alertVideoLink(event) {
this.$emit('click', event.target.href)
}
}
})
}, 2000)
})
},
methods: {
handleClickEventFromChildComponent (href) {
console.info('Clicked on', href)
}
}
});
<div id="app">
<p>Wait 2 seconds</p>
<async-media #click="handleClickEventFromChildComponent" />
</div>
<script src="https://unpkg.com/vue#2.4.2/dist/vue.min.js"></script>
#Phil's answer is correct but in my project need to be changed. in this case, the better way is: using global components vs local components because is simple for this work.
Hi guys I am using Vue JS to try and loop through my data. Here is my whole JS file:
var contentful = require('contentful');
var client = contentful.createClient({
space: 'HIDDEN',
accessToken: 'HIDDEN'
});
Vue.component('careers', {
template: '<div><div v-for="career in careerData">{{ fields.jobDescription }}</div></div>',
data: function() {
return {
careerData: []
}
},
created: function() {
this.fetchData();
},
methods: {
fetchData: function() {
client.getEntries()
.then(function (entries) {
// log the title for all the entries that have it
entries.items.forEach(function (entry) {
if(entry.fields.jobTitle) {
this.careerData = entries.items;
}
})
});
}
}
});
var app = new Vue({
el: '#app'
});
I am using methods to access some data from Contentful, once it has grabbed the necessary data it is sent to my data object.
If I console.log(careerData); within my console the following data is returned:
So I'd expect if I used v-for within my template and tried iterating over careerData it would render correctly however on my front-end I am left with an empty div like so:
<div id="app"><div></div></div>
I am currently pulling my component into my HTML like so:
<div id="app">
<careers></careers>
</div>
No errors are displayed within my console, can you think of any reason this might be happening?
Thanks, Nick
Several problems I think. As #dfsq said, you should use a arrow function if you want to keep context (this).
fetchData: function() {
client.getEntries()
.then(entries => {
this.careerData = entries.items
});
}
Then you may replace {{fields.jobDescription}} by {{career.fields.jobDescription}}, as #unholysheep wrote.
It may work. If it does not, you could add a this.$forceUpdate(); right after this.fetchData();
Use arrow function in forEach callback so you don't loose context:
fetchData: function() {
client.getEntries()
.then(entries => {
this.careerData = entries.items
});
}
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();
},