OpenLayers with VueJS - empty div in place of map - vue.js

I'm trying to implement a simple world map in a Vue application. I have created a MapContainer component that is then imported into my main app. Below is the code for MapContainer.vue:
<template>
<div
ref="map-root"
style="width: 100%, height: 100%">
</div>
</template>
<script>
import View from 'ol/View';
import Map from 'ol/Map';
import TileLayer from 'ol/layer/Tile';
import OSM from 'ol/source/OSM';
import 'ol/ol.css';
export default {
name: 'MapContainer',
components: {},
props: {},
mounted() {
new Map({
target: this.$refs['map-root'],
layers: [
new TileLayer({
source: new OSM()
})
],
view: new View({
zoom: 0,
center: [0, 0],
constrainResolution: true
})
});
}
}
</script>
<style>
</style>
I am registering the MapContainer component and then simply placing it inside a div in the parent component. When I import this component and try to use it, I get an empty div in place of the map. Does anyone know what I'm doing wrong?
Here is the parent component's code:
<template>
<div>
<map-container></map-container>
</div>
</template>
<script>
import MapContainer from '../mapping/MapContainter.vue';
export default {
components: {
'map-container': MapContainer
}
}
</script>

I fixed this by adding the following class to the div with the ref:
.map {
width: 100% !important;
height: 600px !important;
}
(The !important's might not be strictly necessary).

Related

Add Vue component to canvas with fabric.js library

Instead of creating custom object shape, I already have it as a Vue component. Here is what I have tried:
<template>
<div>
<canvas ref="canvas"></canvas>
<button type="button" #click="addVueComponent()">Add to canvas</button>
</div>
</template>
<script>
import { fabric } from 'fabric';
import CustomComponent from "CustomComponent.vue";
export default {
mounted() {
this.canvas = new fabric.Canvas(this.$refs.canvas);
},
methods:
{
addVueComponent() {
var vueComponent = new Vue({
template: '<custom-component ref="customComp"></custom-component>',
components: { 'custom-component': CustomComponent},
});
vueComponent.$mount();
var customObj = new fabric.Rect({
width: 100,
height: 100,
left: 50,
top: 50,
});
customComponent._element = vueComponent.$refs.customComponent.$el;
this.canvas.add(customComponent);
}
}
}
but instead of a rendered component I am just getting black rectangle as a result.
Is it even possible to add Vue component to canvas and what is the way to do it?

Cannot use Swiper with "vue": "^2.6.11"

I am trying to use Swiper with "vue": "^2.6.11", but it throws runtime errors. I followed the guide from https://swiperjs.com/vue, and changed the imports to:
// Import Swiper Vue.js components
import { Swiper, SwiperSlide } from 'swiper/vue/swiper-vue.js';
// Import Swiper styles
import 'swiper/swiper-bundle.css';
Error message:
Property or method "onSwiper" is not defined on the instance but referenced during render, Invalid handler for event "swiper": got undefined , Failed to mount component: template or render function not defined
The Swiper components only work with Vue 3. Those components cannot be used in Vue 2, but the Swiper API can be used directly instead:
Apply a template ref on the target Swiper container element in the template.
In the component's mounted() hook, initialize an instance of Swiper, passing the template ref and Swiper options that include selectors for the pagination/navigation elements in the template.
<script>
import Swiper, { Navigation, Pagination } from 'swiper'
import 'swiper/css'
import 'swiper/css/navigation'
import 'swiper/css/pagination'
export default {
mounted() {
2️⃣
new Swiper(this.$refs.swiper, {
// configure Swiper to use modules
modules: [Navigation, Pagination],
// Optional parameters
loop: true,
// If we need pagination
pagination: {
el: '.swiper-pagination',
},
// Navigation arrows
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev',
},
// And if we need scrollbar
scrollbar: {
el: '.swiper-scrollbar',
},
})
},
}
</script>
<template>
<!-- Slider main container -->
<div 1️⃣ ref="swiper" class="swiper">
<!-- Additional required wrapper -->
<div class="swiper-wrapper">
<!-- Slides -->
<div class="swiper-slide">Slide 1</div>
<div class="swiper-slide">Slide 2</div>
<div class="swiper-slide">Slide 3</div>
</div>
<!-- If we need pagination -->
<div class="swiper-pagination"></div>
<!-- If we need navigation buttons -->
<div class="swiper-button-prev"></div>
<div class="swiper-button-next"></div>
<!-- If we need scrollbar -->
<div class="swiper-scrollbar"></div>
</div>
</template>
<style scoped>
.swiper-slide {
display: flex;
justify-content: center;
align-items: center;
}
</style>
demo
Make some additions for #tony19's good answer.
Here is a demo example project: Live Demo
<script>
import Swiper, { Navigation, Pagination, Autoplay } from 'swiper'
import 'swiper/swiper-bundle.min.css'
export default {
data() {
return {
activeIndex: 0,
}
},
mounted() {
const SECOND = 1000 // milliseconds
new Swiper(this.$refs.swiper, {
modules: [Navigation, Pagination, Autoplay],
loop: true,
autoplay: {
delay: 3 * SECOND,
disableOnInteraction: false,
},
speed: 2 * SECOND,
pagination: {
el: '.swiper-pagination',
clickable: true,
},
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev',
},
on: {
slideChange: (swiper) => {
this.activeIndex = swiper.realIndex
},
},
})
},
}
</script>

set default map centering with props in vue

I'm using vue to make an arc gis driven application, because the map's center can changed based on user location, I am setting the default location to some props then passing them into my map component to be called in the map components mounted() hook for rendering.
I think i'm pretty close to doing this correctly but when I log my props in my map component in the console it says they're undefined.
I'm not sure if my app code is at fault or my child component code?
as you can see in my app.vue I am even trying to pass in plain integers to test the values and they are still undefined.
app.vue
<template>
<div id="app">
<web-map v-bind:centerX="-118" v-bind:centerY="34" />
<div class="center">
<b-button class="btn-block" #click="getLocation" variant="primary">My Location</b-button>
</div>
</div>
</template>
<script>
import WebMap from './components/webmap.vue';
export default {
name: 'App',
components: { WebMap },
data(){
return{
lat: -118,
long: 34
}
},
};
</script>
webmap.vue
<template>
<div></div>
</template>
<script>
import { loadModules } from 'esri-loader';
export default {
name: 'web-map',
props:['centerX, centerY'],
data: function(){
return{
X: this.centerX,
Y: this.centerY
}
},
mounted() {
console.log(this.X,this.Y)
// lazy load the required ArcGIS API for JavaScript modules and CSS
loadModules(['esri/Map', 'esri/views/MapView'], { css: true })
.then(([ArcGISMap, MapView]) => {
const map = new ArcGISMap({
basemap: 'topo-vector'
});
this.view = new MapView({
container: this.$el,
map: map,
center: [this.X,this.Y], ///USE PROPS HERE FOR NEW CENTER
zoom: 8
});
});
},
beforeDestroy() {
if (this.view) {
// destroy the map view
this.view.container = null;
}
}
};
</script>
There is a typo. Change:
props:['centerX, centerY'],
to:
props:['centerX', 'centerY'],

:hover color in vue components from props

Some of my single-file components need to take hover color from props.
My solution is that i set css variables in the following way (the main part is in the mounted(){...})
<template>
<div class="btnWrapper" ref="btnWrapper">...</div>
</template>
...
...
props() {
color1: {type: String, default: 'blue'},
},
mounted () {
this.$refs.btnWrapper.style.setProperty('--wrapHoverColor', this.color1)
}
...
...
<style scoped>
.btnWrapper {
--wrapHoverColor: pink;
}
.btnWrapper:hover {
background-color: var(--wrapHoverColor) !important;
}
</style>
This solution seems kind of woowoo.
But maybe there is no better way with pseudo elements, which are hard to control from js.
Do you guys ever take pseudo element's properties from props in vue components?
You have two different ways to do this.
1 - CSS Variables
As you already know, you can create CSS variables from what you want to port from JS to CSS and put them to your root element :style attr on your components created hooks, and then use them inside your CSS codes with var(--x).
<template>
<button :style="style"> Button </button>
</template>
<script>
export default {
props: ['color', 'hovercolor'],
data() {
return {
style: {
'--color': this.color,
'--hovercolor': this.hovercolor,
},
};
}
}
</script>
<style scoped>
button {
background: var(--color);
}
button:hover {
background: var(--hovercolor);
}
</style>
2 - Vue Component Style
vue-component-style is a tiny (~1kb gzipped) mixin to do this internally. When you active that mixin, you can write your entire style section inside of your component object with full access to the component context.
<template>
<button class="$style.button"> Button </button>
</template>
<script>
export default {
props: ['color', 'hovercolor'],
style({ className }) {
return [
className('button', {
background: this.color,
'&:hover': {
background: this.hovercolor,
},
});
];
}
}
</script>

Vue2-leaflet map not showing properly in BoostrapVue modal

Here's my problem - a Vue2 leaflet map does not render correctly in BootstrapVue modal.
Here's what it looks like visually (it should show just the ocean)
<template>
<div>
<b-modal size="lg" :visible="visible" #hidden="$emit('clear')" title="Event details">
<div class="foobar1">
<l-map :center="center" :zoom="13" ref="mymap">
<l-tile-layer :url="url" :attribution="attribution"></l-tile-layer>
<l-marker :lat-lng="center"></l-marker>
</l-map>
</div>
<template slot="modal-footer">
<b-btn variant="danger" #click="deleteEventLocal(event.id)">Delete</b-btn>
</template>
</b-modal>
</div>
</template>
<script>
import * as moment from "moment";
import { LMap, LMarker, LTileLayer } from "vue2-leaflet";
import { deleteEvent } from "./api";
import "vue-weather-widget/dist/css/vue-weather-widget.css";
import VueWeatherWidget from "vue-weather-widget";
export default {
data() {
return {
center: L.latLng(event.latitude, event.longitude),
url: "http://{s}.tile.osm.org/{z}/{x}/{y}.png",
attribution:
'© OpenStreetMap contributors'
};
},
props: {
visible: {
type: Boolean
},
event: {
required: true,
type: Object
}
},
methods: {
async deleteEventLocal(id) {
await deleteEvent(id);
this.$emit("refresh");
this.$emit("clear");
}
},
components: {
weather: VueWeatherWidget,
LMap,
LMarker,
LTileLayer
}
};
</script>
As you can see there aren't any CSS rules that could make the map spill outside the modal as it does. Which is weird.
I'm kinda asking this question to answer it myself as I couldn't find a solution before.
There were 3 issues because of which this was happening.
1. First - I forgot to load the leaflet css into main.js - this is why the leaflet map was somehow outside the modal.
//src/main.js
import '#babel/polyfill';
import Vue from 'vue';
import './plugins/bootstrap-vue';
import App from './App.vue';
import router from './router';
import store from './store';
//above imports not important to this answer
import 'leaflet/dist/leaflet.css'; //<--------------add this line
new Vue({
router,
store,
render: h => h(App),
}).$mount('#app');
2. Now the map may disappear. Set a width and height on the l-map component's container. I used a class but you can use style="" etc.
<div class="foobar1"> <!-- <--- Add a class on l-map's container -->
<l-map :center="center" :zoom="13">
<l-tile-layer :url="url" :attribution="attribution"></l-tile-layer>
<l-marker :lat-lng="center"></l-marker>
</l-map>
</div>
<style lang="scss">
.foobar1 { /* <--- class we added above */
width: 100%;
height: 400px;
}
</style>
3. Now your map will render within the modal but if you move the map's view, you'll see that leaflet does not download the map's squares in time.
You will see something like this:
To fix this:
create an event handler on b-modal for the #shown event.
<b-modal
#shown="modalShown"
#hidden="$emit('clear')"
size="lg"
:visible="visible"
title="Event details"
>
I called mine modalShown.
Then, add a ref attribute to your l-map. I called mine mymap.
<l-map :center="center" :zoom="13" ref="mymap"> <!-- ref attribute added to l-map -->
<l-tile-layer :url="url" :attribution="attribution"></l-tile-layer>
<l-marker :lat-lng="center"></l-marker>
</l-map>
Then, create a modalShown method in the Vue methods for your view/component and call invalidateSize() inside.
export default {
data() {
//some data here
}
methods: {
modalShown() {
setTimeout(() => {
//mapObject is a property that is part of leaflet
this.$refs.mymap.mapObject.invalidateSize();
}, 100);
}
}
}
Now everything should be fine:
map should not spill outside the modal
map should be visible (duh)
map squares should be downloaded when within map body
Here's my full code, it contains some stuff specific to my app but overall it contains all of the code snippets above.
Addtional to Artur Tagisow answer
You can also use this approach to your parent component if your map is in child component.
export default {
data() {
//some data here
}
methods: {
modalShown() {
setTimeout(() => {
window.dispatchEvent(new Event("resize"));
}, 100);
}
}
}
For vue.js and nuxt.js developers , probably it's because of using v-show or v-if
!in your case display none happening by bootstrap modal
but dont worry the only thing u have to do is using client-only (its like ssr but for new version of js frameworks like nuxt or vue):
<client-only>
<div id="bootstrapModal">
<div id="map-wrap" style="height: 100vh">
<l-map :zoom=13 :center="[55.9464418,8.1277591]">
<l-tile-layer url="http://{s}.tile.osm.org/{z}/{x}/{y}.png"></l-tile-layer>
<l-marker :lat-lng="[55.9464418,8.1277591]"></l-marker>
</l-map>
</div>
</div>
</client-only>
ps: if still not loaded in iphone browsers it's probably because of geolocation