So I have the following Vue app
App.vue
<template id="main-page">
<v-ons-page>
<l-map
ref="map"
v-if="showMap"
:zoom="zoom"
:center="center"
:options="mapOptions"
style="height: 100%"
#update:center="centerUpdate"
#update:zoom="zoomUpdate"
#update:bounds="boundUpdate"
#move="move"
>
<l-tile-layer
:url="url"
:attribution="attribution"
/>
<l-marker
v-if="addNewMarker"
:lat-lng="liveCenter"
>
<l-icon
:icon-size="dynamicSize"
:icon-anchor="dynamicAnchor"
icon-url="https://cdn3.iconfinder.com/data/icons/metro-explorer/512/my_location-512.png"
/>
</l-marker>
<l-marker
v-for="marker in markers"
:key="marker.id"
:visible="marker.visible"
:draggable="marker.draggable"
:lat-lng.sync="marker.position"
#click="alert(marker)"
>
</l-marker>
<l-control class="example-custom-control"
:position="'bottomright'">
<img
#click="showAddMarkerSetup"
src="https://cdn4.iconfinder.com/data/icons/iconsimple-places/512/pin_plus-512.png"
height="42" width="42"
>
</l-control>
</l-map>
<Cancel v-if="addNewMarker"/>
<Ok v-if="addNewMarker"/>
</v-ons-page>
</template>
<style>
.example-custom-control {
background: #fff;
padding: 0 0.5em;
border: 1px solid #aaa;
border-radius: 0.1em;
}
.ok {
position: absolute;
bottom: 20px;
right: 30%;
width: 100px;
height: 50px;
z-index:999;
background: #000;
color: white;
opacity: 0.5;
display: flex;
justify-content: center;
align-items: center;
}
</style>
<script>
import { latLng } from "leaflet";
import {
LMap,
LTileLayer,
LMarker,
LControl,
LIcon
} from "vue2-leaflet";
import Cancel from './components/add/Cancel'
import Ok from './components/add/Ok'
export default {
name: "Example",
components: {
LMap,
LTileLayer,
LMarker,
LControl,
LIcon,
Cancel,
Ok
},
data() {
return {
zoom: 18,
center: latLng(14.601205, 120.974780),
url: 'https://api.test.goxhere.com/place-geoserver/geoserver/gwc/service/gmaps?layers=postgis:goxhere&zoom={z}&y={y}&x={x}&format=image/png',
attribution:
'© GoXhere',
withPopup: latLng(14.601205, 120.974780),
withTooltip: latLng(14.601205, 120.974980),
currentZoom: 11.5,
currentCenter: latLng(14.5905, 120.9781),
liveCenter: latLng(14.601205, 120.974780),
showParagraph: false,
mapOptions: {
zoomSnap: 0.5,
zoomControl: false
},
showMap: true,
markers: [],
addNewMarker: false,
dynamicSize: [32, 32],
dynamicAnchor: [32, 32]
};
},
methods: {
zoomUpdate(zoom) {
this.currentZoom = zoom;
},
centerUpdate(center) {
this.currentCenter = center;
},
showLongText() {
this.showParagraph = !this.showParagraph;
},
innerClick() {
alert("Click!");
},
showAlert() {
alert("Click!");
},
showAddMarkerSetup() {
console.log('showAddMarkerSetup clicked')
console.log('adding a marker at the center')
this.addNewMarker = !this.addNewMarker
},
boundUpdate() {
console.log('bound update')
// check if
},
move(e) {
console.log('map moving')
if (this.addNewMarker) {
this.liveCenter = e.target.getCenter()
}
}
}
};
</script>
Here is my Cancel.vue component
<template>
<div class="cancel">Cancel</div>
</template>
<style scoped>
.cancel {
position: absolute;
bottom: 20px;
left: 30%;
width: 100px;
height: 50px;
z-index:999;
background: #000;
color: white;
opacity: 0.5;
display: flex;
justify-content: center;
align-items: center;
}
</style>
<script>
export default {
data() {
return {
"a": "sss"
}
},
methods: {
mounted() {
console.log('mounted from cancel')
}
}
}
</script>
If I set the addNewMarker field to true, the Cancel component does not appear in the UI (even though I can see its being added in Vue component console)
However, if I use v-show instead of v-if, I can see the Cancel component showing on and off if I toggle the addNewMarker. Is there anything I should configure so that v-if works correctly for single file templates?
Here is a video demo showing v-if (not working) and v-show (working)
Related
So I have seen this pie menu generator which gives you an HTML, CSS and JS code and I wanted to use it in my Vue 3 project. I am new to vue and this was how I imported it.
here is the link to the pie menu generator: http://pmg.softwaretailoring.net/
I did this in my wrapper component. Somehow this bought many errors in which I think is because of how I imported the JS library.
<template>
<div class="context-menu" v-show="show" :style="style" ref="context" tabindex="0" #blur="close">
<div id='piemenu' data-wheelnav data-wheelnav-slicepath='DonutSlice' data-wheelnav-marker
data-wheelnav-markerpath='PieLineMarker' data-wheelnav-rotateoff data-wheelnav-navangle='270'
data-wheelnav-cssmode data-wheelnav-init>
<div data-wheelnav-navitemtext='0' onmouseup='alert("Place your logic here.");'></div>
<div data-wheelnav-navitemtext='1' onmouseup='alert("Place your logic here.");'></div>
</div>
</div>
</template>
<script>
import '../assets/raphael.min.js'
import '../assets/raphael.icons.min.js'
import '../assets/wheelnav.min.js'
var piemenu = new wheelnav('piemenu');
piemenu.wheelRadius = piemenu.wheelRadius * 0.83;
piemenu.createWheel();
export default {
name: 'CmpContextMenu',
props: {
display: Boolean, // prop detect if we should show context menu
},
data() {
return {
left: 0, // left position
top: 0, // top position
show: false, // affect display of context menu
};
},
computed: {
// get position of context menu
style() {
return {
top: this.top + 'px',
left: this.left + 'px',
};
},
},
methods: {
// closes context menu
close() {
this.show = false;
this.left = 0;
this.top = 0;
this.myTrigger = false;
console.log('trigger false');
},
open(evt) {
this.show = true;
// updates position of context menu
this.left = evt.pageX || evt.clientX;
this.top = evt.pageY || evt.clientY;
},
},
};
</script>
<style>
.context-menu {
position: fixed;
z-index: 999;
cursor: pointer;
}
#piemenu>svg {
width: 100%;
height: 100%;
}
#piemenu {
height: 400px;
width: 400px;
margin: auto;
}
#media (max-width: 400px) {
#piemenu {
height: 300px;
width: 300px;
}
}
[class|=wheelnav-piemenu-slice-basic] {
fill: #497F4C;
stroke: none;
}
[class|=wheelnav-piemenu-slice-selected] {
fill: #497F4C;
stroke: none;
}
[class|=wheelnav-piemenu-slice-hover] {
fill: #497F4C;
stroke: none;
fill-opacity: 0.77;
cursor: pointer;
}
[class|=wheelnav-piemenu-title-basic] {
fill: #333;
stroke: none;
}
[class|=wheelnav-piemenu-title-selected] {
fill: #fff;
stroke: none;
}
[class|=wheelnav-piemenu-title-hover] {
fill: #222;
stroke: none;
cursor: pointer;
}
[class|=wheelnav-piemenu-title]>tspan {
font-family: Impact, Charcoal, sans-serif;
font-size: 24px;
}
.wheelnav-piemenu-marker {
stroke: #444;
stroke-width: 2;
}
</style>
MCVE
I have a Tabpane component that takes slots as input. When imported from the template it works as expected.
<Tabpane>
<div caption="I am div 1">Div 1</div>
<div caption="I am div 2">Div 2</div>
</Tabpane>
However when imported from an other component ( Composite in the example ), then it triggers the following warning:
Slot "default" invoked outside of the render function:
this will not track dependencies used in the slot. Invoke the slot function inside the render function instead.
// src/components/Composite.js
import { defineComponent, h } from "vue";
import Tabpane from "./Tabpane.vue";
export default defineComponent({
name: "Composite",
setup() {
const slots = [
h("div", { caption: "I am div 1" }, ["Div 1"]),
h("div", { caption: "I am div 2" }, ["Div 2"])
];
return () => h(Tabpane, {}, () => slots);
}
});
Solved.
The problem was that I called slots.default() from within setup, but not within the returned render function.
Also this component reflected a very beginner approach to reactivity. By now I know better. The old problematic solution is still there in src/components/Tabpane.vue.
The right solution that triggers no warning is:
// src/components/Tabpane2.vue
<script>
import { defineComponent, h, reactive } from "vue";
export default defineComponent({
name: "Tabpane2",
props: {
width: {
type: Number,
default: 400,
},
height: {
type: Number,
default: 200,
},
},
setup(props, { slots }) {
const react = reactive({
selectedTab: 0,
});
return () =>
h("div", { class: ["vertcont"] }, [
h(
"div",
{
class: ["tabs"],
},
slots.default().map((tab, i) =>
h(
"div",
{
class: {
tab: true,
selected: i === react.selectedTab,
},
onClick: () => {
react.selectedTab = i;
},
},
[tab.props.caption]
)
)
),
h(
"div",
{
class: ["slotscont"],
style: {
width: `${props.width}px`,
height: `${props.height}px`,
},
},
slots.default().map((slot, i) =>
h(
"div",
{
class: {
slot: true,
active: react.selectedTab === i,
},
},
[slot]
)
)
),
]);
},
});
</script>
<style>
.tab.selected {
background-color: #efe;
border: solid 2px #afa !important;
border-bottom: transparent !important;
}
.tab {
background-color: #eee;
}
.tabs .tab {
padding: 5px;
margin: 2px;
border: solid 2px #aaa;
border-radius: 8px;
border-bottom: transparent;
cursor: pointer;
user-select: none;
transition: all 0.5s;
color: #007;
}
.tabs {
display: flex;
align-items: center;
margin-left: 5px;
}
.vertcont {
display: flex;
flex-direction: column;
margin: 3px;
}
.slotscont {
position: relative;
overflow: scroll;
padding: 5px;
border: solid 1px #777;
}
.slot {
visibility: hidden;
position: absolute;
}
.slot.active {
visibility: visible;
}
</style>
Slots need to be invoked within the render function and or the <template> box to ensure they keep their reactivity.
A full explanation can be found in this post: https://zelig880.com/how-to-fix-slot-invoked-outside-of-the-render-function-in-vue-3
I'm pretty new to Vue, I have an app running with a few different pages. One of the pages I'm trying to have is one that displays all images that are found in a directory on my host and to have them auto reload whenever the image changes (a separate app updates these images every 10 seconds). I've been able to display all the images (although only by listing them) and I can't seem to figure out how to get the pictures to auto refresh. Below is the code, any help is much appreciated.
<template>
<div id="app" class="media-display">
<figure class="media-post" v-for="(image, index) in images" v-bind:key="image.id" >
<img :key="index" :src="getImage(index)" v-bind:alt="image" width="580" height="390" v-bind:key="index" />
</figure>
</div>
</template>
<script>
import moment from 'moment'
export default {
el: '#app',
data(){
return {
images: [
'/charts/chart1.png?rnd='+Math.random(),
'/charts/chart2.png?rnd='+Math.random(),
'/charts/chart3.png?rnd='+Math.random(),
],
id : 1,
}
},
methods: {
getImage(index) {
return this.images[index]
}
}
}
</script>
<style>
.media-post {
display: inline-block;
padding: 1vmin;
//border-radius: 2vmin;
box-shadow: 0 10px 5px rgba(0, 0, 0, 0.2);
margin: 0;
background: #FFF;
position: relative;
cursor: pointer;
}
.media-post img {
//border-radius: 1vmin;
display: block;
max-width: 100%;
height: auto;
}
.media-post figcaption {
color: #FFF;
position: absolute;
top: 0;
left: 0;
font-weight: bold;
padding: 1rem 1.5rem;
z-index: 2;
font-size: 2rem;
text-shadow: 0 1px 2px #000;
}
</style>
You just need to regenerate the random number suffix periodically. Untested:
<img
v-for="url of images"
:key="url"
:src="url + '?rnd=' + cacheKey"
/>
data() {
return {
images: [
'/charts/chart1.png',
'/charts/chart2.png',
'/charts/chart3.png',
],
cacheKey: +new Date(),
};
},
created() {
this.interval = setInterval(() => {
this.cacheKey = +new Date();
}, 60 * 1000);
},
destroyed() {
clearInterval(this.interval);
},
If it isn't clear, +new Date() returns the number of seconds since 1 January 1970 00:00:00 UTC (docs).
What I am trying to build (and not use an existing solution) is an indeterminate loader with the following template:
<template>
<div class="slider">
<div class="line" :style="getLineStyle"></div>
<div class="subline inc"></div>
<div class="subline dec"></div>
</div>
</template>
I then use a getter to add the styles for the div with the line class (which works fine).
#Prop({ default: "hsl(0, 0%, 90%)" }) private backColor!: string;
...
public get getLineStyle(): any {
return {
"background-color": "black",
position: "absolute",
opacity: "0.4",
background: this.backColor,
width: "150%",
height: "100%"
};
}
I also have the following CSS:
<style lang="scss" scoped>
.slider {
position: relative;
height: 2px;
overflow-x: hidden;
}
.subline {
position: absolute;
background: #4a8df8;
height: 100%;
}
.inc {
animation: increase 2s infinite;
}
.dec {
animation: decrease 2s 0.5s infinite;
}
#keyframes increase {
from {
left: -5%;
width: 5%;
}
to {
left: 130%;
width: 100%;
}
}
#keyframes decrease {
from {
left: -80%;
width: 80%;
}
to {
left: 110%;
width: 10%;
}
}
</style>
What I want to do is turn the .inc and .dec classes to property getters as well so that I can bind the animation duration (currently set to 2s) to a property.
I initially tried modifying the template to:
<template>
<div class="slider">
<div class="line" :style="getLineStyle"></div>
<div class="subline inc" :style="getAnimateIncreaseStyle"></div>
<div class="subline dec" :style="getAnimateDecreaseStyle"></div>
</div>
</template>
With the following getters:
public get getAnimateIncreaseStyle() {
return {
animation: "increase 2s infinite"
};
}
public get getAnimateDecreaseStyle() {
return {
animation: "decrease 2s 0.5s infinite"
};
}
Only to realise that animations cannot work when added inline.
I cannot think of any other way of doing this. Any ideas?
This is how i bind the animation duration on my progress bar in vue 3, as the previous response. It's necessary removes the scope on style
<template>
...
<div v-if="duration > 0" class="w-full bg-gray-200 -mb-1">
<div
:class="`progress-bar h-0.5 progress-bar-${themeColor}`"
:style="animation"
></div>
</div>
...
</template>
<script lang="ts">
...
props: {
duration: {
type: Number,
default: 10,
},
themeColor: {
type: String,
required: false,
default: "blue",
},
},
computed: {
animation(): string {
return `animation: ${this.duration}s linear theme-color, ${this.duration}s ease-out enter forwards`;
},
},
...
<script>
<style lang="scss">
.progress-bar {
#apply bg-gray-300;
transform-origin: right;
&.progress-bar-blue {
#apply bg-gradient-to-r from-blue-600 to-blue-300;
}
&.progress-bar-green {
#apply bg-gradient-to-r from-green-600 to-green-300;
}
&.progress-bar-yellow {
#apply bg-gradient-to-r from-yellow-600 to-yellow-300;
}
&.progress-bar-red {
#apply bg-gradient-to-r from-red-500 to-red-300;
}
}
#keyframes theme-color {
100% {
background-position: 0%;
}
0% {
background-position: 100%;
}
}
#keyframes enter {
100% {
transform: scaleX(0);
}
0% {
transform: scaleX(1);
}
}
</style>
How to accomplish below task using vue-resource:
Include preloader text Loading... or gif image when fetching the data
from the server.
Show success message on form submit.
One way of doing this is :
<template>
<div>
<div class="loader" v-if="loader"></div>
<div>
//display fetchedData using logic you wish like v-for.....
</div>
<form>
//your form inputs
<button #click.prevent="submit">Submit</button>
</form>
</div>
</template>
<script>
export default{
data(){
return{
loader: false,
fetchedData: null
}
},
mounted(){
this.loader = true;
this.$httpget('your_url')
.then(response => {
this.fetchedData = response;
this.loader = false;
},err => {
});
},
methods:{
submit(){
this.loader = true;
this.$http.post('your_url', {your_body})
.then(response => {
this.loader = false;
},err => {
alert('form not submitted');
});
}
},
}
</script>
<style scoped>
loader {
position: absolute;
left:50%;
top:50%;
transform: translate(-50%, -50%);
border: 10px solid #f3f3f3; /* Light grey */
border-top: 16px solid #3498db; /* Blue */
border-radius: 50%;
width: 75px;
height: 75px;
animation: spin 2s linear infinite;
}
#keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
Here is the working fiddle
This was questioned by me, with the help that I got from #Vamsi, here is my solution:
Component
<loading-indicator v-if="loadingGroup" :bgAlpha="'.6'"></loading-indicator>
<script>
import LoadingIndicator from '../partials/LoadingIndicator'
export default {
data () {
return {
loadingGroup: true,
}
},
components: {LoadingIndicator},
methods: {
fetchGroup() {
let _this = this;
this.loadingGroup = true;
api._get({url: 'api/group/' + _this.$route.params.id})
.then(function (response) {
_this.groupData = response.data;
_this.loadingGroup = false;
});
}
},
mounted() {
this.fetchGroup();
}
}
</script>
My Template that's in: ../partials/LoadingIndicator.vue
<template>
<div class="pin pin-xy d-flex"
:style="{ backgroundColor: 'rgba(255, 255 ,255,' + bgAlpha + ')'}">
<div class="loading-indicator">
<div class="loading-indicator-circle"></div>
</div>
</div>
</template>
<script>
export default {
props: {
bgAlpha: String
}
}
</script>
<style lang="scss">
.pin {
position: absolute;
&-xy {
top: 0;
left: 0;
right: 0;
bottom: 0;
}
}
.d-flex {
display: flex;
}
.loading-indicator {
width: 32px;
height: 32px;
margin: auto;
overflow: hidden;
animation: animation-fadeIn 1s ease-in;
}
.loading-indicator-circle {
animation: loading-indicator-rotation 0.67s linear infinite;
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAzMiAzMic+PGxpbmVhckdyYWRpZW50IGlkPSdGYXN0TG9hZGluZ0luZGljYXRvci1saW5lYXJHcmFkaWVudCcgZ3JhZGllbnRVbml0cz0ndXNlclNwYWNlT25Vc2UnIHgxPScxLjc4MDQnIHkxPScxNi4wMzc5JyB4Mj0nMzAuMTQzOScgeTI9JzE2LjAzNzknPjxzdG9wIG9mZnNldD0nMC40MTY5JyBzdG9wLWNvbG9yPScjQ0RDRkQyJy8+PHN0b3Agb2Zmc2V0PScwLjkzNzYnIHN0b3AtY29sb3I9J3JnYmEoMjQ4LDI0OCwyNDksMCknLz48L2xpbmVhckdyYWRpZW50PjxjaXJjbGUgY3g9JzE2JyBjeT0nMTYnIHI9JzEyLjcnIHN0eWxlPSdmaWxsOiBub25lOyBzdHJva2U6IHVybCgjRmFzdExvYWRpbmdJbmRpY2F0b3ItbGluZWFyR3JhZGllbnQpOyBzdHJva2Utd2lkdGg6IDI7Jz48L2NpcmNsZT48L3N2Zz4=");
height: 100%;
width: 100%
}
#keyframes loading-indicator-rotation {
from {
transform: rotate(0deg)
}
to {
transform: rotate(360deg)
}
}
</style>