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).
Related
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: () => {}
},
},
I am trying to pass a variable from a Parent (page) component to a Child (modal) component. After reading a few examples, this works fine. The variable in question is brought in from another component as a route param. If i refresh the page, the variable is lost and can no longer be passed to the child. My question is, is the best way to persist this using the store, or is it possible to persist another way if the user refreshed? Any help would be appreciated
Parent
<b-container>
<Modal v-show="displayModal" #close="closeModal">
<template v-slot:body>
<ExpressionCreate v-show="displayModal" :lender-id="lenderId" #close="closeModal"/>
</template>
</Modal>
<div class="card" style="width: 100%;">
<div class="card-header">
<h5>{{this.lenderName}}</h5>
<b-alert :show="this.loading" variant="info">Loading...</b-alert>
</div>
<b-btn block variant="success" #click="showCreateLenderModal">Add Expression</b-btn>
....
created () {
this.lenderId = this.$route.params.lenderId
...
navigate () {
router.go(-1)
},
showCreateLenderModal () {
this.displayModal = true
},
toggleDisplayModal (isShow) {
this.displayModal = isShow
},
async closeModal () {
this.displayModal = false
}
Child
<label>lender id:</label>{{this.lenderId}}
...
props: {
lenderId: {
type: Number
}
},
You can use VueSession to persist.
Simply persist your lender_id with
this.$session.set('lender_id', this.lender_id)
and you can get it later as
saved_lender = this.$session.get('lender_id')
if (saved_lender) {
// handle saved case here
}
You can use query params in URI by doing:
$route.push('/', { query: { param: 3123 } })
This will generate the URL /?param=3123 and this will work after refresh. Or, you can use vuex and vuex-localstorage plugin, that let you persist data in the browser, and you can close the browser and open again and data will be restored.
In order for the state of application to persist, I recommend that you use vuex to manage the state and use a library that abstracts the persisting to vue in a clean way. One popular library is vuex-persist. https://www.npmjs.com/package/vuex-persist
If you dont want to use a store, VueSession, a package linked here should do the trick
I'm working on a project which expects a lot of places where I have to implement upload file component with different styles. I want to create highly customizable component which design I can modify easily and also I don't want to repeat myself and want to extract all the common logic into one place.
At this moment I have a vue.js version 2.2.2 and bulma css framework. I have a basic implementation of this component with only one design available. It supports a few states which represents current upload status:
component is waiting for the input
upload started
upload finished successfully
upload failed
Also this component is responsible for the upload process.
At this my component has a lot of responsibilities:
1. it knows how to deal with statuses:
<p v-if="isWaiting">
...
</p>
<div v-if="isLoading" class="is-loading">
<p class="title">{{currentPercent}} %</p>
</div>
...
data() { return {
currentStatus = Statuses.waiting
}}
computed: {
isWaiting() {
return this.currentStatus === Statuses.waiting;
},
...
}
it knows how to upload the data and count the current percent of data which is already transfered:
selectedFileChanged: async function(event) {
if (event.target.files.length === 0) return;
this.currentStatus = Statuses.uploading;
this.selectedFile = event.target.files[0];
const formData = new FormData();
formData.append("file", this.selectedFile);
try {
const result = (await axios.post("some url", formData, {
onUploadProgress: progress => {
const loaded = progress.loaded;
const total = progress.total;
this.currentPercent = Math.floor((loaded * 100) / total);
}
})).data;
this.currentStatus = Statuses.uploaded;
this.$emit("fileUploaded", {
file: this.selectedFile,
payload: result
});
} catch (exception) {
this.currentStatus = Statuses.error;
}
}
it has only one style which I can use
you can find the full code here: https://codesandbox.io/s/vue-template-gz2gk
So my question is: how to build this component to have an opportunity to change it's style easily and how to deal with upload statuses?
It seems to me that:
1. the component shouldn't know that axios is and how to upload the data;
2. the component should only be responsible for the current status and how to display it
I can introduce a new upload service which will know how to upload the data and add a new prop (current status) for upload file component and change it from the parent component. But in this case I will write the same code for all instances of the component.
Does someone know best practices of how-to create such customizable component?
UPDATE 1
I've tried to implement this functionality using slots and ended up with: https://codesandbox.io/s/vue-template-pi7e9
The component still knows how to upload the data, but now I can change the style of upload component.
So the new question is: how to work with slots and do not transfer a lot of variables and how to deal with uploading. I don't want my component to know how to upload the data :(
I've finished the component in the followig way:
In my parent component I have two different styles for my component:
<div id="app" class="container box">
<upload-file url="url"></upload-file> <-- the default one with statuses outside -->
<upload-file url="url"> <-- custom one which look like a box with statuses in it -->
<template #fileselect="{status, change}">
<custom-file-upload :status="status" #change="change"></custom-file-upload>
</template> <-- I've used custom-file-upload component here and tnjected all the properties from default implementation -->
</upload-file>
</div>
My default file input is nothing but a slot with default implementation:
<template>
<div>
<slot name="fileselect" :change="selectedFileChanged" :status="status">
<input id="fileselect" #change="selectedFileChanged" class="file-input" type="file">
<div class="help is-info" v-if="isWaiting">Waiting</div>
<div class="help is-success" v-if="isUploaded">Uploaded</div>
<div class="help is-info" v-if="isUploading">Uploading {{currentPercent}} %</div>
<div class="help is-error" v-if="isFailed">Failed</div>
</slot>
</div>
</template>
and what is the code looks like:
name: "upload-file",
props: ["url"], // url which will be used in upload request
data() {
return {
currentStatus: 1,
selectedFile: null,
currentPercent: 0
};
},
computed: {
someOtherProperties,
status() {
return {
isWaiting: this.isWaiting, // this.currentStatus === 1
isUploaded: this.isUploaded,
isUploading: this.isUploading,
isFailed: this.isFailed,
currentPercent: this.currentPercent,
selectedFile: this.selectedFile
};
}
},
methods: {
selectedFileChanged: function(event) {
this.selectedFile = event.target.files[0];
const xhr = new XMLHttpRequest();
// some handlers for XHR
xhr.open("POST", this.url, true);
xhr.send(formData);
}
}
Now I can use the file upload component with different styling but it will encapsulate in a base implementation all the status handling and upload logic.
I hope this solution will help someone :)
To view the full version of the code, please follow https://codesandbox.io/s/vue-template-pi7e9
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
I'm using the Vuejs official router, and am currently working on giving different routes different animations, like in the example here: https://router.vuejs.org/en/advanced/transitions.html (see 'Route-Based Dynamic Transition')
My problem is that some transitions need a specific transition durations, and others don't (those I want to specify in the CSS).
I've made a variable that holds the duration to pass to the router, (same as this.transitionName in the router) but I was wondering if there was a way to set this variable to 'auto', for the routes that don't need a duration?
I don't know enough about your setup, so I'll only address modifying the transition duration.
As you can tell, the transitions are using css to manage the transitions.
I've taken the example in the doc you linked (https://github.com/vuejs/vue-router/blob/dev/examples/transitions/app.js) and added the functionality to override the duration.
const Parent = {
data() {
return {
transitionName: "slide-left",
transitionDuration: 2.5
};
},
computed: {
transitionStyle() {
return {
'transition-duration': this.transitionDuration +'s'
}
}
},
beforeRouteUpdate(to, from, next) {
const toDepth = to.path.split("/").length;
const fromDepth = from.path.split("/").length;
///////////////
// this is where you set the transition duration
// replace with your own implementation of setting duration
this.transitionDuration = Math.random()*1.5+0.5;
///////////////
this.transitionName = toDepth < fromDepth ? "slide-right" : "slide-left";
next();
},
template: `
<div class="parent">
<h2>Parent</h2>
{{transitionStyle}}
<transition :name="transitionName">
<router-view class="child-view" :style="transitionStyle"></router-view>
</transition>
</div>
`
};
This will override the duration by in-lining the style
You would still need to find a way to get the intended duration within beforeRouteUpdate, but I'll leave that up to you.
you can have a look at a working pen here: https://codepen.io/scorch/pen/LOPpYd
Note that the effect is only applied to the parent component, so animation on home(/) will not be affected