setTimeout on a high volume data - javascript

I have tried googling and searching entirety of stack overflow for this question but I think it boils down to the keywords I'm using to search.
Basically my problem boils down to the following: when the cursor leaves an element, wait 500 milliseconds before closing the element. Before close the element, check if the cursor is back in the element, and if its not, do not hide it.
I'm using vuejs to do this but I boiled down the problem to being in setTimeout function. The part where I have the code is fairly complex to post it here, therefore I created a simple POC to demonstrate the problem:
<template>
<div id="app">
<ul v-for="x in 2000" :key="x">
<li #mouseenter="handleMouseEnter(x)" #mouseleave="handleMouseLeave(x)" style="height: 50px;">
Hello
<span style="background-color: red" v-show="showBox[x]">BOX</span>
</li>
</ul>
</div>
</template>
<script>
export default {
name: "App",
components: {},
methods: {
handleMouseEnter(index) {
setTimeout(() => {
let showBox = [...this.showBox];
showBox[index] = true;
this.showBox = showBox;
}, 500);
},
handleMouseLeave(index) {
let showBox = [...this.showBox];
showBox[index] = false;
this.showBox = showBox;
}
},
data() {
return {
showBox: []
};
},
created() {
for (let i = 0; i <= 2000; i++) {
this.showBox[i] = false;
}
}
};
</script>
You can checkout the sandbox here: https://codesandbox.io/s/cold-river-ruz7b
If you hover over from top to bottom in a moderate speed you will realize that even after leaving the li element the red box stays.
I guess the problem lays in the fact that handleMouseEnter is being called with a setTimeout and the handleMouseLeave is not. Therefore, making handleMouseEnter be executed after handleMouseLeave therefore showing the box.
Any light would be highly appreciated here and if a short explanation could be given on why the problem is happening it would be great

Your example seems to operate the opposite way round to the original problem description (the timer is on showing not hiding) but I think I get what you mean.
As you suggest, the problem is that the timer callback is being called after the mouseleave event fires. So the red box does get hidden but shortly thereafter the timer fires and brings it back.
In the example below I have simply cancelled the timer using clearTimeout. In general it might be necessary to store an array of such timers, one for each element, but in this specific example I believe it only makes sense to have one timer active at once so I can get away without an array.
I also moved the initial population of showBox into data. There seemed no reason to use a created hook here.
There's no need to copy the whole array each time, you can just use $set to set the value in the existing array. I haven't changed that in my example.
I would also note that for this particular example you don't need an array to hold all the showBox values. Only one red box can be visible at once so you only need a single property to hold the index of the currently visible box. I haven't changed this in my example as I suspect your real use case is not as straight forward as this.
new Vue({
el: '#app',
methods: {
handleMouseEnter(index) {
this.currentTimer = setTimeout(() => {
let showBox = [...this.showBox];
showBox[index] = true;
this.showBox = showBox;
}, 500);
},
handleMouseLeave(index) {
clearTimeout(this.currentTimer)
let showBox = [...this.showBox];
showBox[index] = false;
this.showBox = showBox;
}
},
data() {
const showBox = [];
for (let i = 0; i <= 2000; i++) {
showBox[i] = false;
}
return {
showBox
};
}
})
<script src="https://unpkg.com/vue#2.6.10/dist/vue.js"></script>
<div id="app">
<ul v-for="x in 2000" :key="x">
<li #mouseenter="handleMouseEnter(x)" #mouseleave="handleMouseLeave(x)" style="height: 50px;">
Hello
<span style="background-color: red" v-show="showBox[x]">BOX</span>
</li>
</ul>
</div>

Related

How to keep track (write, read, update) of element states via localStorage?

first of all, I apologize that I don't speak English, I am trying to improve in this language.
I have a problem that I don't know how to solve it, I have a web in WordPress and it creates buttons dynamically with some dynamic classes, I need to take 1 of those classes to do the following:
Let's imagine that we have 5 buttons, when I clicked on 1 of those 5 that the other 4 buttons are hidden and this is saved in the local storage, ie everything is kept in the browser.
I have this code:
$('.dce-button').click(function(event){
$(this).attr('data-id' , 'Next');
localStorage.setItem("class", "noDisplay");
localStorage.setItem("noDisplay", "true");
$('.dce-button').not(this).each(function(){
$(this).toggleClass("noDisplay");
});
})
I use it to select the button I need and WORKS, it disappears the other 4 but I don't know how to keep it with the local storage, Is someone can help me?
Something like this would work and address Peter's comment
(working JS fiddle since localStorage isnt accessible in snippets on SO https://jsfiddle.net/f7804xwc/1/)
// on page load, get the stored value if there is one and set the buttons accordingly
let selectedBtn = localStorage.getItem('selectedBtn');
if (selectedBtn) {
$('.dce-button').hide();
$('#'+selectedBtn).show()
}
$('.dce-button').click(function(event) {
let selectedBtn = localStorage.getItem('selectedBtn');
console.log(selectedBtn);
if (selectedBtn === this.id) {
// user clicked the selected button again, lets un-select it and re-show all the buttons
localStorage.removeItem('selectedBtn');
$('.dce-button').show();
} else {
// user clicked a button, store selection and hide the others
localStorage.setItem("selectedBtn", this.id);
$('.dce-button').not(this).each(function() {
$(this).hide();
});
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<button class="dce-button" id="btn1">Button 1</button><br>
<button class="dce-button" id="btn2">Button 2</button><br>
<button class="dce-button" id="btn3">Button 3</button><br>
<button class="dce-button" id="btn4">Button 4</button><br>
<button class="dce-button" id="btn5">Button 5</button><br>
From the above comment ...
"Once the OP has managed to solve writing to and reading from the local storage the proper button state there will be never a chance of any other button visible than the very one which was selected at the very first time (unless one resets/erases the local storage's state(s))."
... and all code of the next provided example which relates to initStorageRelatedUIHandling (the last 3 buttons which are for logging, clearing and rendering from the local storage's current state) is going to prove the above said/commented.
Now as for a possible solution, I personally would chose an approach which stores the state(s) of any DCE related button as a JSON.stringify 'ed array. Thus one does not rely on any specific button identification but stays generic from the beginning based on just a list of queried (jquery or selector api ... it doesn't matter) DOM nodes.
One would write 2 functions writeDceState and readDceState which both take care of the storage and JSON handling.
Another function like updateDceState exclusively takes care of generating the current array representation of any buttons state based on the currently targeted/selected button. It queries a list of all DCE buttons and does map each visibility state as false except for the currently chosen. It finally forwards the updated state to writeDceState.
The render part is covered exclusively by renderDceStateFromStorage. It reads the most current state from storage and updates each button's visibility from the parsed array.
Then there is a sole handler function ... handleDceStateChange ... which on click invokes both functions one after the other ... updateDceState and renderDceStateFromStorage.
function writeDceState(state = null) {
// `mock` is a specifc namespace which carries
// `localStorage` behavior for SO environments.
// - Thus the next line can be omitted later.
const { localStorage } = mock;
localStorage
.setItem('dce', JSON.stringify(state)
);
}
function readDceState() {
// `mock` is a specifc namespace which carries
// `localStorage` behavior for SO environments.
// - Thus the next line can be omitted later.
const { localStorage } = mock;
return JSON
.parse(
localStorage.getItem('dce') || null
);
}
function updateDceState(dceTarget) {
const state = Array
.from(
document
.querySelectorAll('.dce-button')
)
.map(elm => (elm === dceTarget));
writeDceState(state);
}
function renderDceStateFromStorage() {
const state = readDceState();
document
.querySelectorAll('.dce-button')
.forEach((elm, idx) =>
elm.disabled = !!state && !state[idx]
);
}
function handleDceStateChange({ currentTarget }) {
updateDceState(currentTarget);
renderDceStateFromStorage();
}
function initDceHandling() {
document
.querySelectorAll('.dce-button')
.forEach(elm =>
elm.addEventListener('click', handleDceStateChange)
);
}
function initStorageRelatedUIHandling() {
document
.querySelector('[data-state-display]')
.addEventListener('click', () =>
console.log( readDceState() )
);
document
.querySelector('[data-state-clear]')
.addEventListener('click', mock.localStorage.clear);
document
.querySelector('[data-state-rerender]')
.addEventListener('click', renderDceStateFromStorage);
}
initDceHandling();
initStorageRelatedUIHandling();
body { margin: 0; }
ul { list-style: none; padding: 0; margin: 0; }
li:nth-child(6) { margin-top: 20px; }
li:nth-child(8) { margin-top: 10px; }
.as-console-wrapper { left: auto!important; width: 50%; min-height: 100%; }
<ul>
<li>
<button class="dce-button">Foo</button>
</li>
<li>
<button class="dce-button">Bar</button>
</li>
<li>
<button class="dce-button">Baz</button>
</li>
<li>
<button class="dce-button">Biz</button>
</li>
<li>
<button class="dce-button">Buzz</button>
</li>
<li>
<button data-state-clear>clear storage state</button>
</li>
<li>
<button data-state-display>display storage state</button>
</li>
<li>
<button data-state-rerender>rerender from storage</button>
</li>
</ul>
<script>
// mock for the SO specific stack snippet
// due to the policies and environment are
// not allowing an original storage access.
const mock = {
localStorage: (function () {
// https://developer.mozilla.org/en-US/docs/Web/API/Storage
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map
const storage = new Map;
function key(int) {
return [
...storage.keys()
][parseInt(int, 10)];
}
function setItem(key, value) {
return storage.set(String(key), String(value));
}
function getItem(key) {
return storage.get(String(key));
}
function removeItem(key) {
return storage.delete(String(key));
}
function clear() {
return storage.clear();
}
return {
get length() {
return storage.size;
},
key,
getItem,
setItem,
removeItem,
clear,
};
}()),
}
</script>

Checking if a value or multiple values inside an array of objects have changed and then rerun a method in VueJS

EDIT:
I've updated my watcher and recall the function to update the percentage live, but it's still not verbose enough to check and update all changed objs.
watch: {
tableData(newVal, oldVal) {
const changedObjIndex = this.tableData.findIndex((obj) => {
return obj.status === "In Progress"
})
console.log(`changed obj`, changedObjIndex)
this.handleDisplayStatus(changedObjIndex)
console.log(
`obj that changed`,
this.tableData[changedObjIndex].completion_progress
)
},
},
Problem
I have a table that shows the details and progress of a video(s) upload. The progress bar is where I am having trouble updating it live (more on this below).
I have a blank tableData array in my data that is filled when the component is rendered with a handleDisplayStatus function. This method only runs once obviously, so I was planning on putting a watcher on the tableData array and then rerunning the handleDisplayStatus function if there is a change. I initially went with:
this.tableData.every((item) =>
console.log(
`completion progress from watcher`,
item.completion_progress
)
)
However .every only returns a boolean and while it would rerun the handleDisplayStatus function, that function takes an index, _row so that doesn't seem to be the fix.
Also putting this.handleDisplayStatus(index, _row) in the watcher will give this error in the console Error in callback for watcher "tableData": "ReferenceError: index is not defined".
Which makes sense because that function needs the indexes (_row param is not being used I think).
From there I went with .findIndex, but again that just gives me the first element that satisfies the condition. Since the table can have multiple videos loading at the same time, this is not the correct fix either.
tableData(newVal, oldVal) {
const changedObjIndex = this.tableData.findIndex((obj) => {
return obj.status === "In Progress"
})
console.log(changedObjIndex)
}
As I mentioned, everything works fine, but I need a page refresh to show the updated number. We are purposely not using socket.io for this; even though I think it's probably a good idea.
Any ideas of how I can get this function to call properly and get the percentages to update in real time? Even the old :key to force a rerender doesn't want to work here.
Any help or guidance would be greatly appreciated!
NOTE:
my progress bar just takes in a percentage prop as a number to render and is pretty straightforward. If you need to see that code as well I can edit this post!
tableData Array of Objs with key/values
(NOTE: the task_status should be: "IN PROGRESS", but I couldn't edit that on the mySQL workbench)
Method that needs to be recalled
handleDisplayStatus(index, _row) {
if (this.tableData[index].task_status == "IN PROGRESS") {
let trainingProcessId = this.tableData[index].training_process_id
this.tableData[index].intervalId = setInterval(() => {
if (
this.tableData[index].completion_progress != 100 &&
this.tableData[index].task_status == "IN PROGRESS"
) {
getTrainingProcessInfo({
trainingProcessId: trainingProcessId,
}).then((res) => {
let jsonObj = JSON.parse(JSON.stringify(res.data))
this.tableData[index].completion_progress =
jsonObj.completion_progress
})
} else {
clearInterval(this.tableData[index].intervalId)
}
}, 6000)
}
return this.tableData[index].task_status
}
template (fwiw)
<template>
<div>
<el-card class="box-card">
<div style="margin-left: 5px" class="_table-wrapper">
<el-table
ref="multipleTable"
v-loading="listLoading"
:data="tableData"
id="el-table"
>
<el-table-column
:label="$t('training_monitor_content.table_column_5')"
align="center"
>
<template slot-scope="scope">
<ProgressBar
:key="scope.row.user"
style="width: 100%; height: 20px; flex-shrink: 0"
:percentage="scope.row.completion_progress"
/>
</template>
</el-table-column>
</el-table>
</div>
</el-card>
</div>
</template>
If I understood you correctly try with deep watch:
watch: {
tableData: {
handler(newValue) {
newValue.forEach((obj, idx) => {
if (obj.status === 'In Progress') this.handleDisplayStatus(idx)
});
},
deep: true
}
}

How to generate Vue components from Javascript

I'm new to Vue.js (with a background in Computer Science and programming, including interactive Javascript webpages) and as I'm a teacher, I have a quiz site I use to give homework to my students.
My codebase is messy, so I decided to migrate the whole thing to Vue, with the idea that I could use a component for each individual type of question -- separation of concerns, and all that.
However, I can't seem to find a way to generate appropriate components on the fly and include them in my page.
Here's a simplified version of my framework, with two question types. If I include the components directly in the HTML, they work fine.
Vue.component("Freetext",{
props: ["prompt","solution"],
data : function() {return {
response:""
}},
methods : {
check : function () {
if (this.solution == this.response) {
alert ("Correct!");
app.nextQuestion();
} else {
alert ("Try again!");
}
}
},
template:'<span><h1>{{prompt}}</h1> <p><input type="text" v-model="response"></input></p> <p><button class="LG_checkbutton" #click="check()">Check</button></p></span>'
})
Vue.component("multi",{
props : { prompt: String,
options : Array,
key_index : Number // index of correct answer
},
data : function() {return {
response:""
}},
methods : {
check : function (k) {
if (k == this.key_index) {
alert ("Correct!");
app.nextQuestion();
} else {
alert ("Try again!");
}
}
},
template:'<span><h1>{{prompt}}</h1><button v-for="(v,k) in options" #click="check(k)">{{v}}</button></span>'
})
</script>
<div id="app">
<Freetext prompt="Type 'correct'." solution="correct"></freetext>
<multi prompt="Click the right answer." :options='["right","wrong","very wrong"]' :key_index=0></multi>
</div>
<script>
var app = new Vue({
el: "#app",
data : {
questions:[ {type:"Multi",
prompt: "Click the right answer.",
options:["right","wrong","very wrong"],
key:0},
{type:"Freetext",
prompt:"Type 'correct'.",
solution:"correct"}
],
question_number:0
},
methods : {
nextQuestion : function () {
this.question_number ++;
}
}
})
</script>
But what I want to do is generate the contents of the div app on the fly, based on using the data member app.question_number as an index to app.questions, and the .type member of the question indicated (i.e. app.questions[app.question_number].type)
If I try to make the app of the form:
{{question}}
</div>
<script>
//...
computed : {
question : function () {
var typ = this.questions[this.question_number].type;
return "<"+typ+"></"+typ+">";
}
...I just get as plain text, and it isn't parsed as HTML.
If I try document.getElementById("app").innerHTML = "<multi prompt='sdf'></multi>"; from the console, the tag shows up in the DOM inspector, and isn't processed by Vue, even if I call app.$forceUpdate().
Is there any way round this?
While Keith's answer works for most of what I need to do, there's another way to handle this that I've just found out about, which I thought I'd share in case anyone else is looking for it: giving a block level HTML element a v-html property.
For me, this is handy as a short term fix as I'm migrating a codebase that generates dynamic HTML as strings, and I can quickly integrate some of my existing code without reworking it completely.
For example, I have a function makeTimetable that takes a custom datastructure representing a week's actively and turns it into a table with days across the top and times down the left-hand side, setting appropriate rowspans for all the activities. (It's a bit of a convoluted function, but it does what I need and isn't really worth refactoring at this point.)
So I can use this as follows:
<script type="text/x-template" id="freetext-template">
<span>
<div v-html="tabulated_timetable"></div>
<p>{{prompt}}</p>
<p><input type="text" v-model="response"></input></p>
<p><button class="LG_checkbutton" #click="check()">Check</button></p>
</span>
</script>
<script>
var freetext = Vue.component("Freetext",{
props: {"prompt":String,
"timetable":Object,
"solution":String,
data : function() {return {
response:""
}},
computed : {
tabulated_timetable : function () {
return makeTimetable (this.timetable);
}},
methods : {
check : function () {
if (this.solution == this.response) {
alert ("Correct!");
app.nextQuestion();
} else {
alert ("Try again!");
}
}
},
template:'#freetext-template'
})
</script>
(I suppose I could put `tabulated_timetable` in `methods` rather than `computed`, as it's set once and never changed, but I don't know if there would be any performance benefit to doing it that way.)
I think maybe a slightly different approach, Vue supports the concept of "dynamic components"
see https://v2.vuejs.org/v2/guide/components-dynamic-async.html
this will let you define what component to use on each question which would look something like
<component v-bind:is="question.component" :question="question"></component>

How to replace #click by addEventListener and make addEventListener so flexible as #click?

Chrome on Android does not allow to play music\video if there is no any user movements. I found a piece of Google's code which uses addEventListener which sees user's interactions and plays music\video.
For some reason, Chrome on Android does not allow to play track if #click is used. Because of debugging and reading lots of info, I found that Chrome starts playing for a moment, but then stops. It probably happens because Chrome thinks that #click is kind of auto-play that may show some adverts and does not have any relation to user's interaction because Chrome requires an explicit action by the user. If I start to use addEventListener, Chrome starts playing without any problems. It could be ok for me to use addEventListener, but:
1) I need somehow to pass trackObj from template to event listener's function;
2) There are could be hundreds of tracks, I do not know if it is ok to add listeners to all of them.
<template>
<div v-for="(trackObj, index) in tagObj.tracks" :key="trackObj.trackId">
// What I currently have and it works on PC, but not on Anroid + Chrome
<b-card #click="nextTrack({whatToDo: 'playTrackObj', trackObj: trackObj})"
class="dragula-move w-100 pl-2" no-body>
{{ index + 1 }}. {{ trackObj.trackTitle }}
</b-card>
</div>
</template>
<script>
import { mapActions } from 'vuex'
export default {
methods: {
...mapActions('header', [
'nextTrack'
])
},
// This work on Android + Chrome
mounted () {
// let self = this
// let allTracks = document.getElementsByClassName('card dragula-move w-100 pl-2')
// for (let i = 0; i < allTracks.length; i++) {
// allTracks[i].addEventListener('click', function (event) {
// self.nextTrack({whatToDo: 'playDefaultTrack'})
// })
// }
}
}
</script>
If I remove #click="nextTrack(...) from <b-card> and uncomment the code in mounted (), Chrome on Android starts to see user's interaction and starts track playing.
Is there any way how to help Chrome on Android to see user's clicks using +- my existing code (ie #click="nextTrack(...)) or can anyone show me how it is possible to pass to EventListener a trackObj from template which can be used by self.nextTrack(...) function?
#click.native does not help.
I suggest using a custom directive with those objects as arguments. See below:
Vue.directive('next-track', {
bind: function(el, binding, vnode) {
var vm = vnode.context;
el.addEventListener('click', function(event) {
vm.nextTrack({whatToDo: binding.value.whatToDo})
})
}
})
new Vue({
el: '#app',
data: {
message: 'hello!',
trackObj: 'something'
},
methods: {
nextTrack(arg) {
console.log('nextTrack called with', JSON.stringify(arg))
}
}
})
<script src="https://unpkg.com/vue#2.5.13/dist/vue.min.js"></script>
<div id="app">
<div v-next-track="{whatToDo: 'playTrackObj', trackObj: trackObj}">
playTrackObj
</div>
<br>
<div v-next-track="{whatToDo: 'click uss!', trackObj: trackObj}">
click us and check the console
</div>
</div>

Scroll to bottom of div with Vue.js

I have a Vue.js component with several elements in it. I want to automatically scroll to the bottom of that element when a method in the component is called.
Basically, do the same as this. However, I haven't found a way to get the element within my component and modify scrollTop
I'm currently using Vue.js 2.0.8.
2022 easy, readable, smooth scrolling ability, & won't hurt your brain... use el.scrollIntoView()
scrollIntoView() has options you can pass it like scrollIntoView({behavior: 'smooth'}) to get smooth scrolling out of the box and does not require any external libraries.
Here is a fiddle.
methods: {
scrollToElement() {
const el = this.$refs.scrollToMe;
if (el) {
// Use el.scrollIntoView() to instantly scroll to the element
el.scrollIntoView({behavior: 'smooth'});
}
}
}
Then if you wanted to scroll to this element on page load you could call this method like this:
mounted() {
this.scrollToElement();
}
Else if you wanted to scroll to it on a button click or some other action you could call it the same way:
<button #click="scrollToElement">scroll to me</button>
The scroll works all the way down to IE 8. The smooth scroll effect does not work out of the box in IE or Safari. If needed there is a polyfill available for this here as #mostafaznv mentioned in the comments.
As I understand, the desired effect you want is to scroll to the end of a list (or scrollable div) when something happens (e.g.: an item is added to the list). If so, you can scroll to the end of a container element (or even the page it self) using only pure JavaScript and the VueJS selectors.
var container = this.$el.querySelector("#container");
container.scrollTop = container.scrollHeight;
I've provided a working example in this fiddle.
Every time a item is added to the list, the list is scrolled to the end to show the new item.
I tried the accepted solution and it didn't work for me. I use the browser debugger and found out the actual height that should be used is the clientHeight BUT you have to put this into the updated() hook for the whole solution to work.
data(){
return {
conversation: [
{
}
]
},
mounted(){
EventBus.$on('msg-ctr--push-msg-in-conversation', textMsg => {
this.conversation.push(textMsg)
// Didn't work doing scroll here
})
},
updated(){ <=== PUT IT HERE !!
var elem = this.$el
elem.scrollTop = elem.clientHeight;
},
Use the ref attribute on the DOM element for reference
<div class="content scrollable" ref="msgContainer">
<!-- content -->
</div>
You need to setup a WATCH
data() {
return {
count: 5
};
},
watch: {
count: function() {
this.$nextTick(function() {
var container = this.$refs.msgContainer;
container.scrollTop = container.scrollHeight + 120;
});
}
}
Ensure you're using proper CSS
.scrollable {
overflow: hidden;
overflow-y: scroll;
height: calc(100vh - 20px);
}
Here is a simple example using ref to scroll to the bottom of a div.
/*
Defined somewhere:
var vueContent = new Vue({
el: '#vue-content',
...
*/
var messageDisplay = vueContent.$refs.messageDisplay;
messageDisplay.scrollTop = messageDisplay.scrollHeight;
<div id='vue-content'>
<div ref='messageDisplay' id='messages'>
<div v-for="message in messages">
{{ message }}
</div>
</div>
</div>
Notice that by putting ref='messageDisplay' in the HTML, you have access to the element through vueContent.$refs.messageDisplay
If you need to support IE11 and (old) Edge, you can use:
scrollToBottom() {
let element = document.getElementById("yourID");
element.scrollIntoView(false);
}
If you don't need to support IE11, the following will work (clearer code):
scrollToBottom() {
let element = document.getElementById("yourID");
element.scrollIntoView({behavior: "smooth", block: "end"});
}
Try vue-chat-scroll:
Install via npm: npm install --save vue-chat-scroll
Import:
import Vue from 'vue'
import VueChatScroll from 'vue-chat-scroll'
Vue.use(VueChatScroll)
in app.js after window.Vue = require('vue').default;
then use it with :
<ul class="messages" v-chat-scroll>
// your message/chat code...
</ul>
For those that haven't found a working solution above, I believe I have a working one. My specific use case was that I wanted to scroll to the bottom of a specific div - in my case a chatbox - whenever a new message was added to the array.
const container = this.$el.querySelector('#messagesCardContent');
this.$nextTick(() => {
// DOM updated
container.scrollTop = container.scrollHeight;
});
I have to use nextTick as we need to wait for the dom to update from the data change before doing the scroll!
I just put the above code in a watcher for the messages array, like so:
messages: {
handler() {
// this scrolls the messages to the bottom on loading data
const container = this.$el.querySelector('#messagesCard');
this.$nextTick(() => {
// DOM updated
container.scrollTop = container.scrollHeight;
});
},
deep: true,
},
The solution did not work for me but the following code works for me. I am working on dynamic items with class of message-box.
scrollToEnd() {
setTimeout(() => {
this.$el
.getElementsByClassName("message-box")
[
this.$el.getElementsByClassName("message-box").length -
1
].scrollIntoView();
}, 50);
}
Remember to put the method in mounted() not created() and add class message-box to the dynamic item. setTimeout() is essential for this to work. You can refer to https://forum.vuejs.org/t/getelementsbyclassname-and-htmlcollection-within-a-watcher/26478 for more information about this.
This is what worked for me
this.$nextTick(() => {
let scrollHeight = this.$refs.messages.scrollHeight
window.scrollTo(0, scrollHeight)
})
In the related question you posted, we already have a way to achieve that in plain javascript, so we only need to get the js reference to the dom node we want to scroll.
The ref attribute can be used to declare reference to html elements to make them available in vue's component methods.
Or, if the method in the component is a handler for some UI event, and the target is related to the div you want to scroll in space, you can simply pass in the event object along with your wanted arguments, and do the scroll like scroll(event.target.nextSibling).
I had the same need in my app (with complex nested components structure) and I unfortunately did not succeed to make it work.
Finally I used vue-scrollto that works fine !
My solutions without modules:
Template
<div class="scrollable-content" ref="conversations" />
Script
scrollToBottom() {
const container = this.$refs.conversations;
container.scrollTop = container.scrollHeight;
},
scrollToBottom() {
this.$nextTick(function () {
let BoxEl = document.querySelector('#Box');
if(BoxEl)
BoxEl.scrollTop = BoxEl.scrollHeight;
});
}
Agree with Lurein Perera
Just want to add extra info
watch: {
arrayName: {
handler() {
const container = this.$el.querySelector("#idName");
this.$nextTick(() => {
container.scrollTop = container.scrollHeight;
});
},
deep: true,
},
},
Where as:
arrayName = Name of array
idName = The id attribute has to be added to the div where you want the scrollbar to auto-scroll down when arrayName length increases.
scrollToElement() {
const element = this.$refs.abc; // here abc is the ref of the element
if (element) {
el.scrollIntoView({behavior: 'smooth'});
}
}
}
here you need to use ref for the particular div or element which you want make visible on scroll.
if you have a table and you want to locate the last row of the table then you have to use -
element.lastElementChild.scrollIntoView({behaviour:'smooth'})
Here not that if you ware asynchronously adding the element to the table then you have to take care of it. you can test it using setTimeout, if that is making any difference.
e.g.
const element = this.$refs.abc;
if (element) {
setTimeout(() => {
element.lastElementChild.scrollIntoView({behaviour:'smooth'})
}, 1000);
}
}
replace set timeout with your own async logic.
Using Composition API and TypeScript
I set the parameter scrollTop equal to scrollHeightfrom the HTMLDivElment API.
<template>
<div id="container" ref="comments">
Content ...
</div>
</template>
<script lang="ts">
import { defineComponent, ref, Ref, watchEffect } from 'vue'
export default defineComponent({
setup() {
const comments: Ref<null | HTMLDivElement> = ref(null)
watchEffect(() => {
if(comments.value) {
comments.value.scrollTop = comments.value.scrollHeight
}
})
return {
comments
}
}
})
</script>

Categories

Resources