VueJS / JS DOM Watch / Observer in a multi phase render scenario - javascript

Scenario:
I’m developing a Vue scroll component that wraps around a dynamic number of HTML sections and then dynamically builds out vertical page navigation allowing the user to scroll or jump to page locations onScroll.
Detail:
a. In my example my scroll component wraps 3 sections. All section id’s start with "js-page-section-{{index}}"
b. The objective is to get the list of section nodes (above) and then dynamically build out vertical page (nav) navigation based on the n number of nodes found in the query matching selector criteria. Therefore, three sections will result in three page section navigation items. All side navigation start with “js-side-nav-{{index}}>".
c. Once the side navigation is rendered I need to query all the navigation nodes in order to control classes, heights, display, opacity, etc. i.e document.querySelectorAll('*[id^="js-side-nav"]');
EDIT
Based on some research here are the options for my problem. Again my problem being 3 phase DOM state management i.e. STEP 1. Read all nodes equal to x, then STEP 2. Build Side Nav scroll based on n number of nodes in document, and then STEP 3. Read all nav nodes to sync with scroll of document nodes:
Create some sort of event system is $emit() && $on. In my opinion this gets messy very quickly and feels like a poor solution. I found myself quickly jumping to $root
Vuex. but that feels like an overkill
sync. Works but really that is for parent child property state management but that again requires $emit() && $on.
Promise. based service class. This seems like the right solution, but frankly it became a bit of pain managing multiple promises.
I attempted to use Vue $ref but frankly it seems better for managing state rather than multi stage DOM manipulation where a observer event approach is better.
The solution that seems to work is Vues $nextTick(). which seems to be similar to AngularJS $digest. In essence it is a . setTimeout(). type approach just pausing for next digest cycle. That said there is the scenario where the tick doesn’t sync the time requires so I built a throttle method. Below is the code update for what is worth.
The refactored watch with nextTick()
watch: {
'page.sections': {
handler(nodeList, oldNodeList){
if (this.isNodeList(nodeList) && _.size(nodeList) && this.sideNavActive) {
return this.$nextTick(this.sideNavInit);
}
},
deep: true
},
},
The REFACTORED Vue component
<template>
<div v-scroll="handleScroll">
<nav class="nav__wrapper" id="navbar-example">
<ul class="nav">
<li role="presentation"
:id="sideNavPrefix + '-' + (index + 1)"
v-for="(item, key,index) in page.sections">
<a :href="'#' + getAttribute(item,'id')">
<p class="nav__counter" v-text="('0' + (index + 1))"></p>
<h3 class="nav__title" v-text="getAttribute(item,'data-title')"></h3>
<p class="nav__body" v-text="getAttribute(item,'data-body')"></p>
</a>
</li>
</ul>
</nav>
<slot></slot>
</div>
</template>
<script>
import ScrollPageService from '../services/ScrollPageService.js';
const _S = "section", _N = "sidenavs";
export default {
name: "ScrollSection",
props: {
nodeId: {
type: String,
required: true
},
sideNavActive: {
type: Boolean,
default: true,
required: false
},
sideNavPrefix: {
type: String,
default: "js-side-nav",
required: false
},
sideNavClass: {
type: String,
default: "active",
required: false
},
sectionClass: {
type: String,
default: "inview",
required: false
}
},
directives: {
scroll: {
inserted: function (el, binding, vnode) {
let f = function(evt) {
if (binding.value(evt, el)) {
window.removeEventListener('scroll', f);
}
};
window.addEventListener('scroll', f);
}
},
},
data: function () {
return {
scrollService: {},
page: {
sections: {},
sidenavs: {}
}
}
},
methods: {
getAttribute: function(element, key) {
return element.getAttribute(key);
},
updateViewPort: function() {
if (this.scrollService.isInCurrent(window.scrollY)) return;
[this.page.sections, this.page.sidenavs] = this.scrollService.updateNodeList(window.scrollY);
},
handleScroll: function(evt, el) {
if ( !(this.isScrollInstance()) ) {
return this.$nextTick(this.inViewportInit);
}
this.updateViewPort();
},
getNodeList: function(key) {
this.page[key] = this.scrollService.getNodeList(key);
},
isScrollInstance: function() {
return this.scrollService instanceof ScrollPageService;
},
sideNavInit: function() {
if (this.isScrollInstance() && this.scrollService.navInit(this.sideNavPrefix, this.sideNavClass)) this.getNodeList(_N);
},
inViewportInit: function() {
if (!(this.isScrollInstance()) && ((this.scrollService = new ScrollPageService(this.nodeId, this.sectionClass)) instanceof ScrollPageService)) this.getNodeList(_S);
},
isNodeList: function(nodes) {
return NodeList.prototype.isPrototypeOf(nodes);
},
},
watch: {
'page.sections': {
handler(nodeList, oldNodeList){
if (this.isNodeList(nodeList) && _.size(nodeList) && this.sideNavActive) {
return this.$nextTick(this.sideNavInit);
}
},
deep: true
},
},
mounted() {
return this.$nextTick(this.inViewportInit);
},
}
</script>
END EDIT
ORIGINAL POST
Problem & Question:
PROBLEM:
The query of sections and render of navs work fine. However, querying the nav elements fails as the DOM has not completed the render. Therefore, I’m forced to use a setTimeout() function. Even if I use a watch I’m still forced to use timeout.
QUESTION:
Is there a promise or observer in Vue or JS I can use to check to see when the DOM has finished rendering the nav elements so that I can then read them? Example in AngularJS we might use $observe
HTML EXAMPLE
<html>
<head></head>
<body>
<scroll-section>
<div id="js-page-section-1"
data-title="One"
data-body="One Body">
</div>
<div id="js-page-section-2"
data-title="Two"
data-body="Two Body">
</div>
<div id="js-page-section-3"
data-title="Three"
data-body="THree Body">
</div>
</scroll-section>
</body>
</html>
Vue Compenent
<template>
<div v-scroll="handleScroll">
<nav class="nav__wrapper" id="navbar-example">
<ul class="nav">
<li role="presentation"
:id="[idOfSideNav(key)]"
v-for="(item, key,index) in page.sections.items">
<a :href="getId(item)">
<p class="nav__counter">{{key}}</p>
<h3 class="nav__title" v-text="item.getAttribute('data-title')"></h3>
<p class="nav__body" v-text="item.getAttribute('data-body')"></p>
</a>
</li>
</ul>
</nav>
<slot></slot>
</div>
</template>
<script>
export default {
name: "ScrollSection",
directives: {
scroll: {
inserted: function (el, binding, vnode) {
let f = function(evt) {
_.forEach(vnode.context.page.sections.items, function (elem,k) {
if (window.scrollY >= elem.offsetTop && window.scrollY <= (elem.offsetTop + elem.offsetHeight)) {
if (!vnode.context.page.sections.items[k].classList.contains("in-viewport") ) {
vnode.context.page.sections.items[k].classList.add("in-viewport");
}
if (!vnode.context.page.sidenavs.items[k].classList.contains("active") ) {
vnode.context.page.sidenavs.items[k].classList.add("active");
}
} else {
if (elem.classList.contains("in-viewport") ) {
elem.classList.remove("in-viewport");
}
vnode.context.page.sidenavs.items[k].classList.remove("active");
}
});
if (binding.value(evt, el)) {
window.removeEventListener('scroll', f);
}
};
window.addEventListener('scroll', f);
},
},
},
data: function () {
return {
page: {
sections: {},
sidenavs: {}
}
}
},
methods: {
handleScroll: function(evt, el) {
// Remove for brevity
},
idOfSideNav: function(key) {
return "js-side-nav-" + (key+1);
},
classOfSideNav: function(key) {
if (key==="0") {return "active"}
},
elementsOfSideNav:function() {
this.page.sidenavs = document.querySelectorAll('*[id^="js-side-nav"]');
},
elementsOfSections:function() {
this.page.sections = document.querySelectorAll('*[id^="page-section"]');
},
},
watch: {
'page.sections': function (val) {
if (_.has(val,'items') && _.size(val.items)) {
var self = this;
setTimeout(function(){
self.elementsOfSideNavs();
}, 300);
}
}
},
mounted() {
this.elementsOfSections();
},
}
</script>

I hope I can help you with what I'm going to post here. A friend of mine developed a function that we use in several places, and reading your question reminded me of it.
"Is there a promise or observer in Vue or JS I can use to check to see when the DOM has finished rendering the nav elements so that I can then read them?"
I thought about this function (source), here below. It takes a function (observe) and tries to satisfy it a number of times.
I believe you can use it at some point in component creation or page initialization; I admit that I didn't understand your scenario very well. However, some points of your question immediately made me think about this functionality. "...wait for something to happen and then make something else happen."
<> Credits to #Markkop the creator of that snippet/func =)
/**
* Waits for object existence using a function to retrieve its value.
*
* #param { function() : T } getValueFunction
* #param { number } [maxTries=10] - Number of tries before the error catch.
* #param { number } [timeInterval=200] - Time interval between the requests in milis.
* #returns { Promise.<T> } Promise of the checked value.
*/
export function waitForExistence(getValueFunction, maxTries = 10, timeInterval = 200) {
return new Promise((resolve, reject) => {
let tries = 0
const interval = setInterval(() => {
tries += 1
const value = getValueFunction()
if (value) {
clearInterval(interval)
return resolve(value)
}
if (tries >= maxTries) {
clearInterval(interval)
return reject(new Error(`Could not find any value using ${tries} tentatives`))
}
}, timeInterval)
})
}
Example
function getPotatoElement () {
return window.document.querySelector('#potato-scroller')
}
function hasPotatoElement () {
return Boolean(getPotatoElement())
}
// when something load
window.document.addEventListener('load', async () => {
// we try sometimes to check if our element exists
const has = await waitForExistence(hasPotatoElement)
if (has) {
// and if it exists, we do this
doThingThatNeedPotato()
}
// or you could use a promise chain
waitForExistence(hasPotatoElement)
.then(returnFromWaitedFunction => { /* hasPotatoElement */
if (has) {
doThingThatNeedPotato(getPotatoElement())
}
})
})

Related

Vue v-html renders [object HTMLQuoteElement]

I'm trying to create a generic slideshow component using slots. I'm able to access the elements passed into the slot, and when I log them they appear to be DOM elements, but when they appear on the page they're just [object HTMLQuoteElement].
Here's the component:
<template>
<div :class="cssClasses">
<div v-show="false">
<slot></slot>
</div>
<transition v-if="slots.length" name="carousel-fade" tag="div">
<div v-html="currentSlide"></div>
</transition>
</div>
</template>
<script>
export default {
props: {
// Classes to apply to wrapping element.
cssClasses: { default: '' },
// Integer for miliseconds to remain on each slide.
interval: { default: 5000 }
},
data() {
return {
// Internal: Timer for playing through slides.
timer: null,
// Internal: Current slide.
index: 0
}
},
computed: {
currentSlide() {
if (this.$slots.default[this.index].elm) {
console.log('currentSlide', this.$slots.default[this.index].elm) // prints correctly
return this.$slots.default[this.index].elm // renders wrong
}
return ''
},
slots() {
return this.$slots.default
}
},
methods: {
// Internal: Start slideshow.
startSlide() {
if (this.timer) clearInterval(this.timer)
this.timer = setInterval(this.next, this.interval)
},
// Internal: Render next slide. If at end, return to the beginning.
next() {
this.startSlide()
if (this.index === this.slots.length - 1) {
this.index = 0
} else {
this.index += 1
}
}
},
mounted() {
this.startSlide()
}
}
</script>
The goal is to be able to pass any html elements into the slot so the slideshow goes through them. At this point I'm just adding nonsense text because stack overflow won't let me post my question because it's mostly code. But really, there isn't much more to say. You can read the code. You know what I'm trying to do. If I'm doing something wrong, let me know. There isn't a point in me wasting both of our time so a robot can count characters to decide one has enough information.

TipTap Vue component - how to toggle wrap on node from component button

The awesome tiptap wrapper for prosemirror comes with nice documentation but it lacks some clarification how to approach some (i think) basic scenarios when developing custom extensions.
My question is how to invoke toggleWrap on the node when in vue component's context.
I found example that uses transactions and allows for delete - but what i want is to clear the node leaving the text of node intact.
get view() {
return {
directives: {
"click-outside": clickOutside
},
props: ['node', 'updateAttrs', 'view', 'selected', 'getPos'],
data() {
return {
showMenu: false
}
},
computed: {
href: {
get() {
return this.node.attrs.href
},
set(href) {
this.updateAttrs({
href,
})
},
},
},
methods: {
// deleteNode() {
// let transaction = this.view.state.tr // tr - transaction
// let pos = this.getPos()
// transaction.delete(pos, pos + this.node.nodeSize)
// this.view.dispatch(transaction)
// },
stopLinkPropagation(){
return null;
},
hideMenu(){
this.showMenu = false
}
},
template: `<div #click="showMenu = true" v-click-outside="hideMenu">
<a class="email-button" #click.prevent="stopLinkPropagation" :href="href" v-text="node.textContent"></a>
<input class="iframe__input" type="text" v-model="href" v-if="showMenu" />
<button #click="clearNode">clear button wrap</button>
</div>`,
}
}
Any help would be awesome. Thanks.

Vuejs: Passing SAVE function into CRUD component

I am struggeling with a proper solution which requires an advanced parent-child communication in vuejs. There can be many different parent components which has a logic how to save data. From the other side there will be only one child component which has a list of elements and a form to create new elements but it doesn't know how to save the data.
The question is: Is there any other way (better approach) to have the same functionality but to get rid of this.$refs.child links. For example I am wondering if I can just pass a function (SaveParent1(...) or SaveParent2(...)) to the child component. But the problem is the function contains some parent's variables which won't be available in child context and those variables could be changed during the runtime.
Just few clarifications:
The methods SaveParent1 and SaveParent2 in real life return
Promise (axios).
The child-component is like a CRUD which is used
everywhere else.
At the moment the communication looks like that: CHILD -event-> PARENT -ref-> CHILD.
Bellow is the example:
<div id="app">
<h2>😀Advanced Parent-Child Communication:</h2>
<parent-component1 param1="ABC"></parent-component1>
<parent-component2 param2="XYZ"></parent-component2>
</div>
Vue.component('parent-component1', {
props: { param1: { type: String, required: true } },
methods: {
onChildSubmit(p) {
// Here will be some logic to save the param. Many different parents might have different logic and all of them use the same child component. So child-component contains list, form and validation message but does not know how to save the param to the database.
var error = SaveParent1({ form: { p: p, param1: this.param1 } });
if (error)
this.$refs.child.paramFailed(error);
else
this.$refs.child.paramAdded(p);
}
},
template: `<div class="parent"><p>Here is parent ONE:</p><child-component ref="child" #submit="onChildSubmit"></child-component></div>`
});
Vue.component('parent-component2', {
props: { param2: { type: String, required: true } },
methods: {
onChildSubmit(p) {
// Here is a different logic to save the param. In prictice it is gonna be different requests to the server.
var error = SaveParent2({ form: { p: p, param2: this.param2 } });
if (error)
this.$refs.child.paramFailed(error);
else
this.$refs.child.paramAdded(p);
}
},
template: `<div class="parent"><p>Here is parent TWO:</p><child-component ref="child" #submit="onChildSubmit"></child-component></div>`
});
Vue.component('child-component', {
data() {
return {
currentParam: "",
allParams: [],
errorMessage: ""
}
},
methods: {
submit() {
this.errorMessage = "";
this.$emit('submit', this.currentParam);
},
paramAdded(p) {
this.currentParam = "";
this.allParams.push(p);
},
paramFailed(msg) {
this.errorMessage = msg;
}
},
template: `<div><ol><li v-for="p in allParams">{{p}}</li></ol><label>Add Param: <input v-model="currentParam"></label><button #click="submit" :disabled="!currentParam">Submit</button><p class="error">{{errorMessage}}</p></div>`
});
function SaveParent1(data) {
// Axios API to save data. Bellow is a simulation.
if (Math.random() > 0.5)
return null;
else
return 'Parent1: You are not lucky today';
}
function SaveParent2(data) {
// Axios API to save data. Bellow is a simulation.
if (Math.random() > 0.5)
return null;
else
return 'Parent2: You are not lucky today';
}
new Vue({
el: "#app"
});
There is also a live demo available: https://jsfiddle.net/FairKing/novdmcxp/
Architecturally I recommend having a service that is completely abstract from the component hierarchy and that you can inject and use in each of the components. With this kind of component hierarchy and architecture it is easy to run into these issues. It is important to abstract as much functionality and business logic from the components as possible. I think of components in these modern frameworks just merely as HTML templates on steroids, which should at most act as controllers, keeping them as dumb and as thin as possible so that you don't run into these situations. I do not know vue.js so I cannot give you the technical solution but hope this indication helps
I think I have found a solution. So no two ways communication. I can just pass a method and the child will do everything without communicating with parent. I am happy with that I am marking it as an answer. Thanks everyone for your help.
Let me please know what do you think guys.
Bellow is my solution:
<div id="app">
<h2>😀Advanced Parent-Child Communication:</h2>
<parent-component1 param1="ABC"></parent-component1>
<parent-component2 param2="XYZ"></parent-component2>
</div>
Vue.component('parent-component1', {
props: { param1: { type: String, required: true } },
computed: {
saveFunc() {
return function(p) { SaveParent1({ form: { p: p, param1: this.param1 } }); }.bind(this);
}
},
template: `<div class="parent"><p>Here is parent ONE:</p><child-component :saveFunc="saveFunc"></child-component></div>`
});
Vue.component('parent-component2', {
props: { param2: { type: String, required: true } },
computed: {
saveFunc() {
return function(p) { SaveParent2({ form: { p: p, param2: this.param2 } }); }.bind(this);
}
},
template: `<div class="parent"><p>Here is parent TWO:</p><child-component :saveFunc="saveFunc"></child-component></div>`
});
Vue.component('child-component', {
props: {
saveFunc: { type: Function, required: true }, // This is gonna be a Promise in real life.
},
data() {
return {
currentParam: "",
allParams: [],
errorMessage: ""
}
},
methods: {
submit() {
this.errorMessage = "";
var error = this.saveFunc(this.currentParam);
if (error)
this.paramFailed(error);
else
this.paramAdded(this.currentParam);
},
paramAdded(p) {
this.currentParam = "";
this.allParams.push(p);
},
paramFailed(msg) {
this.errorMessage = msg;
}
},
template: `<div><ol><li v-for="p in allParams">{{p}}</li></ol><label>Add Param: <input v-model="currentParam"></label><button #click="submit" :disabled="!currentParam">Submit</button><p class="error">{{errorMessage}}</p></div>`
});
function SaveParent1(data) {
console.log(data);
// Axios API to save data
if (Math.random() > 0.5)
return null;
else
return 'Parent1: You are not lucky today';
}
function SaveParent2(data) {
console.log(data);
// Axios API to save data
if (Math.random() > 0.5)
return null;
else
return 'Parent2: You are not lucky today';
}
new Vue({
el: "#app"
});
The demo link: https://jsfiddle.net/FairKing/novdmcxp/126/

Vue.js component model update

Im absolutely new in Vue framework and I need create reusable component with live BTC/LTC/XRP price
For live prices Im using Bitstamp websockets API. Here is example usage with jQuery - run this snippet, is really live.
var bitstamp = new Pusher('de504dc5763aeef9ff52')
var channel = bitstamp.subscribe('live_trades')
channel.bind('trade', function (lastTrade) {
$('p').text(lastTrade.price)
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pusher/4.1.0/pusher.min.js"></script>
<h3>BTC/USD price</h3>
<p>loading...</p>
As you can see, its really simple. But, I need to use Vue.js component. So I created this, and its also fully functional:
var bitstamp = new Pusher('de504dc5763aeef9ff52')
Vue.component('live-price', {
template: '<div>{{price}}</div>',
data: function () {
return {
price: 'loading...'
}
},
created: function () {
this.update(this)
},
methods: {
update: function (current) {
var pair = current.$attrs.pair === 'btcusd'
? 'live_trades'
: 'live_trades_' + current.$attrs.pair
var channel = bitstamp.subscribe(pair)
channel.bind('trade', function (lastTrade) {
current.price = lastTrade.price
})
}
}
})
new Vue({
el: '.prices'
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/pusher/4.1.0/pusher.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.4.1/vue.min.js"></script>
<section class="prices">
<live-price pair="btcusd"></live-price>
<live-price pair="ltcusd"></live-price>
<live-price pair="xrpusd"></live-price>
</section>
But, there is big BUT. Am I using Vue right way? WHERE IS IDEAL PLACE to run Pusher? In "created" or "mounted" method? In "computed"? In "watch"? Or where? Am i doing it right? I really dont known, I started with Vue ... today :(
Looks pretty good for your first day using Vue! I would just make a few changes.
The component is reaching out and using a global, bitstamp. Generally with components, you want them to be independent, and not reaching out of themselves to get values. To that end, declare the socket as a property that can be passed in to the component.
Likewise, the pair is passed in as a property, but you do not declare it and instead, use current.$attrs.pair to get the pair. But that's not very declarative and makes it harder for anyone else to use the component. Moreover, by making it a property, you can reference it using this.pair.
When using something like a socket, you should always remember to clean up when you are done using it. In the code below, I added the unsubscribe method to do so. beforeDestroy is a typical lifecycle hook to handle these kinds of things.
Computed properties are useful for calculating values that are derived from your components data: the channel you are subscribing to is a computed property. You don't really need to do this, but its generally good practice.
A Vue can only bind to a single DOM element. You are using a class .prices which works in this case because there is only one element with that class, but could be misleading down the road.
Finally, created is an excellent place to initiate your subscription.
console.clear()
var bitstamp = new Pusher('de504dc5763aeef9ff52')
Vue.component('live-price', {
props:["pair", "socket"],
template: '<div>{{price}}</div>',
data() {
return {
price: 'loading...',
subscription: null
}
},
created() {
this.subscribe()
},
beforeDestroy(){
this.unsubscribe()
},
computed:{
channel(){
if (this.pair === 'btcusd')
return 'live_trades'
else
return 'live_trades_' + this.pair
}
},
methods: {
onTrade(lastTrade){
this.price = lastTrade.price
},
subscribe() {
this.subscription = this.socket.subscribe(this.channel)
this.subscription.bind('trade', this.onTrade)
},
unsubscribe(){
this.subscription.unbind('trade', this.onTrade)
this.socket.unsubscribe(this.channel)
}
}
})
new Vue({
el: '#prices',
data:{
socket: bitstamp
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/pusher/4.1.0/pusher.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.4.1/vue.min.js"></script>
<section id="prices">
<live-price pair="btcusd" :socket="bitstamp"></live-price>
<live-price pair="ltcusd" :socket="bitstamp"></live-price>
<live-price pair="xrpusd" :socket="bitstamp"></live-price>
</section>
Rewrited - is it ok now?
var config = {
key: 'de504dc5763aeef9ff52'
}
var store = new Vuex.Store({
state: {
pusher: null
},
mutations: {
initPusher (state, payload) {
state.pusher = new Pusher(payload.key)
}
}
})
var livePrice = {
template: '#live-price',
props: ['pair'],
data () {
return {
price: 'loading...',
subscription: null
}
},
computed: {
channel () {
return this.pair === 'btcusd'
? 'live_trades'
: 'live_trades_' + this.pair
}
},
methods: {
onTrade (lastTrade) {
this.price = lastTrade.price
},
subscribe () {
this.subscription = this.$store.state.pusher.subscribe(this.channel)
this.subscription.bind('trade', this.onTrade)
},
unsubscribe () {
this.subscription.unbind('trade', this.onTrade)
this.$store.state.pusher.unsubscribe(this.channel)
}
},
created () {
this.subscribe()
},
beforeDestroy () {
this.unsubscribe()
}
}
new Vue({
el: '#prices',
store,
components: {
'live-price': livePrice
},
created () {
store.commit({
type: 'initPusher',
key: config.key
})
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/pusher/4.1.0/pusher.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.4.1/vue.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vuex/2.3.1/vuex.min.js"></script>
<section id="prices">
<live-price pair="btcusd"></live-price>
<live-price pair="ltcusd"></live-price>
<live-price pair="xrpusd"></live-price>
</section>
<template id="live-price">
<div>
{{price}}
</div>
</template>

vue.js: how to handle click and dblclick events on same element

I have a vue component with separate events for click/dblclick. Single click (de)selects row, dblclick opens edit form.
<ul class="data_row"
v-for="(row,index) in gridData"
#dblclick="showEditForm(row,$event)"
#click="rowSelect(row,$event)"
>
Doing it like this, i get 3 events fired on double click. Two click events and lastly one dblclick. Since the click event fires first , is there a way (short of deferring click event for a fixed amount of ms) for stopping propagation of click event on double click ?
Fiddle here
As suggested in comments, You can simulate the dblclick event by setting up a timer for a certain period of time(say x).
If we do not get another click during that time span, go for the single_click_function().
If we do get one, call double_click_function().
Timer will be cleared once the second click is received.
It will also be cleared once x milliseconds are lapsed.
See below code and working fiddle.
new Vue({
el: '#app',
data: {
result: [],
delay: 700,
clicks: 0,
timer: null
},
mounted: function() {
console.log('mounted');
},
methods: {
oneClick(event) {
this.clicks++;
if (this.clicks === 1) {
this.timer = setTimeout( () => {
this.result.push(event.type);
this.clicks = 0
}, this.delay);
} else {
clearTimeout(this.timer);
this.result.push('dblclick');
this.clicks = 0;
}
}
}
});
<div id="example-1">
<button v-on:dblclick="counter += 1, funcao()">Add 1</button>
<p>The button above has been clicked {{ counter }} times.</p>
</div>
var example1 = new Vue({
el: '#example-1',
data: {
counter: 0
},
methods: {
funcao: function(){
alert("Sou uma funcao");
}
}
})
check out this working fiddle https://codepen.io/robertourias/pen/LxVNZX
i have a simpler solution i think (i'm using vue-class but same principle apply):
private timeoutId = null;
onClick() {
if(!this.timeoutId)
{
this.timeoutId = setTimeout(() => {
// simple click
}, 50);//tolerance in ms
}else{
clearTimeout(this.timeoutId);
// double click
}
}
it does not need to count the number of clicks.
The time must be short between click and click.
In order to get the click and double click, only one counter is required to carry the number of clicks(for example 0.2s) and it is enough to trap the user's intention when he clicks slowly or when he performs several that would be the case of the double click or default case.
I leave here with code how I implement these features.
new Vue({
el: '#app',
data: {numClicks:0, msg:''},
methods: {
// detect click event
detectClick: function() {
this.numClicks++;
if (this.numClicks === 1) { // the first click in .2s
var self = this;
setTimeout(function() {
switch(self.numClicks) { // check the event type
case 1:
self.msg = 'One click';
break;
default:
self.msg = 'Double click';
}
self.numClicks = 0; // reset the first click
}, 200); // wait 0.2s
} // if
} // detectClick function
}
});
span { color: red }
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.2.0/vue.js"></script>
<div id='app'>
<button #click='detectClick'>
Test Click Event, num clicks
<span>{{ numClicks }}</span>
</button>
<h2>Last Event: <span>{{ msg }}</span></h2>
</div>
I use this approach for the same problem. I use a promise that is resolved either by the timeout of 200ms being triggered, or by a second click being detected. It works quite well in my recent web apps.
<div id="app">
<div
#click="clicked().then((text) => {clickType = text})">
{{clickType}}
</div>
</div>
<script>
new Vue({
el: "#app",
data: {
click: undefined,
clickType: 'Click or Doubleclick ME'
},
methods: {
clicked () {
return new Promise ((resolve, reject) => {
if (this.click) {
clearTimeout(this.click)
resolve('Detected DoubleClick')
}
this.click = setTimeout(() => {
this.click = undefined
resolve('Detected SingleClick')
}, 200)
})
}
}
})
</script>
Working fiddle:
https://jsfiddle.net/MapletoneMartin/9m62Lrwf/
vue Component
// html
<div class="grid-content">
<el-button
   #click.native="singleClick"
   #dblclick.native="doubleClick"
   class="inline-cell">
click&dbclickOnSameElement</el-button>
</div>
// script
<script>
let time = null; // define time be null
export default {
name: 'testComponent',
data() {
return {
test:''
};
},
methods: {
singleClick() {
// first clear time
clearTimeout(time);
time = setTimeout(() => {
console.log('single click ing')
}, 300);
},
  
doubleClick() {
clearTimeout(time);
console.log('double click ing');
}
}
}
</script>
selectedFolder = ''; // string of currently selected item
folderSelected = false; // preview selected item
selectFolder(folder) {
if (this.selectedFolder == folder) {
// double click
this.folderSelected = false;
this.$store.dispatch('get_data_for_this_folder', folder);
} else {
// single click
this.selectedFolder = folder;
this.folderSelected = true;
}
},
#click.stop handles a single click and #dblclick.stop handles double click
<v-btn :ripple="false"
class="ma-0"
#click.stop="$emit('editCompleteGrvEvent', props.item)"
#dblclick.stop="$emit('sendCompleteGrvEvent',props.item)">
<v-icon>send</v-icon>
</v-btn>
Unless you need to do expensive operations on single select, you can rework rowSelect into a toggle. Setting a simple array is going to be a lot faster, reliable, and more straightforward compared to setting up and canceling timers. It won't matter much if the click event fires twice, but you can easily handle that in the edit function.
<template>
<ul>
<li :key="index" v-for="(item, index) in items">
<a
:class="{ 'active-class': selected.indexOf(item) !== -1 }"
#click="toggleSelect(item)"
#dblclick="editItem(item)"
>
{{ item.title }}
</a>
<!-- Or use a checkbox with v-model
<label #dblclick="editItem(item)">
<input type="checkbox" :value="item.id" v-model.lazy="selected" />
{{ item.title }}
</label>
-->
</li>
</ul>
</template>
<script>
export default {
data: function () {
return {
items: [
{
id: 1,
title: "Item 1",
},
{
id: 2,
title: "Item 2",
},
{
id: 3,
title: "Item 3",
},
],
selected: [],
};
},
methods: {
editItem(item) {
/*
* Optionally put the item in selected
* A few examples, pick one that works for you:
*/
// this.toggleSelect(item); // If the item was selected before dblclick, it will still be selected. If it was unselected, it will still be unselected.
// this.selected = []; // Unselect everything.
// Make sure this item is selected:
// let index = this.selected.indexOf(item.id);
// if (index === -1) {
// this.selected.push(item.id);
// }
// Make sure this item is unselected:
// let index = this.selected.indexOf(item.id);
// if (index !== -1) {
// this.selected.splice(index, 1);
// }
this.doTheThingThatOpensTheEditorHere(item);
},
toggleSelect(item) {
let index = this.selected.indexOf(item.id);
index === -1
? this.selected.push(item.id)
: this.selected.splice(index, 1);
},
// For fun, get an array of items that are selected:
getSelected() {
return this.items.filter((item) => this.selected.indexOf(item.id) !== -1);
},
},
};
</script>

Categories

Resources