Vue Draggable with touch - drop doesn't trigger - vue.js

I have been working on a website that has to function on both desktop and tablets. Part of the website is having three columns and being able to drag orders from column to column. Sometimes on drop, the user has to answer a few questions or change some of the data of that specific order. This happens in a pop-up window that is triggered by an #drop function (for example #drop="approved()". The method approved() then checks the status of the dropped order and shows the pop-up window).
When I am on desktop, everything works just fine. But when I switch to iPad Pro in the developer tools, nothing happens. I implemented Vue Draggable, which says to work with touch devices. In their examples I can't find anything about touch events or adding new handles for touch, so I don't know what to do now.
The dragging works just fine with touch devices, it's just the #drop function that doesn't trigger.
The dropzone (it includes a component that contains the draggables and a lot of if-statements):
<div class="col-md-4 border" #dragover.prevent #drop="approved()">
<Wachtrij class="fullHeight" :data2="opdrachtenData2"></Wachtrij>
</div>
The method:
export default {
methods: {
...
approved() {
console.log("Function approved() is being executed.")
if (this.draggingOrder.status === 5) {
this.popupGekeurd = true;
}
else if (this.draggingOrder.status === 6) {
this.popupTochGoed = true;
}
else if ([40, 52, 42,41,49,55,54].indexOf(this.draggingOrder.status) !== -1) {
this.back = true;
}
},
...
}
}

The problem seems to be that you are using native events, while the touch implementation does not (always?) use these events. It is intended that you use a draggable component with one of the events outlined in the documentation. In your case the start and end events look promising. This event has a few properties (docs), some of them being to and from.
Let's assume that we have the following code:
<draggable v-for="(zone, index) in zones" v-model="zones[index]" :class="['dropzone', `zone-${index}`]" :key="`dropzone-${index}`" :options="options" #start="start" #end="end">
<div v-for="item in zones[index]" class="dropitem" :key="`dropitem-${item.id}`">
{{ item.title }}
</div>
</draggable>
This creates a few zones, each filled with their own items. Each array item of zones is changed based on where you move each item. You can then use start to have information on when you start moving an item, and end to have information on when you stop moving an item, and where that item came from and where it ended up. The following methods show off what you can do with that in this case:
methods: {
start (event) {
console.log('start', event);
},
end (event) {
console.log('end', event);
const { from, to } = event;
if (to.className.match(/\bzone-2\b/)) {
console.log('Zone 2 has something added!')
}
if (from.className.match(/\bzone-0\b/)) {
console.log('Zone 0 had something removed!');
}
}
}
We make our dropzones with a class zone-0, zone-1 or zone-2 in this case, so we can use the class name to determine which dropzone we ended up in.
An alternative way to determine which zone was changed is to simply use a watcher. Since zones changes based on where you move items, you can simply watch a particular dropzone for changes and do things based on that.
watch: {
'zones.1': {
handler (oldZone, newZone) {
if (Array.isArray(oldZone) && Array.isArray(newZone) && oldZone.length !== newZone.length) {
console.log('Zone 1 was changed from', oldZone, 'to', newZone);
}
}
}
}
A full example can be found on codesandbox.

Related

Vue class components dynamically add component depending on answer from backend

So from the backend I get a array of objects that look kind of like this
ItemsToAdd
{
Page: MemberPage
Feature: Search
Text: "Something to explain said feature"
}
So i match these values to enums in the frontend and then on for example the memberpage i do this check
private get itemsForPageFeatures(): ItemsToAdd[] {
return this.items.filter(
(f) =>
f.page== Pages.MemberPage &&
f.feature != null
);
}
What we get from the backend will change a lot over time and is only the same for weeks at most. So I would like to avoid to have to add the components in the template as it will become dead code fast and will become a huge thing to have to just go around and delete dead code. So preferably i would like to add it using a function and then for example for the search feature i would have a ref on the parent like
<SearchBox :ref="Features.Search" />
and in code just add elements where the ItemsToAdd objects Feature property match the ref
is this possible in Vue? things like appendChild and so on doesn't work in Vue but that is the closest thing i can think of to kind of what I want. This function would basically just loop through the itemsForPageFeatures and add the features belonging to the page it is run on.
For another example how the template looks
<template>
<div class="container-fluid mt-3">
<div
class="d-flex flex-row justify-content-between flex-wrap align-items-center"
>
<div class="d-align-self-end">
<SearchBox :ref="Features.Search" />
</div>
</div>
<MessagesFilter
:ref="Features.MessagesFilter"
/>
<DataChart
:ref="Features.DataChart"
/>
So say we got an answer from backend where it contains an object that has a feature property DataChart and another one with Search so now i would want components to be added under the DataChart component and the SearchBox component but not the messagesFilter one as we didnt get that from the backend. But then next week we change in backend so we no longer want to display the Search feature component under searchbox. so we only get the object with DataChart so then it should only render the DataChart one. So the solution would have to work without having to make changes to the frontend everytime we change what we want to display as the backend will only be database configs that dont require releases.
Closest i can come up with is this function that does not work for Vue as appendChild doesnt work there but to help with kind of what i imagine. So the component to be generated is known and will always be the same type of component. It is where it is to be placed that is the dynamic part.
private showTextBoxes() {
this.itemsForPageFeatures.forEach((element) => {
let el = this.$createElement(NewMinorFeatureTextBox, {
props: {
item: element,
},
});
var ref = `${element.feature}`
this.$refs.ref.appendChild(el);
});
}
You can use dynamic components for it. use it like this:
<component v-for="item in itemsForPageFeatures" :is="getComponent(item.Feature)" :key="item.Feature"/>
also inside your script:
export default {
data() {
return {
items: [
{
Page: "MemberPage",
Feature: "Search",
Text: "Something to explain said feature"
}
]
};
},
computed: {
itemsForPageFeatures() {
return this.items.filter(
f =>
f.Page === "MemberPage" &&
f.Feature != null
);
}
},
methods: {
getComponent(feature) {
switch (feature) {
case "Search":
return "search-box";
default:
return "";
}
}
}
};

fire event from external controller

i'm moving to Rails 7 and i feel like there are so many changes but i'm confident on understanding them and be able to upgrade a personal applications i made myself for keeping my personal records and appointments
More specific i need to communicate between controllers (#hotwire/stimulus) between a flatpickr controller and fullcalendar contorller. The idea is to jump to a date when selecting from flatpicr
I've tried so many diferent options but i'm really stuck.. any help is welcome :)
Rails 7.0.3.1
index.html.erb
<div data-controller="flatpickr" name="" data-action=""></div>
<div data-controller="calendar">
<div data-calendar-target="window"></div>
<turbo-frame id="popup" data-calendar-target="popup"></turbo-frame>
</div>
flatpickr_controller.js
import Flatpickr from 'stimulus-flatpickr'
export default class extends Flatpickr {
connect() {
this.config = {
inline: true,
enableTime: false,
time_24hr: false,
onChange: function(selectedDates, dateStr, instance) {
const calendarController = this.application.getControllerForElementAndIdentifier(this.calendarTarget, "calendar")
calendarController.gotoDate('18-01-2025') //random date
},
};
super.connect();
}
}
calendar_controller.js
import { Controller } from "#hotwired/stimulus";
import { Calendar } from '#fullcalendar/core';
import resourceTimeGridPlugin from '#fullcalendar/resource-timegrid';
import interactionPlugin from '#fullcalendar/interaction';
export default class extends Controller {
static targets = [ "popup", "window" ];
connect() {
let overlay = this.popupTarget;
this.calendar = new Calendar(this.windowTarget, {
plugins: [ resourceTimeGridPlugin, interactionPlugin ],
themeSystem: 'bootstrap5',
initialView: 'resourceTimeGridDay',
aspectRatio: 1.8,
nowIndicator: true,
selectable: true,
editable: true,
allDaySlot: false,
});
window.addEventListener('load', () => {
this.calendar.render();
});
}
refresh(e) {
if (e.detail.success) {
this.calendar.refetchEvents();
}
}
}
output
application-7082a89999639e6d01ae0ef0aaaf6707b39fab96541f1dcd1c79da24753cb0ed.js:28271 Uncaught TypeError: Cannot read properties of undefined (reading 'getControllerForElementAndIdentifier')
at Object.onChange (ap ...
I think I'm gonna get mad with this... thank you!
Well done on trying to understand all of this, it can be hard to learn something new and especially when you have 'working' code and you are kind of forced to change.
One thing that can help is to revisit the Stimulus documentation, it does have pretty much all the answers you need for these issues but maybe needs a bit of a re-read.
The other thing which can be super frustrating is JavaScript's usage of this and how it works.
Hopefully the below breakdown helps.
Problems
1. Understanding this (JavaScript)
The first problem with the code above is that you are referencing this with the assumption that it refers to your controller instance, but rather it is referring to the event's context.
onChange: function(selectedDates, dateStr, instance) {
const calendarController = this.application.getControllerForElementAndIdentifier(this.calendarTarget, "calendar")
calendarController.gotoDate('18-01-2025') //random date
},
In the above code, this.application and this.calendarTarget will never work as the this here is the context created by the onChange handler calling context.
The quick way around this this issue is to just use an arrow function. In the below revised code snippet (which will still not work, due to issues 2 & 3 below), the arrow function approach is used instead of a function declaration, which pulls in the this from the parent context, which will be the Controller's instance.
onChange: (selectedDates, dateStr, instance) => {
const calendarController = this.application.getControllerForElementAndIdentifier(this.calendarTarget, "calendar")
calendarController.gotoDate('18-01-2025') //random date
},
The best way, however, is to read the documentation on Mozilla here https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this in full, read it again and then maybe a third time. After that, find some YouTube videos and watch those. You will find JavaScript development much easier if you truly 'grok' this concept, but it is hard to understand.
2. Understanding Stimulus Targets
The next issue is your use of this.calendarTarget in your flatpackr controller, this controller will not have any target available due to it not being set up correctly.
In the Stimulus docs - https://stimulus.hotwired.dev/reference/targets you can read that the target must be in the controller's scope. But in the HTML below the data-controller="flatpickr" div has no children and also has no targets in the HTML anywhere that can be accessed by this controller.
<div data-controller="flatpickr" name="" data-action="">No Children here?</div>
<div data-controller="calendar">
<div data-calendar-target="window"></div>
<turbo-frame id="popup" data-calendar-target="popup"></turbo-frame>
</div>
There are a few ways to access something outside the controller's scope, but the easiest way would be to bypass this problem all together and use the Stimulus' preferred way to communicate with other controllers.
But, if you want to use a target you need to do two things.
A. Ensure the target static attribute is declared on your controller.
export default class extends Flatpickr {
static targets = [ "calendar" ]; // this is required
B. Ensure the target element has the right attribute and is a child of the desired controller.
<div data-controller="flatpickr" name="" data-action="">
<div data-controller="calendar" data-flatpickr-target="calendar">
<div data-calendar-target="window"></div>
<turbo-frame id="popup" data-calendar-target="popup"></turbo-frame>
</div>
</div>
3. Stimulus Cross-Controller Coordination With Events
Finally, your use of getControllerForElementAndIdentifier is documented as a work around if there is no other way to communicate with another controller.
The preferred way is using events and it is incredibly powerful, flexible and will probably solve 99.9% of your use cases. Have a read of https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/dispatchEvent & https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent if you are not sure what browser events are first.
Then, you can dispatch an event from your flatpackr controller for your calendar controller to pick up on.
The simplest way to go about this is an event that jut gets dispatched on your first controller and 'bubbles' up the DOM and then your calendar controller listens to this globally.
Solution - Example Code
First, start with your HTML, the only real change below is the data-action attribute on your calendar.
This will listen for a global event flatpackr:changed and when it sees that, it will call your calendar controller's method goToDate.
<div data-controller="flatpickr"></div>
<div data-controller="calendar" data-action="flatpackr:changed#window->calendar#goToDate">
<div data-calendar-target="window"></div>
<turbo-frame id="popup" data-calendar-target="popup"></turbo-frame>
</div>
In your flatpackr controller, using the arrow function approach described above, you can call this.dispatch() which will trigger the dispatching of a CustomEvent with the supplied options.
Stimulus will prefix the name supplied with the controller's name.
Note: You could be more specific with where this event gets dispatched to, but if there is only ever going to be one calendar instance, no need to worry for now.
import Flatpickr from 'stimulus-flatpickr'
export default class extends Flatpickr {
connect() {
this.config = {
inline: true,
enableTime: false,
time_24hr: false,
onChange: (selectedDates, dateStr, instance) => {
// note: Stimulus sets `bubbles` to true by default but good to be explicit
const someDate = '18-01-2025'; // random date
// passing data to the other controller can be via the `detail` object in the CustomEvent & Stimulus will automatically add 'flatpackr:' to the start of the event name for you (Thanks Stimulus!)
this.dispatch('changed', { detail: { date: someDate } , bubbles: true } );
},
};
// super.connect(); - not sure that you need this in most cases so commented out
}
}
In the calendar controller, all that is needed is the method to be declared goToDate.
You can read the supplied detail in the event.detail from the param.
import { Controller } from "#hotwired/stimulus";
import { Calendar } from '#fullcalendar/core';
import resourceTimeGridPlugin from '#fullcalendar/resource-timegrid';
import interactionPlugin from '#fullcalendar/interaction';
export default class extends Controller {
static targets = [ "popup", "window" ];
connect() {
// ...
// note: you may not need the window on load listener as `connect` will only be called when there is a DOM ready to attach to.
}
refresh(e) {
// ...
}
goToDate(event) {
// note: you can use destructuring above and change the signature to ` goToDate({ detail: { date } }) {` instead
const date = event.detail.date;
console.log('do something with the date now', date);
}
}
Note: I have not tested locally but should be close enough

Vuejs: Touchmove fired but always return on the first item when touchstart

I have question related to touch event with Vuejs.
I want to join some number with mouse event (for PC) and touch event (for Mobile Devices) like this:
<div
#mousedown="startMove(item)"
#mouseup="endMove(item)"
#mousemove="doMove(item)"
#touchstart="startMove(item)"
#touchend="endMove(item)"
#touchmove="doMove(item)"
v-for="item in 9"
:key="item"
>
{{ item }}
</div>
Since mousedown, mouseup, mousemove working well on PC, touch event also fired on mobile devices but it always return item of touchstart.
Example if I move(with touch) from 2 to 5, item always return 2 on touchmove and touchend event.
Here is my data:
data(){
return {
isMoving: false
}
},
methods: {
startMove(e){
this.isMoving= true;
},
endMove(e) {
this.isMoving= false;
},
doMove(e) {
if (this.isMoving) {
console.log(e)
}
}
}
I tested with Chrome develop mode, and tested on Ipad. Can you tell me why and suggest me the solution for this.
Thank for your help.
Touch events work a little differently, try changing your doMove method.
doMove(e) {
if (this.isMoving) {
const clientX = e.clientX || e.changedTouches[0].clientX;
const clientY = e.clientY || e.changedTouches[0].clientY;
console.log(document.elementFromPoint(
clientX,
clientY
).innerHTML);
}
},
Explanation,
In this method, we try to get the position of the current touch or pointer. based on that we are getting the current element.

Keep props updated with programatically generated components

Imagine an empty virtual bulletin board where an unknown number of virtual notes will be placed. The board is the parent component and the note is the child.
When I click the board a new note appears on the board. When I move the mouse the note should follow the mouse cursor (weird UI I know, but I'm simplifying for the sake of this post).
I'm generating a new note by instancing it and then adding it to the dom like this:
let NoteClass = Vue.extend(Note);
let note = new NoteClass({
propsData: { x: this.clientX, y: this.clientY },
});
note.$mount();
this.$refs.board.appendChild(note.$el);
Notice the mouse x/y is passed to the note via props. This causes the note to appear at the position of the mouse cursor when I click. Great.
However, once the Note is instanced it no longer updates the x/y props. The Note does not continuously read the position of the mouse cursor from its parent.
Here's the full code:
https://codesandbox.io/s/boring-wiles-pru1y?file=/src/App.vue
For comparison, check out this version where the Note is NOT generated in code. A single note is placed the typical way. It follows the cursor just fine:
https://codesandbox.io/s/proud-tree-xnthc?file=/src/App.vue
I found a way better solution thanks to Michal LevĂ˝'s comment -- the data driven way:
<template>
<div ref="board" class="board" #click.self="onClick" #mousemove.prevent="drag">
<Note v-for="(note, key) in notes" :key="key" :x="clientX" :y="clientY"></Note>
</div>
</template>
<script>
import Note from '#/components/Note.vue';
export default {
name: 'Board',
data() {
return {
clientX: 0,
clientY: 0,
notes: []
}
},
components: {
Note
},
methods: {
onClick() {
this.notes.push({});
},
drag(event) {
this.clientX = event.clientX;
this.clientY = event.clientY;
}
}
}
</script>

You may have an infinite update loop in a component render function using click event conditional rendering

I am rendering two texts based on a condition and be able to pass methods to the click event based on the condition. The default text is ADD TO COLLECTION because initially hasPaid property is false. Once payment has been made, I want to set that property to true
The function addToCollection first opens a modal, on the modal, the handlePayment function is implemented. I have been able to conditionally render the div to show either ADD TO COLLECTION or DOWNLOAD using v-on="". I also return hasPaid property from the handlePayment function.
<div class="float-right peexo-faded-text card-inner-text" :face="face" v-on="!hasPaid ? {click: addToCollection} : {click: handleDownload(face)}">
{{!hasPaid ? 'ADD TO COLLECTION': 'DOWNLOAD' }}
</div>
data: function () {
return {
hasPaid: false,
}
},
addToCollection(){
this.showcollectionModal = true;
},
handlePayment(){
this.showcollectionModal = false;
let accept = true;
this.showpaymentsuccessmodal = true;
//this.hasPaid = true;
return {
hasPaid: accept
}
},
I want to be able to set hasPaid property on the handlePayment function for the render function to pick it, so that the handleDownload function can then work.
The last section of this bit is going to be problematic:
v-on="!hasPaid ? {click: addToCollection} : {click: handleDownload(face)}"
When hasPaid is true it will invoke the method handleDownload immediately. That is, it will be called during render, not when the <div> is clicked.
You could fix it by 'wrapping' it in a function:
{click: () => handleDownload(face)}
I've used an arrow function in my example but you could use a normal function if you prefer.
Personally I wouldn't try to do this using the object form of v-on.
My first instinct is that you should consider just having two <div> elements and use v-if to decide which one is showing.
If you did want to use a single <div> I would put the click logic in a method. So:
<div class="..." :face="face" #click="onDivClick(face)">
Note that despite the apparent syntactic similarity to the way you defined your click listener this won't invoke the method immediately.
Then in the methods for the component:
methods: {
onDivClick (face) {
if (this.hasPaid) {
this.handleDownload(face)
} else {
this.addToCollection()
}
}
}