D3 working properly only in first instance of Vue 3 component - vue.js

I'm working on Vue 3 app where I would like to use multiple instances of one component. Each component should have its instance of D3 for displaying various SVG images. In my case D3 works as intended only on first instance of Vue component.
Dots are random generated by D3. I can see when inspecting elements that none of dots has been appended in second instance of component. Screenshot of the problem may be found here.
My component with D3 looks like this:
<template>
<div class="fill-100">
<svg ref="svgRef" width="400" height=667>
<g></g>
</svg>
</div>
</template>
<script>
import {ref, onMounted} from "#vue/runtime-core";
import {select, zoom} from "d3";
export default {
name: "SldSvgD3",
props: ["id"],
setup() {
const svgRef = ref(null);
onMounted(() =>{
const svg = select(svgRef.value);
svg.append("svg")
let data = [], width = 400, height = 667, numPoints = 100;
let zoom3 = zoom()
.on('zoom', handleZoom);
function handleZoom(e) {
select('svg g')
.attr('transform', e.transform);
}
function initZoom() {
select('svg')
.call(zoom3);
}
function updateData() {
data = [];
for(let i=0; i<numPoints; i++) {
data.push({
id: i,
x: Math.random() * width,
y: Math.random() * height
});
}
}
function update() {
select('svg g')
.selectAll('circle')
.data(data)
.join('circle')
.attr('cx', function(d) { return d.x; })
.attr('cy', function(d) { return d.y; })
.attr('r', 3);
}
initZoom();
updateData();
update();
});
return {svgRef}
}
}
</script>
<style lang="scss">
.fill-100{
width: 100%;
height: 100%;
}
</style>
Implementation of D3 zoom and pan taken from this site

What I didn't know is that scope of d3.select() call is global for the whole app. Solution in my case was just creating unique id for root div and selecting this div before any manipulation.
This question was very helpful to me.
Complete code:
<template>
<div class="fill-100" :id="'sld_div'+this.id">
</div>
</template>
<script>
import {ref, onMounted} from "#vue/runtime-core";
import * as d3 from "d3";
export default {
name: "SldSvgD3",
props: ["id"],
setup(props) {
const svgRef = ref(null);
const svg_width = 400;
const svg_height = 667;
onMounted(() =>{
const svg = d3
.select("#sld_div"+props.id)
svg.append("svg")
.attr("id","sld_root"+props.id)
.attr("width", svg_width)
.attr("height", svg_height)
.append("g")
.attr("id","sld_root_g"+props.id)
let data = [], width = 600, height = 400, numPoints = 100;
let zoom = d3.zoom()
.on('zoom', handleZoom);
function handleZoom(e) {
d3.select("#sld_div"+props.id)
.select('svg g')
.attr('transform', e.transform);
}
function initZoom() {
d3.select("#sld_div"+props.id)
.select('svg')
.call(zoom);
}
function updateData() {
data = [];
for(let i=0; i<numPoints; i++) {
data.push({
id: i,
x: Math.random() * width,
y: Math.random() * height
});
}
}
function update() {
d3.select("#sld_div"+props.id)
.select('svg g')
.selectAll('circle')
.data(data)
.join('circle')
.attr('cx', function(d) { return d.x; })
.attr('cy', function(d) { return d.y; })
.attr('r', 3);
}
initZoom();
updateData();
update();
});
return {svgRef}
}
}
</script>
<style lang="scss">
.fill-100{
width: 100%;
height: 100%;
}
</style>

Related

how to remove an event listener in vue 3 correctly

I have an event listener that gets the viewport dimensions when the project is mounted and after each resize event.
I don't know how to remove the event listener correctly.
const { createApp, onMounted, ref } = Vue;
const app = createApp({
setup() {
const dim = ref({})
onMounted(() => {
dim.value = getDimensions()
// adding the event listener...
window.addEventListener('resize', debounce(() => {
// ...but how to remove the event listener correctly?
console.log('resize')
dim.value = getDimensions()
}, 250))
})
function getDimensions () {
return {
w: window.innerWidth,
h: window.innerHeight
}
}
// generic debounce function
function debounce (func, wait) {
let timeout
return function executedFunction (...args) {
const later = () => {
timeout = null
func(...args)
}
clearTimeout(timeout)
timeout = setTimeout(later, wait)
}
}
return {
dim
}
}
});
app.mount("#app");
.navbar {
position: fixed;
width: 100%;
height: 50px;
top: 0;
left: 0;
background-color: #555;
text-align: center;
}
p {
color: #fff;
}
<script src="https://unpkg.com/vue#next"></script>
<div id="app">
<div class="navbar">
<p>
<span>width: {{ dim.w + ' ' }}</span>
<span>| height: {{ dim.h }}</span>
</p>
</div>
</div>
How would you remove the event listener?
I am using Vue 3 with the composition API.
why not in a unmounted life cycle hook (docs)
window.removeEventListener("resize", debounce);
For Vue 3 Composition API the respective hook is onUnmounted (see the docs)
I usually define my event method outside of mounted
onResize() {
debounce(() => {
// ...but how to remove the event listener correctly?
console.log('resize')
dim.value = getDimensions()
}, 250))
})
}
then inside of mounted you can use
onMounted(() => {
dim.value = getDimensions()
// adding the event listener...
window.addEventListener('resize', this.onResize)
})
and inside of beforeUnmount
beforeUnmount(() => {
window.removeEventListener('resize', this.onResize)
})
In vue 3 script setup it works like this.
<script setup>
import {onMounted, onUnmounted} from "vue";
function yourFunction(event) {
//do something
}
onMounted(() => {
window.addEventListener('click', yourFunction)
})
onUnmounted(() => {
window.removeEventListener('click', yourFunction);
})
</script>
If you figure out how to remove event listeners from anonymous functions, let me know.
You can use VueUse useEventListener which automatically removes the listener on unmount.
Example from the docs:
import { useEventListener } from '#vueuse/core'
const element = ref<HTMLDivElement>()
useEventListener(element, 'keydown', (e) => {
console.log(e.key)
})

GeoLocation can't pass coordinates to another variable

So i would like to pass latitude and longitude to width and height variables in "data()", instead of just writing it in html.
I tried to pass variable through all these functions to have access to variable but this doesn't worked in function showLocation(location).
<template>
<div class="app">
<div class="app-Inner">
</div>
<button #click="getLocation">VIEW YOUR LOCATION</button>
<div>{{ width }}</div>
<div class="location"></div>
<div class="map"></div>
</div>
</template>
<script>
import Home from './components/Home.vue';
import MainValues from './components/MainValues.vue';
import Form from './components/Form.vue';
import More from './components/More.vue';
export default {
name: 'App',
data() {
return {
width: 0,
height: 0,
};
},
methods: {
getLocation() {
const geo = navigator.geolocation;
if (geo) {
// eslint-disable-next-line
geo.getCurrentPosition(showLocation);
} else {
const divLocation2 = document.querySelector('.location');
divLocation2.innerHTML = 'error';
}
function showLocation(location) {
// edit: !!!
// here instead of sending location cords through innerHTML i
// want to assign them into variables "width" and "height" which
// are defined in "data()" function above.
// tried: this.width = location.coords.latitude, and
// also : this.length = location.coords.longitude
// !!!
const divLocation = document.querySelector('.location');
divLocation.innerHTML = `${location.coords.latitude} ${location.coords.longitude}`;
}
},
},
};
</script>

vue.js nuxt.js Creating dynamical pages and pagination

I have several scenes that I want to include in my nuxt project.
The project should have pages, and each page has 4 scenes.
I'm currently 'coding' each page and import the scenes manually.
This is my pages tree:
pages tree
This is my first page
<template>
<main class="main">
<Scene1 />
<Scene2 />
<Scene3 />
<!-- <Scene4 /> -->
</main>
</template>
<script>
import Scene1 from './1/Scene1.vue'
import Scene2 from './1/Scene2.vue'
import Scene3 from './1/Scene3.vue'
// import Scene4 from './1/Scene4.vue'
export default {
components: {
Scene1,
Scene2,
Scene3,
// Scene4
}
}
</script>
<style scoped>
.main{
display: grid;
grid-template-columns: 50% 50%;
}
</style>
And this is how a scene looks like:
<template>
<div class="three-item">
<div class="item-title">
Color Rectangle
</div>
<p>Vertex</p>
<pre><code id="vertexShader">
void main() {
gl_Position = vec4( position, 1.0 );
}
</code></pre>
<p>Fragment</p>
<pre><code id="fragmentShader">
#ifdef GL_ES
precision mediump float;
#endif
uniform float u_time;
void main() {
// Magenta (1,0,1)
gl_FragColor = vec4(1.0,0.0,1.0,1.0);
}
</code></pre>
<div id="scene1" class="scene">
</div>
</div>
</template>
<script>
import * as Three from 'three'
import hljs from 'highlight.js'
import glsl from 'highlight.js/lib/languages/glsl';
export default{
name: 'Scene1',
data() {
return {
camera: null,
scene: null,
renderer: null,
mesh: null
}
},
methods: {
init: function() {
this.container = document.getElementById('scene1');
this.camera = new Three.PerspectiveCamera(70, this.container.clientWidth/this.container.clientHeight, 0.01, 10);
this.camera.position.z = 1;
this.scene = new Three.Scene();
this.uniforms = {
u_time: { type: "f", value: 1.0 },
u_resolution: { type: "v2", value: new Three.Vector2() },
u_mouse: { type: "v2", value: new Three.Vector2() }
};
let material = new Three.ShaderMaterial( {
uniforms: this.uniforms,
vertexShader: document.getElementById( 'vertexShader' ).textContent,
fragmentShader: document.getElementById( 'fragmentShader' ).textContent
} );
let geometry = new Three.BoxBufferGeometry(0.2, 0.2, 0.2);
// let material = new Three.MeshNormalMaterial();
this.mesh = new Three.Mesh(geometry, material);
this.scene.add(this.mesh);
this.renderer = new Three.WebGLRenderer({alpha:true});
this.renderer.setSize(this.container.clientWidth, this.container.clientHeight);
this.container.appendChild(this.renderer.domElement);
window.addEventListener( 'resize', this.onWindowResize, false );
hljs.registerLanguage('glsl', glsl);
document.querySelectorAll('pre code').forEach((block) => {
hljs.highlightBlock(block);
});
},
animate: function() {
requestAnimationFrame(this.animate);
this.mesh.rotation.x += 0.01;
this.mesh.rotation.y += 0.02;
this.uniforms.u_time.value += 0.05;
this.renderer.render(this.scene, this.camera);
},
onWindowResize: function() {
this.camera.aspect = this.container.clientWidth / this.container.clientHeight;
this.camera.updateProjectionMatrix();
this.renderer.setSize(this.container.clientWidth, this.container.clientHeight);
}
},
mounted() {
this.init();
this.animate();
this.onWindowResize();
}
}
</script>
To be honest, I don't get behind the dynamical created pages with nuxt. Is there a way to have a template that fetches 4 scenes, puts them into a page and does it all over again? It could be that I want to add scenes in the middle of the pages. That would destroy my current setup.
My goal is to have something like this:
dreams tree
the page.vue is crawling through the scenes, picks 4, creates a page called '1', and does it all over again. Can someone lend me a hand with that? 😊In the examples at the nuxt homepage they use .json files. I already set up all three.js scenes, but these have javascript files in it.
Erik

Pattern to pass computed data as prop to child component (Promise problem)

I am wondering how to pass a computed variable to a child-component (in a slot). The main component revieves an array of objects by using a post request. What actually happpens is that the variable "Branches" seems to be filled with an empty promise which represents the result-data. So i get a warning because the list-component expects "Branches" to be an array. I tried to delay the rendering of the slot content by using "v-if="Array.isArray(Branches)" or a flag which is set in the computed-method ("syncedBranches") but none of these seems to do it.
How to delay the rendering of that list till "Branches" is a filled array of objects? Shouldnt i use a computed var and pass the data by a getter?
Main Component
<branches-widget-tabs :items="register" :activeItem="activeRegister">
<template #tabbody_0="Branches" >
<h1>Content Register 1</h1>
<branches-widget-list :items="Branches" v-if="syncedBranches"></branches-widget-list>
</template>
<template #tabbody_1="Branches" v-if="Array.isArray(Branches)">
<h1>Content Register 2</h1>
<branches-widget-list :items="Branches" v-if="syncedBranches"></branches-widget-list>
</template>
</branches-widget-tabs>
</div>
</template>
<style>
#branchesWidget {
min-width: 150px;
min-height: 150px;
background-color: #333;
}
#branchesWidget:hover {
background-color: #666;
}
</style>
<script>
import chroma from 'chroma-js';
//console.log('chroma',chroma);
import HUSL from 'hsluv';
//console.log('HUSL',HUSL);
import BranchesWidgetTabs from './BranchesWidgetTabs';
import BranchesWidgetList from './BranchesWidgetList';
const random = function(min, max){
return Math.floor(Math.random() * (max - min + 1)) + min;
};
var generateColors = function(n, startHex = '#ff6000', padding = 0, step = 5, randomSat = true, randomLight = true){
let colors = [];
const baseHex = HUSL.hexToHsluv(startHex);
const baseHue = baseHex[0];
//console.log('baseHue',baseHue);
var degrees = [];
for (let i = 0; i < n; i++) {
degrees.push( 360 / n * i);
}
//console.log('degrees',degrees);
const hues = degrees.map((offset) => {
return (baseHue + offset) % 360;
});
//console.log('hues',hues);
if(randomSat){
var baseSaturation = random(55, 85);
}else{
var baseSaturation = baseHex[1];
}
if(randomLight){
var baseLightness = random(35, 75);
}else{
var baseLightness = baseHex[2];
}
var subs = Math.min(n,Math.max(step,2));
for(let i = 0; i < subs; i++) {
colors.push( HUSL.hsluvToHex([
hues[i],
baseSaturation,
baseLightness
]));
}
console.log('colors',colors);
return chroma.scale(colors).padding(0).mode('lab').colors(n);
};
export default {
name: 'BranchesWidget',
props : [],
data() {
return {
activeRegister : null,
register : [
{
'title' : 'tab1',
}
,
{
'title' : 'tab2',
}
],
rawBranches : null,
syncedBranches : false
}
},
computed: {
Branches : function(){
if(this.rawBranches !== null){
let colorArr = generateColors(this.rawBranches.length);
console.log('colorArr',colorArr);
// Der Liste der Branchen die Farben zuordnen und als "Branches" bereitstellen
var l = [];
for(var i=0;i<this.rawBranches.length;i++){
var c = JSON.parse(JSON.stringify(this.rawBranches[i]));
c.color = colorArr[i];
l.push(c);
}
console.log('compute Branches',l);
this.syncedBranches = true;
return l;
}
console.log('compute Branches',null);
return null;
}
},
components: {
BranchesWidgetTabs,
BranchesWidgetList
},
mounted () {
axios
.post('/assets/get',{ entity : 'industryBranches' })
.then(response => ( this.rawBranches = response.data.data ))
},
created(){
//console.log('created',this.rawData);
},
methods : {
// das die Componenten eine ref mit der Bezeichnung "bwidget" hat, ist die Methode in der Seite mit app.$refs.bwidget.getBranches() erreichbar.
getBranches : function(){
return this.Branches;
}
}
}
</script>
Tabs-Compoent
<template>
<div class="BranchesWidgetTabs">
<div class="menu">
<div class="item" v-for="(item, index) in list">
<div>
<div class="i">
<div v-if="item.active">active</div>
</div>
<div class="title">
{{ item.title }}
</div>
</div>
</div>
<div class="spacer"></div>
</div>
<div class="tabbody" v-for="(item, index) in list">
<div class="content" v-if="item.active">
<slot :name="`tabbody_${index}`"></slot>
</div>
</div>
</div>
</template>
<style>
div.BranchesWidgetTabs {
background-color: yellow;
min-height: 40px;
}
div.BranchesWidgetTabs > div.menu {
display: flex;
flex-direction: row;
}
div.BranchesWidgetTabs > div.menu > .item {
flex: 0 0 auto;
min-width: 10px;
background-color: blue;
color: white;
}
div.BranchesWidgetTabs > div.menu > .item > div {
display: flex;
flex-direction: column;
padding: 0px 20px;
}
div.BranchesWidgetTabs > div.menu > .item:nth-child(odd) > div {
padding-right: 0;
}
div.BranchesWidgetTabs > div.menu > .item > div > div {
flex: 1;
}
div.BranchesWidgetTabs > div.menu > .item > div > div.i {
background-color: darkgrey;
min-height: 10px;
}
div.BranchesWidgetTabs > div.menu > .item > div > div.title {
background-color: pink;
padding: 10px 20px;
}
div.BranchesWidgetTabs > div.menu > .spacer {
flex: 1;
}
</style>
<script>
export default {
name: 'BranchesWidgetTabs',
props : {
items : Array,
activeItem : {
required : true,
validator: function(i){
return typeof i === 'number' || i === null;
}
},
},
data(){
return {
}
},
computed: {
list: function(){
var l = [];
var s = (this.activeItem !== null)? this.activeItem : 0;
for(var i=0;i<this.items.length;i++){
var c = JSON.parse(JSON.stringify(this.items[i]));
if(s === i){
c.active = true;
}else{
c.active = false;
}
l.push(c);
}
return l;
}
},
created(){
console.log('created',this.activeItem);
}
}
</script>
List-Component which revieves items from main component
<template>
<div class="BranchesWidgetList">
Liste
</div>
</template>
<style>
div.BranchesWidgetList {
}
</style>
<script>
export default {
name: 'BranchesWidgetList',
props : {
items : Array
},
data(){
return {
}
},
computed: {
},
created(){
console.log('created BranchesWidgetList',this.items);
}
}
</script>
EDIT:
I got it! Somehow i got misslead by the v-slot-directive. I thought i would have to pass the Branches-Array down to the child-component. But it seems that the context of template and main component is a shared one. So only thing to make sure of is that the async-call for that array is completed by using "Branches.length" in a v-if - no need for an extra variable like "syncedBranches".
Full main component with working code.
<template>
<div id="branchesWidget">
<branches-widget-tabs :items="register" :activeItem="activeRegister">
<template #tabbody_0 v-if="Branches.length">
<h1>Content Register 1</h1>
<branches-widget-list :items="Branches"></branches-widget-list>
</template>
<template #tabbody_1 v-if="Branches.length">
<h1>Content Register 2</h1>
<branches-widget-list :items="Branches"></branches-widget-list>
</template>
</branches-widget-tabs>
</div>
</template>
<style>
#branchesWidget {
min-width: 150px;
min-height: 150px;
background-color: #333;
}
#branchesWidget:hover {
background-color: #666;
}
</style>
<script>
import chroma from 'chroma-js';
//console.log('chroma',chroma);
import HUSL from 'hsluv';
//console.log('HUSL',HUSL);
import BranchesWidgetTabs from './BranchesWidgetTabs';
import BranchesWidgetList from './BranchesWidgetList';
const random = function(min, max){
return Math.floor(Math.random() * (max - min + 1)) + min;
};
var generateColors = function(n, startHex = '#ff6000', padding = 0, step = 5, randomSat = true, randomLight = true){
let colors = [];
const baseHex = HUSL.hexToHsluv(startHex);
const baseHue = baseHex[0];
//console.log('baseHue',baseHue);
var degrees = [];
for (let i = 0; i < n; i++) {
degrees.push( 360 / n * i);
}
//console.log('degrees',degrees);
const hues = degrees.map((offset) => {
return (baseHue + offset) % 360;
});
//console.log('hues',hues);
if(randomSat){
var baseSaturation = random(55, 85);
}else{
var baseSaturation = baseHex[1];
}
if(randomLight){
var baseLightness = random(35, 75);
}else{
var baseLightness = baseHex[2];
}
var subs = Math.min(n,Math.max(step,2));
for(let i = 0; i < subs; i++) {
colors.push( HUSL.hsluvToHex([
hues[i],
baseSaturation,
baseLightness
]));
}
console.log('colors',colors);
return chroma.scale(colors).padding(0).mode('lab').colors(n);
};
export default {
name: 'BranchesWidget',
props : [],
data() {
return {
activeRegister : null,
register : [
{
'title' : 'tab1',
}
,
{
'title' : 'tab2',
}
],
rawBranches : null
}
},
computed: {
Branches : function(){
var l = [];
if(this.rawBranches !== null){
let colorArr = generateColors(this.rawBranches.length);
//console.log('colorArr',colorArr);
// Der Liste der Branchen die Farben zuordnen und als "Branches" bereitstellen
for(var i=0;i<this.rawBranches.length;i++){
var c = JSON.parse(JSON.stringify(this.rawBranches[i]));
c.color = colorArr[i];
l.push(c);
}
}
console.log('compute Branches',l);
return l;
}
},
components: {
BranchesWidgetTabs,
BranchesWidgetList
},
mounted () {
axios
.post('/assets/get',{ entity : 'industryBranches' })
.then(response => ( this.rawBranches = response.data.data ))
},
created(){
//console.log('created',this.rawData);
},
methods : {
getBranches : function(){
return this.Branches;
}
}
}
</script>
I got it! Somehow i got misslead by the v-slot-directive. I thought i would have to pass the Branches-Array down to the child-component. But it seems that the context of template and main component is a shared one. So only thing to make sure of is that the async-call for that array is completed by using "Branches.length" in a v-if - no need for an extra variable like "syncedBranches".
To pass the variable "Branches" as a prop there is no need for passing it in as a scoped variable. That is only needed if you want to access those data between the template tags in the main component file.
Full main component with working code.
<template>
<div id="branchesWidget">
<branches-widget-tabs :items="register" :activeItem="activeRegister">
<template #tabbody_0 v-if="Branches.length">
<h1>Content Register 1</h1>
<branches-widget-list :items="Branches"></branches-widget-list>
</template>
<template #tabbody_1 v-if="Branches.length">
<h1>Content Register 2</h1>
<branches-widget-list :items="Branches"></branches-widget-list>
</template>
</branches-widget-tabs>
</div>
</template>
<style>
#branchesWidget {
min-width: 150px;
min-height: 150px;
background-color: #333;
}
#branchesWidget:hover {
background-color: #666;
}
</style>
<script>
import chroma from 'chroma-js';
//console.log('chroma',chroma);
import HUSL from 'hsluv';
//console.log('HUSL',HUSL);
import BranchesWidgetTabs from './BranchesWidgetTabs';
import BranchesWidgetList from './BranchesWidgetList';
const random = function(min, max){
return Math.floor(Math.random() * (max - min + 1)) + min;
};
var generateColors = function(n, startHex = '#ff6000', padding = 0, step = 5, randomSat = true, randomLight = true){
let colors = [];
const baseHex = HUSL.hexToHsluv(startHex);
const baseHue = baseHex[0];
//console.log('baseHue',baseHue);
var degrees = [];
for (let i = 0; i < n; i++) {
degrees.push( 360 / n * i);
}
//console.log('degrees',degrees);
const hues = degrees.map((offset) => {
return (baseHue + offset) % 360;
});
//console.log('hues',hues);
if(randomSat){
var baseSaturation = random(55, 85);
}else{
var baseSaturation = baseHex[1];
}
if(randomLight){
var baseLightness = random(35, 75);
}else{
var baseLightness = baseHex[2];
}
var subs = Math.min(n,Math.max(step,2));
for(let i = 0; i < subs; i++) {
colors.push( HUSL.hsluvToHex([
hues[i],
baseSaturation,
baseLightness
]));
}
console.log('colors',colors);
return chroma.scale(colors).padding(0).mode('lab').colors(n);
};
export default {
name: 'BranchesWidget',
props : [],
data() {
return {
activeRegister : null,
register : [
{
'title' : 'tab1',
}
,
{
'title' : 'tab2',
}
],
rawBranches : null
}
},
computed: {
Branches : function(){
var l = [];
if(this.rawBranches !== null){
let colorArr = generateColors(this.rawBranches.length);
//console.log('colorArr',colorArr);
// Der Liste der Branchen die Farben zuordnen und als "Branches" bereitstellen
for(var i=0;i<this.rawBranches.length;i++){
var c = JSON.parse(JSON.stringify(this.rawBranches[i]));
c.color = colorArr[i];
l.push(c);
}
}
console.log('compute Branches',l);
return l;
}
},
components: {
BranchesWidgetTabs,
BranchesWidgetList
},
mounted () {
axios
.post('/assets/get',{ entity : 'industryBranches' })
.then(response => ( this.rawBranches = response.data.data ))
},
created(){
//console.log('created',this.rawData);
},
methods : {
getBranches : function(){
return this.Branches;
}
}
}
</script>

How to get the value with konva at vue.js

I can move and transfer the rectangles with the code below.
I used konva library at vue.js
This works well.
But I want to get the x,y position to save into local-storage after moving it
Could you teach how to get that?
And I am sorry for the long code
You can attach this code at '.vue' which works well without problem.
It moves and transfer well, but I can 't see the value of position moving it
<template>
<div>
<v-stage ref="stage" :config="stageSize" #mousedown="handleStageMouseDown">
<v-layer ref="layer">
<v-rect v-for="item in rectangles" :key="item.id" :config="item"/>
<v-transformer ref="transformer"/>
</v-layer>
</v-stage>
<div>
<p>{{ rectangles[0].x }}</p>
<button #click="addCounter">+</button>
<button #click="subCounter">-</button>
<button #click="position">SAVE</button>
</div>
</div>
</template>
<script>
const width = window.innerWidth;
const height = window.innerHeight;
export default {
data() {
return {
stageSize: {
width: width,
height: height
},
rectangles: [
{
x: 150,
y: 100,
width: 100,
height: 100,
fill: "red",
name: "rect1",
draggable: true
},
{
x: 150,
y: 150,
width: 100,
height: 100,
fill: "green",
name: "rect2",
draggable: true
}
],
selectedShapeName: ""
};
},
methods: {
position() {
localStorage.setItem(this.rectangles[0].x, true);
},
addCounter() {
this.rectangles[0].x++;
},
subCounter() {
this.rectangles[0].x--;
},
handleStageMouseDown(e) {
// clicked on stage - cler selection
if (e.target === e.target.getStage()) {
this.selectedShapeName = "";
this.updateTransformer();
return;
}
// clicked on transformer - do nothing
const clickedOnTransformer =
e.target.getParent().className === "Transformer";
if (clickedOnTransformer) {
return;
}
// find clicked rect by its name
const name = e.target.name();
const rect = this.rectangles.find(r => r.name === name);
if (rect) {
this.selectedShapeName = name;
} else {
this.selectedShapeName = "";
}
this.updateTransformer();
},
updateTransformer() {
// here we need to manually attach or detach Transformer node
const transformerNode = this.$refs.transformer.getStage();
const stage = transformerNode.getStage();
const { selectedShapeName } = this;
const selectedNode = stage.findOne("." + selectedShapeName);
// do nothing if selected node is already attached
if (selectedNode === transformerNode.node()) {
return;
}
if (selectedNode) {
// attach to another node
transformerNode.attachTo(selectedNode);
} else {
// remove transformer
transformerNode.detach();
}
transformerNode.getLayer().batchDraw();
}
}
};
</script>
You can use dragmove and transform events.
<v-rect
v-for="item in rectangles"
:key="item.id"
:config="item"
#dragmove="handleRectChange"
#transform="handleRectChange"
/>
handleRectChange(e) {
console.log(e.target.x(), e.target.y()); // will log current position
},
Demo: https://codesandbox.io/s/lp53194w59