vue-konva: removing specific node from stage working incorrectly - vue.js

I'm passing down an array of image urls as props to my Konva component, creating a image object for each url, storing that object in a data object (using Vue's $set method to keep the object literal reactive), then using v-for to create a v-image for each image object in my data object. This seems to be working fine, however I'm running into a problem where if I try to remove one of the images, 2 images will be removed. This only happens if the image that I try to remove is not the topmost image. In the console, I'm getting the warning Konva warning: Node has no parent. zIndex parameter is ignored.. My hunch is that this is a result of konva's destroy method clashing with vue's $delete method on a data object used in a v-for. I've been battling with this for hours, and would appreciate any help I can get. Relevant code is below. Thanks!
Parent
<template>
<editor ref="editor" :image-props.sync="images"/>
<button #click="remove">remove</button>
</template>
export default {
components: {
Editor,
},
data() {
return {
images: [url1, url2, etc...],
};
},
methods: {
remove() {
this.$refs.editor.removeSelectedImage();
},
}
child
<template>
<div>
<v-stage>
<v-layer>
<v-group>
<v-image v-for="image in Object.values(images)"
:key="image.id" :config="image"/>
</v-group>
</v-layer>
</v-stage>
</div>
</template>
export default {
props: {
imageProps: Array,
},
data() {
return {
images: {},
selectedNode: null //this gets updated on click
},
watch: {
imageProps() {
this.registerImages();
},
mounted() {
this.registerImages();
},
methods: {
registerImages() {
this.imageProps.forEach(url => {
if (!this.images[url]) {
let img = new Image();
img.src = url;
img.onload = () => {
this.$set(this.images, url, {
image: img,
draggable: true,
name: url,
x: 0,
y: 0,
});
}
}
});
},
removeSelectedLayer() {
let newImageProps = this.imageProps.filter(url => url !== this.selectedImageName);
this.$emit('update:image-props', newImageProps);
this.selectedNode.destroy();
this.$delete(this.images, this.selectedImageName);
this.stageArea.draw();
},
If I inspect the component in Vue devtools, the images object looks correct as well as imageProps, (even the Vue DOM tree looks right with the correct amount of v-images) however the canvas shows 1 less image than it should. Again, this only happens if I remove a image that wasn't initially on top. It seems to function fine if I remove the top-most image.

When you are developing an app with vue-konva it is better not to touch Konva nodes manually (there only rare cases when you need it, like updating Konva.Transformer).
You don't need to call node.destroy() manually. Just an item for your data.
From your demo, I see you are not using key attribute (you are using image.id for that, but it is undefined). It is very important to use keys in such case.
Updated demo: https://codesandbox.io/s/30qpxpx38q

Related

Vue3 model updates initially after saving code, but not after a browser refresh

I have a bit of a quantum problem happening. I have some code (below) that is not displaying the value of the data model. So I put a console.log in to see what's going on and then it works... none of the other code changes, but by looking at it, it exists. Then if I refresh the page, it's gone again
[Update] It's not the console.log itself (whew - that would be wierd). It's the state when I change the file vs when i do a fresh browser load. What's different there that I should be looking at? I'm new to VueJS, and I'm really more of a python / backend dev anyway, so sorry if this is basic stuff.
The template code in child component
<v-text-field v-else
#change="updateValue(field.name, $event)"
persistent-placeholder
:model-value="(item as any)[field.name]"
:label="field.title || field.name"
></v-text-field>
item is passed in as a Model and converted as follows
props: {
input: {
type: Object || null,
default: null
},
},
data() {
return {
item: {...this.input},
}
},
which comes from the parent
<ModelComponentVue
:input="item"
/>
which gets the data in created()
DataStore.query(User).then((items:Array<User>) => {
if (items.length > 0) {
this.item = items[0] // I suspect this could be not responding to reactivity in Vue3 ???
}
})
Parts of your code only use initialData contrary to what you might think. Pay attention to the code below.
data() {
return {
item: {...this.input},
}
},
In the above code, you will only have the initial value of the input and you will lose the other values.
The easiest way to use props is to use them directly.
However, for more complex concepts, you can use computed or toRef, etc.
Try the code below
<v-text-field v-else
#change="updateValue(field.name, $event)"
persistent-placeholder
:model-value="input[field.name]"
:label="field.title || field.name"
></v-text-field>
props: {
input: {
type: Object ,
default: () => {}
},
},

VueMapbox trying to create multiple markers

I am trying to create multiple markers in Vue using VueMapbox. Currently the map displays correctly but there is only one marker. I think there is something wrong either with my v-for statement or perhaps in the forEach statement. I am trying to place a marker on each location but only the first location is added.
Here is the code for my vue component:
<template>
<MglMap
:accessToken="accessToken"
:mapStyle.sync="mapStyle"
>
<MglMarker v-for="coordinate in coordinates" :key="coordinate" :coordinates="coordinates">
<MglPopup>
<VCard>
<div>{{ country }}</div>
<div>{{ cases }}</div>
</VCard>
</MglPopup>
</MglMarker>
</MglMap>
</template>
<script>
import Mapbox from "mapbox-gl";
import { MglMap, MglPopup, MglMarker } from "vue-mapbox"
export default {
name: 'Map',
components: {
MglMap,
MglPopup,
MglMarker,
},
data() {
return {
accessToken: 'pk.accesstoken...blahblahblah',
mapStyle: 'mapbox://styles/mapbox/dark-v10',
coordinates: [],
country: '',
cases: 0,
}
},
created() {
this.mapbox = Mapbox;
this.getData();
},
methods: {
getData: function () {
fetch('https://coronavirus-tracker-api.herokuapp.com/v2/locations')
.then(response => response.json())
.then(data => {
const locations = data.locations;
locations.forEach(stat => {
const country = stat.country;
this.country = country;
const cases = stat.latest.confirmed;
this.cases = cases;
const coordinates = [stat.coordinates.longitude, stat.coordinates.latitude]
this.coordinates = coordinates;
})
})
}
}
}
</script>
You're currently doing a v-for on coordinates. It should be on locations.
If locations don't have all the required props a MglMarker needs, transform them in the forEach but that's all you should do in that forEach (if you need it at all). Don't use it to populate this.country, this.cases or this.coordinates. You only want to set those when a marker is clicked (if, and only if, you have any functionality listening to changes on those Vue instance properties).
There might be more details which need to be fixed but, without a minimal reproducible example it's very difficult to spot them. Note: you'll need to create a mapbox public token with readonly permissions for your example to work.
To summarize: Move the functionality from your forEach into a function called showMarker or activateMarker. Call that function whenever a marker is clicked or, if that's what you want, call it on one of the locations to make it the currently active one.
What your code does now is: it makes all markers the currently active one, therefore only the last one iterated will be currently active.
Here's what your MglMarker iterator might look like:
<MglMarker v-for="(l, key) in locations"
:key="key"
:coordinates="l.coordinates"
#click="activateMarker(l)"
>
<MglPopup>
VCard>
<div>{{ l.country }}</div>
<div>{{ l.latest.confirmed }}</div>
</VCard>
</MglPopup>
</MglMarker>
In activateMarker method you can dispatch any action to let the rest of the app know about your selection. Or you can close any other open popups, for example (although that can be handled easier with :closeOnClick="true" on each MglPopup).

How does this `layout` prop get updated without `:layout.sync`?

I don't understand how vue-grid-layout manages to change myLayout in the following code sandbox:
<grid-layout
:layout="myLayout"
....
>
I thought that:
<MyComponent :layout="myLayout"/>
Meant that any changes to myLayout here in the parent scope would become changes in in the layout prop in MyComponent, but MyComponent would not be able to change myLayout.
Which is what the sync modifier was for, so that:
<MyComponent :layout.sync="myLayout"/>
Would be the equivalent of:
v-bind:layout="myLayout"
v-on:update:layout="myLayout = $event"
And so without the .sync, it would not be possible for a component to change a prop in the parent scope.
But the code sandbox demonstrates that vue-grid-layout manages to change myLayout using just :layout="myLayout".
(:layout.sync="layout" is used in the README code for vue-grid-layout and then I understand how it works, but it works without the .synctoo, which I don't understand.)
What am I missing or misunderstanding?
In javascript objects are passed around by reference, if you must prop an object, clone it and manipulate the clone to avoid it updating the original object.
for example:
props: {
myLayout: Object
},
data() {
return {
layout: {}
}
},
watch: {
myLayout: {
handler(myLayout) {
this.layout = { ...myLayout };
},
immediate: true
}
}

Vue - How to render $slots.default in a div to calculate its height?

I'm trying to simplify my problem:
Let us say I have a modal component
//example.php
<modal>
<div>A big div</div>
</modal>
Before the modal is shown I need to calculate the height for the proper animation. Inside the modal Vue it looks like this:
//Modal.vue
...
<transition
:name="transition"
#before-enter="beforeTransitionEnter"
#after-leave="afterTransitionLeave"
>
<div
v-if="visibility.modal"
ref="modal"
class="v--modal v--modal-box"
:style="modalStyle"
>
<slot/>
</div>
</transition>
I know that with this.$slots.default I get the node of the slot.
But I'm not sure how I can create a div, add the node to the div so that I can then calculate the height of it?
Edit:
Is it possible to call your own render function so I can use it like in the docs?
render: function (createElement) {
// `<div><slot></slot></div>`
return createElement('div', this.$slots.default)
}
Like
guessSlotsHeight(){
let modalDiv = document.createElement('div')
modalDiv.className = 'v--modal v--modal-box'
//something like
let slotDiv = this.render('div', this.$slots.default)
modalDiv.appendChild(slotDiv);
},
Your logic is too complex. It's easiest than you think.
Look, animation is the thing which happens to DOM. Therefore, DOM is available when animation starts. So, you can access any DOM element using Vue ref (recommended) or native javascript syntax in mounted hook. And there you can take the element height or any other property you want and store in in your data (for example).
Possible solution example #1
Vue transition before-enter event function has the target element as an argument by default. So, you can get the modal height in your beforeTransitionEnter function:
Modal.vue
data() {
return {
modalHeight: 0,
visibility: {
modal: false
}
}
},
methods: {
beforeTransitionEnter(element) {
this.modalHeight = element.offsetHeight;
}
}
Possible solution example #2
For this example there is one important detail. I see in your code example that you display the next div conditionally:
`<div v-if="visibility.modal" class="v--modal v--modal-box">`
Be careful with that, as I don't know what is the init value of visibility.modal.
I would use watch feature to know content height every time once visibility.modal gets true in this particular case.
Look at the example. It's based on the code which you provided and it's a possible solution in your case.
Modal.vue
data() {
return {
isMounted: false,
modalHeight: 0,
visibility: {
modal: false
}
}
},
mounted() {
this.isMounted = true;
},
watch: {
visibility: {
handler(value) {
if (value.modal && this.isMounted) {
this.modalHeight = this.$refs.modal.clientHeight;
}
},
deep: true
}
}
As you can see - you will always have actual modalHeight value and you can use it whenever you want within the Modal.vue component.

VueJs - Passing variables between methods

There is a v-select component and on change I am firing up fillData(selected) where selected is the v-model. And I need to update the label in datacollection.datasets.label on change. How do I do that ?
<script>
import BarChart from './BarChart.js'
import { mapGetters, mapActions } from "vuex";
export default {
name : "TestLegPerformance",
components: {
BarChart
},
data: () => ({
datacollection : {
labels: ['Week-1','Week-2','Week-3'],
datasets: [
{
label: '',
backgroundColor: '#C58917',
data: [40, 50, 20]
}
]
},
selected: []
}),
computed: {
...mapGetters({
planNames: "planNames"
})
},
mounted () {
this.getAllPlanNamesAction();
},
methods: {
...mapActions(["getAllPlanNamesAction"]),
fillData(selected){
console.log(selected)
},
}
}
</script>
Inside methods, you can reference to data properties using this.
In your case, you can use this.datacollection.datasets.label and assign to it:
methods: {
// ...
fillData(selected){
this.datacollection.datasets[0].label = selected;
},
}
Of course, this assuming that selected is the string you want to assign to the label.
Note: the this will only work when you declare the methods using methodName() {} (as you are) or methodName: function (){.... So don't use arrow functions when declaring vue methods, they will mess up your this.
Bind to events using # (v-on) not : v-bind)
Your template:
<v-select label="Select a Plan" :items="planNames" v-model="selected" single-line max-height="auto" :change="fillData(selected)" required >
To listen to the change event, don't use:
:change="fillData(selected)"
use
#change="fillData"
Don't send an argument (it will mess things up). v-select will send you one already.
Notice the replacement of : with #.
The first, : is an alias to v-bind. So :change="xyz" is the same as v-bind:change="xyz".
The second, # is an alias to v-on. So #change="xyz" is the same as v-on:change="xyz". which is what you want.
See demo JSFiddle here.
Updating label of vue-chartjs's BarChart automatically
Even though you are
using the reactiveProp mixin; and
changing the label
The chart is not reflecting the changes (the label does not change) automatically.
I noticed this happens because the chart only reacts to whole datacollection changes, not to inner properties (like label).
So the solution is to:
"clone" datacollection
update the label of the clone
assign the clone to this.datacollection
And the chart will react (the label change will be reflected).
So, change your fillData method to the following:
fillData(selected){
let collectionClone = Object.assign({}, this.datacollection);
collectionClone.datasets[0].label = selected;
this.datacollection = collectionClone;
},
Check here a working DEMO CODESANDBOX of this solution (see the changeLabelAndReassign() method of BarChart.vue).