How to propmt user in Vuejs pwa app on update available? - vue.js

I've read through a couple of vuejs PWA examples for updating PWA, and it seems non worked for me and made me confused with these problems:
1. When I build the app and run it,
it will instantly download the content and shows the new content.
2. It will ask every time that there is a new version.
How should I fix these problems?
I just want to prompt the user to see that there is a new version(e.g.v1) and if he/she wants, update the app after accepting, and don't bother he/her on the next refresh until there is a new version(e.g. v2)

Thanks to custom service worker exmaple, Working solution on pwa update:
//vue.config.js
module.exports = {
publicPath: "./",
pwa: {
themeColor: "#42b983",
msTileColor: "#42b983",
appleMobileWebAppCache: "yes",
manifestOptions: {
background_color: "#42b983"
}
}
};
//registerServiceWorker.js:
import { Workbox } from "workbox-window";
let wb;
if ("serviceWorker" in navigator) {
wb = new Workbox(`${process.env.BASE_URL}service-worker.js`);
wb.addEventListener("controlling", () => {
window.location.reload();
});
wb.register();
} else {
wb = null;
}
export default wb;
//main.js
import Vue from "vue";
import App from "./App.vue";
import wb from "./registerServiceWorker";
Vue.prototype.$workbox = wb;
new Vue({
render: h => h(App)
}).$mount("#app");
//App.vue
<template>
<div id="app">
<img alt="Vue logo" src="./assets/logo.png" />
<HelloWorld msg="Vue.js with PWA" />
<div class="update-dialog" v-if="prompt">
<div class="update-dialog__content">
A new version is found. Refresh to load it?
</div>
<div class="update-dialog__actions">
<button
class="update-dialog__button update-dialog__button--confirm"
#click="update"
>
Update
</button>
<button
class="update-dialog__button update-dialog__button--cancel"
#click="prompt = false"
>
Cancel
</button>
</div>
</div>
</div>
</template>
<script>
import HelloWorld from "./components/HelloWorld.vue";
export default {
name: "App",
components: {
HelloWorld,
},
methods: {
async update() {
this.prompt = false;
await this.$workbox.messageSW({ type: "SKIP_WAITING" });
},
},
data() {
return {
prompt: false,
};
},
created() {
if (this.$workbox) {
this.$workbox.addEventListener("waiting", () => {
this.prompt = true;
});
}
},
};
</script>
<style lang="scss">
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
.update-dialog {
position: fixed;
left: 50%;
bottom: 64px;
transform: translateX(-50%);
border-radius: 4px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
padding: 12px;
max-width: 576px;
color: white;
background-color: #2c3e50;
text-align: left;
&__actions {
display: flex;
margin-top: 8px;
}
&__button {
margin-right: 8px;
&--confirm {
margin-left: auto;
}
}
}
</style>

I'm not sure about the Vue,
But I used these below codes and worked just fine for me in a React project.
First, register the ServiceWorker by calling method registerValidSW:
function registerValidSW(swUrl: string) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (installingWorker == null) {
return;
}
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
const event = new Event("app-event-newContentAvailable");
window.dispatchEvent(event);
}
}
};
};
})
.catch(error => {
console.error('Error during service worker registration:', error);
});
}
I used workbox and this is what my workbox-build.js file looks like:
const workboxBuild = require('workbox-build');
// NOTE: This should be run *AFTER* all your assets are built
const buildSW = () => {
// This will return a Promise
return workboxBuild.injectManifest({
swSrc: 'src/sw-template.js', // this is your sw template file
swDest: 'build/sw.js', // this will be created in the build step
globDirectory: 'build',
globIgnores: ['**/service-worker.js', '**/precache-manifest.*.js'],
globPatterns: [
'**\/*.{js,css,html,png}',
]
}).then(({count, size, warnings}) => {
// Optionally, log any warnings and details.
warnings.forEach(console.warn);
console.log(`${count} files will be precached, totaling ${size} bytes.`);
});
}
buildSW();
And this is sw-template.js:
if ('function' === typeof importScripts) {
importScripts(
// 'https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js'
'static/sw-workbox/v4.3.1/workbox-sw.js'
);
/* global workbox */
if (workbox) {
/* injection point for manifest files. */
workbox.precaching.precacheAndRoute([]);
const cacheConfig = {
images: [
/\.(?:jpg|jpeg|png|gif|svg|ico)$/,
new workbox.strategies.CacheFirst({
cacheName: "images",
plugins: [
new workbox.expiration.Plugin({
maxEntries: 6000,
maxAgeSeconds: 30 * 24 * 60 * 60 // 30 Days
})
]
}),
"GET"
],
fonts: [
/\.(?:eot|ttf|woff|woff2)$/,
new workbox.strategies.CacheFirst({
cacheName: "fonts",
plugins: [
new workbox.cacheableResponse.Plugin({
statuses: [0, 200]
}),
new workbox.expiration.Plugin({
maxAgeSeconds: 60 * 60 * 24 * 365, // 1 year
maxEntries: 30
})
]
}),
"GET"
],
manifest: [
new RegExp('manifest.json'),
new workbox.strategies.CacheFirst({
cacheName: "manifest",
plugins: [
new workbox.expiration.Plugin({
// maxAgeSeconds: 60 * 60 * 24 * 2, // 2 days
maxAgeSeconds: 30 * 24 * 60 * 60, // 30 Days
maxEntries: 1
})
]
}),
"GET"
],
};
/* custom cache rules*/
workbox.routing.registerNavigationRoute('/index.html', {
blacklist: [
/^\/_/, /\/[^\/]+\.[^\/]+$/,
],
});
workbox.routing.registerRoute(...cacheConfig.images);
workbox.routing.registerRoute(...cacheConfig.fonts);
workbox.routing.registerRoute(...cacheConfig.manifest);
const SkipWaitingAndClaim = () => {
workbox.core.skipWaiting();
workbox.core.clientsClaim();
};
SkipWaitingAndClaim();
} else {
console.log('Workbox could not be loaded. No Offline support');
}
}
In the package.json I added a new line to my scripts so that I can build my sw.js file before building the app:
"scripts": {
"build-sw": "node ./src/sw-build.js",
...
...
},
And now you can add your EventListener in for instance App.tsx (I used modal with the reload action button):
componentDidMount() {
window.addEventListener("app-event-newContentAvailable", () => {
this.setState({ showConfirmReloadModal: true });
});
}

Related

How to change the content of webpage when I change the path of dynamic vue-router

I am using vue-router to write a dynamic router .and when I jump from one path to another, I find the path is indeed changed but the web page does not reload and I have no idea why.
the router definition is as follows:
const routes: Array<RouteRecordRaw> = [
{
path: '/lecture/:lecture_name',
name: 'Lecture',
component: LecturesBase,
}
]
and the dynamic web page vue components is like this:
LectureBase.vue
<template>
<div>
<a-layout>
<LectureNavigation />
<LectureTemplate
:page_name="page_name"
/>
<!-- <router-view>
</router-view> -->
<tui-juhe />
</a-layout>
</div>
</template>
<script>
import { ref } from 'vue';
import router from "#/router/index";
import { onBeforeRouteUpdate, useRoute } from "vue-router";
import LectureNavigation from "#/views/lecture/LectureNavigation.vue";
import LectureTemplate from "#/views/lecture/LectureTemplate.vue";
import TuiJuhe from "#/components/advertisement/TuiJuhe.vue";
export default {
name: "LectureBase",
setup() {
const route = useRoute();
},
components: {
LectureNavigation,
TuiJuhe,
LectureTemplate,
},
// props: {
// page_name: String, // 中文标题
// },
data() {
return {
$router: router,
page_name: ref(this.$route.params.lecture_name),
}
},
watch: {
'$route' (to, from) {
console.log('# to url: ' + to.path);
console.log('# to url: ' + this.$route.params.lecture_name);
this.page_name = this.$route.params.lecture_name;
console.log('# to url: ' + this.page_name);
this.$forceUpdate();
}
},
// methods: {
// refresh() {
// d
// }
// },
created() {
console.log("# url: " + this.$route.params.lecture_name);
}
};
</script>
<style>
</style>
LectureTemplate.vue
<template>
<a-layout class="variable_content" style="padding: 0 24px 24px">
<a-breadcrumb style="margin: 16px 0" :routes="router">
<a-breadcrumb-item>
<router-link to="/">
<home-outlined />
</router-link>
</a-breadcrumb-item>
<a-breadcrumb-item>
<router-link to="/lecture/lecture_home_page">
NLP 教程
</router-link>
</a-breadcrumb-item>
<!--a-breadcrumb-item> Home </a-breadcrumb-item-->
<a-breadcrumb-item>{{ this.title }}</a-breadcrumb-item>
</a-breadcrumb>
<a-layout-content :style="{
background: '#fff',
padding: '24px',
margin: 0,
width: '100%',
minHeight: '280px',
}">
<div>
<h1><b class="b_green">{{ this.title }}</b></h1>
<p class="description_text">
发布日期:{{ this.established_time }} 阅读量:{{ this.frontend_page_count }}
</p>
<div v-html="markdownToHtml"></div>
</div>
</a-layout-content>
</a-layout>
</template>
<script>
import { useMeta } from 'vue-meta';
import router from "../../router/index";
import { stat_instance } from "#/utils/request";
import blog_asset from "#/utils/blog_request";
import {
HomeOutlined,
} from "#ant-design/icons-vue";
export default {
name: 'LectureTemplate',
components: {
HomeOutlined,
},
// setup() {
// useMeta({ title: this.title})
// },
props: {
page_name: String, // 英文名,用于请求后端,展示 url
},
data() {
return {
router: router,
title: '',
established_time: '',
frontend_page_count: 0,
markdown: "### loading ...",
}
},
computed: {
markdownToHtml() {
var markdown_content = this.md(this.markdown);
return markdown_content;
}
},
created() {
console.log("## temp url: " + this.page_name);
stat_instance({
url: "/stat_api/frontend_page_statistics",
data: {
page_name: this.page_name,
}
})
.then((response) => {
this.frontend_page_count = response.data.frontend_page_count;
this.title = response.data.title;
this.established_time = response.data.established_time;
})
.catch(() => {
this.frontend_page_count = 0;
});
blog_asset({
url: "/lecture/" + this.page_name + "/README.md",
})
.then((response) => {
this.markdown = response.data;
})
.catch(() => {
this.markdown = "### Failed to request markdown file.";
});
}
}
</script>
<style lang="less" scoped>
h1 {
width: 100%;
// height: 80px;
font-size: 28px;
padding-top: 5px;
margin: 10px;
}
.b_green {
color: #00B441;
}
.description_text {
text-align: right;
font-size: 10px;
color: #777777;
}
</style>
So, what is wrong with my code, and the complete project code is at JioNLP_online.
You could git clone this repository and execute npm run serve to try this code and check the bug.
The program indeed captured the dynamic path of the vue-router but the web page does not changed at all.
Solution I found
I found this post about the same issue here.
Basically you need to add the :key attribute to the <vue-router> component where your page is rendered.
Example:
// this re-renders the page when the path changes
<router-view :key="$route.fullPath"></router-view>
My solution
The :key solution didn't work for me, because I didn't want the router to re-render. This would cause my smooth transition animation between the routes to break. So I tried solving it in a different way.
<script lang="ts" setup>
import { onMounted, watchEffect, ref } from "vue";
import { useRoute } from "vue-router";
const isLoading = ref<boolean>(true);
const route = useRoute();
const loadData = async (id) => {
isLoading.value = true;
// load dynamic data here and change ref values
isLoading.value = false; // after load
};
watchEffect(() => {
loadData(route.params.id);
});
onMounted(() => {
loadData(route.params.id);
});
</script>

Selecting a field in an ArcGis layer/map in a Nuxt application result in adding legend colorSlider on top of each other in the same div

I am using ArcGis in a Nuxt application. I have got a map with a feature layer (hosted on ArcGis) and a legend with a color slider in the top-right corner. The user can visualise different fields from the layer. For each field selected a new renderer is generated and therefore a new colorSlider. My problem is that every time the user select a new field, a new colorSlider is added above the previous one and I end up with three coloSliders in the legend. How can I fix that ?? I tried to destroy the previous colorSlider when I select a new field but it seems to destroy the div which contains the slider and then I have no slider at all anymore... This is the code =>
<template>
<div>
<div id="viewDiv"></div>
<div id="legend"></div>
<div id="containerDiv" class="esri-widget">
<span id="title" class="esri-widget">impact legend</span>
<div id="slider" ref="sliderr"></div>
</div>
</div>
</template>
<script>
import Map from '#arcgis/core/Map'
import MapView from '#arcgis/core/views/MapView'
import esriConfig from '#arcgis/core/config'
import FeatureLayer from '#arcgis/core/layers/FeatureLayer'
import * as colorRendererCreator from '#arcgis/core/smartMapping/renderers/color'
import ColorSlider from '#arcgis/core/widgets/smartMapping/ColorSlider'
export default {
props: {
selectedTab: {
type: Number,
default: 1,
},
},
data() {
return {
url: 'https://blablabla',
countries:
'https://blablabla',
projectLyr: undefined,
countryLyr: undefined,
map: new Map({ basemap: 'osm-light-gray' }),
view: undefined,
fieldName: '',
renderer: {},
filter: '',
rendererResult: undefined,
colorSlider: undefined,
}
},
mounted() {
esriConfig.apiKey =
'myApiKey'
this.projectLyr = new FeatureLayer({
url: this.url,
outFields: ['*'],
})
this.countryLyr = new FeatureLayer({
url: this.countries,
outFields: ['*'],
})
this.view = new MapView({
map: this.map,
center: [15, 50],
zoom: 6,
container: 'viewDiv',
})
this.updateLayer({ layer: this.projectLyr, value: 'Impact_PA_area' })
this.$nuxt.$on('filter-selected', this.updateLayer)
},
beforeDestroy() {
this.$nuxt.$off('tab-selected')
this.$nuxt.$off('filter-selected')
},
methods: {
generateRenderer(lyr) {
const colorParams = {
layer: lyr.layer,
field: `${lyr.field}`,
view: this.view,
theme: 'above-and-below',
}
colorRendererCreator
.createContinuousRenderer(colorParams)
.then((response) => {
// Set the renderer to the layer and add it to the map
this.rendererResult = response
lyr.layer.renderer = this.rendererResult.renderer
})
.then(() => {
// Construct a color slider from the result of smart mapping renderer
this.colorSlider = ColorSlider.fromRendererResult(this.rendererResult)
this.colorSlider.container = 'slider'
this.colorSlider.primaryHandleEnabled = true
this.colorSlider.viewModel.precision = 1
this.view.ui.add('containerDiv', 'top-right')
function changeEventHandler() {
const renderer = lyr.layer.renderer.clone()
const colorVariable = renderer.visualVariables[0].clone()
const outlineVariable = renderer.visualVariables[1]
colorVariable.stops = this.colorSlider.stops
renderer.visualVariables = [colorVariable, outlineVariable]
lyr.layer.renderer = renderer
}
this.colorSlider.on(
['thumb-change', 'thumb-drag', 'min-change', 'max-change'],
changeEventHandler
)
})
.catch((error) => {
console.error('Error: ', error)
})
},
filtering(value) {
if (value.value.isFilter) {
this.filter = `${value.value.value}`
this.projectLyr.definitionExpression = this.filter
} else {
this.projectLyr.definitionExpression = `${value.value.value} AND IS NOT NULL`
if (this.filter !== '') {
this.projectLyr.definitionExpression = this.filter
}
value.isCountry
? this.generateRenderer({
layer: this.countryLyr,
field: value.value.value,
})
: this.generateRenderer({
layer: this.projectLyr,
field: value.value.value,
})
}
},
updateLayer(value) {
this.$nextTick(() => {
if (this.selectedTab === 0) {
this.map.remove(this.projectLyr)
this.map.add(this.countryLyr)
this.filtering({ value, isCountry: true })
} else {
this.map.remove(this.countryLyr)
this.map.add(this.projectLyr)
this.filtering({ value, isCountry: false })
}
})
},
},
}
</script>
<style scoped>
#import 'https://js.arcgis.com/4.23/#arcgis/core/assets/esri/themes/light/main.css';
#viewDiv {
padding: 0;
margin: 0;
height: 100%;
width: 100%;
}
#containerDiv {
background-color: white;
padding: 3px;
text-align: center;
min-width: 260px;
}
</style>
I think you can just update the ColorSlider with the new ContinuousRendererResult data instead of trying to destroy/recreate. In a similar way you create it, use updateFromRendererResult method to update it (ArcGIS JS API - ColorSlider).

Getting navigation duplicated error while the route is being replaced on Vue-Router

I have a problem with vue router as defined on the title above.
Let's say I have a router-view which is renders pages dynamically when the user selected a page from the page selector component. What I'm expecting is I have to get the url to be like this:
http://localhost:port/editor/{appSlug}/layout-editor/page/{pageSlug}
But instead, I got this:
http://localhost:port/editor/{appSlug}/layout-editor/page/{pageSlug}-randomString
And the console shows this error:
NavigationDuplicated {_name: "NavigationDuplicated", name: "NavigationDuplicated", message: "Navigating to current location ("/editor/penerimaa…/page/input-pendaftaran-edrpekvl") is not allowed", stack: "Error↵ at new NavigationDuplicated (webpack-int…/node_modules/vue/dist/vue.runtime.esm.js:3876:9)"}`
I already checked the router file and still can't find out what is wrong with my route. I also tried the solution from this question but still having this error.
Can somebody please help me with this?
Please take a look at my code:
router.js
import Vue from 'vue'
import Router from 'vue-router'
import store from './store/index'
import Home from './views/home/Index.vue'
Vue.use(Router)
let router = new Router({
mode: 'history',
linkActiveClass: 'active',
linkExactActiveClass: 'exact-active',
routes: [{
path: '/',
name: 'home',
component: Home,
meta: {
requiresAuth: true
}
},
{
path: '/login',
name: 'login',
// route level code-splitting
// this generates a separate chunk (login.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import('./views/auth/Login.vue'),
meta: {
requiresGuest: true
}
},
{
path: '/register',
name: 'register',
component: () => import('./views/auth/Register.vue'),
meta: {
requiresGuest: true
}
},
{
path: '/forgot-password',
name: 'forgot-password',
component: () => import('./views/auth/extras/ForgotPassword.vue'),
meta: {
requiresGuest: true
}
},
{
path: '/database',
name: 'database',
component: () => import('./views/database/Index.vue'),
meta: {
requiresAuth: true
}
},
{
path: '/third-parties',
name: 'third-parties',
component: () => import('./views/third-parties/Index.vue'),
meta: {
requiresAuth: true
}
},
{
path: '/editor',
component: () => import('./components/ViewRenderer.vue'),
meta: {
requiresAuth: true,
requiresAdmin: true,
requiresEditor: true,
},
children: [{
props: true,
path: ':appSlug/layout-editor',
name: 'layout-editor',
component: () => import('./views/editor/Index.vue'),
children: [{
props: true,
path: 'page/:pageSlug',
name: 'layout-renderer',
component: () => import('./components/LayoutRenderer.vue'), // this is where the error occured.
}],
}]
},
]
})
// Route Middlewares
router.beforeEach((to, from, next) => {
const isLoggedIn = store.getters['auth/isLoggedIn']
// Role getters
const isAdmin = store.getters['auth/isAdmin']
const isEditor = store.getters['auth/isEditor']
// Redirect to the login page if the user is not logged in
// and the route meta record is requires auth
if (to.matched.some(record => record.meta.requiresAuth) && !isLoggedIn) {
next('/login')
}
// Redirect to the homepage page if the user is logged in
// and the route meta record is requires guest
if (to.matched.some(record => record.meta.requiresGuest) && isLoggedIn) {
next('/')
}
// Redirect to the preview page if the user is logged in
// but has no role assigned or the role is user
if (to.matched.some(
record => (
record.meta.requiresAuth &&
record.meta.requiresAdmin &&
record.meta.requiresEditor
)) && isLoggedIn && isAdmin !== true && isEditor !== true) {
next('/')
}
// Pass any access if not matches any of conditions above
next()
})
export default router
Editor/Index.vue
<template>
<div class="layout-editor container-fluid">
<ActivityBar></ActivityBar>
<Sidebar title="Layout Editor">
<PalleteControl></PalleteControl>
<Pallete :items="components" :list-style="pallete"></Pallete>
</Sidebar>
<Navbar class="editor-navbar">
<PageSelector></PageSelector>
<BaseButton id="create-page-button" text="Create new page"></BaseButton>
</Navbar>
<!-- Every selected page layout rendered here -->
<ViewRenderer></ViewRenderer>
<CommitBar></CommitBar>
</div>
</template>
<script>
import components from "#/data/components.json";
import data from "#/data/table.json";
import { mapGetters } from "vuex";
export default {
name: "LayoutEditor",
data() {
return {
components,
pallete: "grid"
};
},
computed: {
...mapGetters({
current: "apps/current" // Get current app
})
},
mounted() {
this.listenPalleteEvent();
this.listenPageSelectorEvent();
},
methods: {
listenPalleteEvent() {
EventBus.$on("switch-list-style", () => this.switchPallete());
},
switchPallete() {
if (this.pallete == "grid") return (this.pallete = "list");
return (this.pallete = "grid");
},
listenPageSelectorEvent() {
EventBus.$on("page-selected", component => {
this.$router.replace({
name: "layout-renderer",
params: { pageSlug: component.pageSlug, component }
});
});
}
}
};
</script>
<style lang="scss" scoped>
.layout-editor {
padding-left: 530px;
}
</style>
components/PageSelector.vue
<template>
<BaseDropdown
id="pages-dropdown-button"
id-obj="pageId"
name-obj="pageName"
:items="filtered"
:has-item-control="true"
text="Create new page or choose one from here"
event-keyword="page-selected"
>
<BaseInput
name="page-filter"
v-model="filter"
:borderless="true"
placeholder="Search by page name..."
></BaseInput>
<template #item-control>
<div class="item-control">
<BaseButton id="duplicate-page-button" text="Duplicate"></BaseButton>
<BaseButton id="delete-page-button" text="Delete"></BaseButton>
</div>
</template>
</BaseDropdown>
</template>
<script>
import { mapGetters } from "vuex";
export default {
data() {
return {
filter: ""
};
},
created() {
// Dispatch fetch page request on vuex store when the instance was created.
this.$store.dispatch("pages/load", this.currentApp);
},
computed: {
// Map getters from vuex store.
...mapGetters({
pages: "pages/pages",
currentApp: "apps/current"
}),
// Filter pages as long as user type in the dropdown input.
filtered() {
return this.pages.filter(page => {
return page.pageName.toLowerCase().includes(this.filter.toLowerCase());
});
}
}
};
</script>
<style lang="scss" scoped>
#import "../../sass/variables";
::v-deep .dropdown-item {
position: relative;
display: flex;
justify-content: space-between;
align-items: center;
&:hover {
.item-control {
opacity: 1;
}
}
}
::v-deep .item-control {
display: flex;
align-items: center;
justify-content: flex-end;
opacity: 0;
.form-group {
margin-bottom: 0;
}
.form-group .btn {
border-radius: 30px;
height: auto;
}
.form-group:first-child .btn {
margin-right: 5px;
}
.form-group:last-child .btn {
background-color: $red;
border-color: $red;
color: white;
&:hover {
background-color: darken($color: $red, $amount: 3);
}
}
}
</style>
components/ViewRenderer.vue
<template>
<router-view />
</template>
components/LayoutRenderer.vue
<template>
<div class="layout-renderer">
<GridLayout
:layout.sync="components"
:col-num="12"
:row-height="30"
:is-draggable="true"
:is-resizable="true"
:is-mirrored="false"
:vertical-compact="true"
:margin="[10, 10]"
:use-css-transforms="false"
:responsive="true"
:auto-size="true"
>
<GridItem
v-for="component in components"
:key="component.i"
:x="component.x"
:y="component.y"
:w="component.w"
:h="component.h"
:i="component.i"
>
<ComponentRenderer :component="component" />
</GridItem>
</GridLayout>
</div>
</template>
<script>
import { mapState } from "vuex";
import VueGridLayout from "vue-grid-layout";
export default {
components: {
GridLayout: VueGridLayout.GridLayout,
GridItem: VueGridLayout.GridItem
},
data() {
return {
components: []
};
},
created() {
this.fetchComponents();
},
methods: {
/**
* Fetch component from the backend based on the pageId
* occured by the vue-router's route parameters.
*
* #return void
*/
fetchComponents() {
let pageId = this.$route.params.component.pageId;
this.$store.dispatch("components/fetchComponents", pageId).then(() => {
this.components = this.$store.getters["components/components"];
});
}
}
};
</script>
<style lang="scss" scoped>
.layout-renderer {
margin-bottom: 100px;
}
#media only screen and (max-width: 501px) {
.vue-grid-item {
height: fit-content !important;
transform: none !important;
position: relative !important;
margin-bottom: 10px;
}
}
#media (hover: none), (hover: on-demand) {
.vue-grid-item {
height: fit-content !important;
transform: none !important;
position: relative !important;
margin-bottom: 10px;
}
}
</style>
While joyBinary's answer solves the problem, it also swallows all other errors which might not be the desired behaviour.
This approach solves this issue:
const originalPush = Router.prototype.push;
Router.prototype.push = function push(location) {
return originalPush.call(this, location).catch(err => {
if (err.name !== 'NavigationDuplicated') throw err
});
}
For those using Typescript, here's Errik Sven Puudist's answer converted:
const originalPush = Router.prototype.push;
Router.prototype.push = async function (location: RawLocation) {
let route: Route;
try {
route = await originalPush.call<Router, [RawLocation], Promise<Route>>(this, location);
} catch (err) {
if (err.name !== 'NavigationDuplicated') {
throw err;
}
}
return route!;
}
Use this code in router.js file:
const originalPush = VueRouter.prototype.push;
VueRouter.prototype.push = function push(location) {
return originalPush.call(this, location).catch(err => err);
}
This code can override catch's exceptions.

How to mount custom Vue component to markers for L.markerClusterGroup

I need to render custom markers with custom popups in Vue component.
I use L.markerClusterGroup and try to mount custom component for marker and popup, but it does not work.
map.js
<template>
<div id="map" class="map"></div>
</template>
<script>
import L from 'mapbox.js';
import 'mapbox.js/dist/mapbox.css';
import 'leaflet.markercluster';
import Vue from 'vue';
import Pin from './Pin';
import Popup from './Popup';
import config from '#/config';
const EnhancedPin = Vue.extend(Pin);
const EnhancedPopup = Vue.extend(Popup);
export default {
props: {
geojson: {
type: Object,
required: true,
},
},
mounted() {
this.initMap();
},
methods: {
initMap() {
this.createMap(this.geojson);
this.addClustersToMap(this.geojson);
},
createMap(geojson) {
L.mapbox.accessToken = config.mapBoxKey;
this.map = L.mapbox
.map('map', null, {
attributionControl: { compact: false },
zoomControl: true,
minZoom: 1,
})
.addLayer(L.mapbox.styleLayer('mapbox://styles/mapbox/streets-v11'));
.setView([0, 0], 2);
},
addClustersToMap(geojson) {
var clusterGroup = L.markerClusterGroup({
showCoverageOnHover: false,
iconCreateFunction: cluster => {
return L.divIcon({
className: 'cluster-icon',
iconSize: 40,
html: cluster.getChildCount(),
});
},
});
geojson.features.forEach(feature => {
var cssIcon = L.divIcon({
className: 'icon',
html: '<div class="icon-inner"></div>',
});
const popup = L.popup({ minWidth: 220 }).setContent('<div class="popup-inner"></div>');
const marker = L.marker([feature.geometry.coordinates[1], feature.geometry.coordinates[0]]);
marker.setIcon(cssIcon).bindPopup(popup);
clusterGroup.addLayer(marker);
new EnhancedPin({
propsData: {
item: feature,
},
}).$mount('.icon-inner');
marker.on('click', e => {
setTimeout(() => {
new EnhancedPopup({
propsData: {
item: feature,
},
}).$mount('.popup-inner');
});
});
});
this.map.addLayer(clusterGroup);
},
},
};
</script>
<style>
.cluster-icon {
display: flex;
align-items: center;
justify-content: center;
background: var(--primary);
border-radius: 50%;
box-shadow: 0 0 0 3px rgba(48, 72, 107, 0.3);
color: var(--white);
font-size: 1em;
font-weight: bold;
}
</style>
I have an error in console
[Vue warn]: Cannot find element: .icon-inner
because .icon-inner doesn't exist in DOM.
How can I use Vue component as a marker? It needs to work when zooming and moving the map.
The usual "trick" to use Vue components in a Leaflet map is simply to render them off-document and to pass the resulting $el HTMLElement for Leaflet to handle:
const myIconVueEl = new EnhancedPin(myPinData).$mount().$el; // no selector to mount into
const myIcon = L.divIcon({
html: myIconVueEl.outerHTML // directly pass an HTMLElement
});
const myPopupVueEl = new EnhancedPopup(myPopupData).$mount().$el;
const marker = L.marker(latLng, {
icon: myIcon
}).bindPopup(myPopupVueEl);

Vue.js can not set options afer setting data

I try to set an option for my Vue component after getting my required data through an API. The data is set correctly when the Vue instance is created but it seems that does not affect my condition.
This is the snippet:
import axios from 'axios';
Vue.component("order-now", {
delimiters: ["${", "}"],
props: {
dynamic: {
type: Boolean,
default: false
},
template: null
},
data() {
return {
order: '',
startInterval: false,
}
},
/**
* created
*/
created() {
this.getOrderNow();
this.$options.template = this.template;
},
mounted() {
if(this.startInterval)
this.$options.interval = setInterval(this.getOrderNow(), 10000);
},
/**
* beforeDestroy
*/
beforeDestroy() {
clearInterval(this.$options.interval);
},
methods: {
/**
* getOrderNow
*
* Receive data from api route
* and store it to components data
*/
getOrderNow() {
axios.get('/rest/order-now').then(({data}) => {
this.order = data.orderNow.order;
this.startInterval = data.orderNow.startInterval;
}).catch(e => {
console.error('Could not fetch data for order string.')
});
}
}
});
I call my getOrderNow() method when the created hook is called. This works fine and my data is set.
As you can see, in the mounted() hook, I try to look if setInterval is set true or false and condionally set an option but setInterval is always false.
I thought that might has been changed after calling my method in the created hook but it does not.
this.startInterval is false because it probably never gets set to true at the time mounted() is applied. The thing is that you set startInterval after the promise returned by axios is resolved, which most likely happens after mounted().
To solve this you can just set interval inside axios.then().
Update after reading a comment (working demo):
const API = {
counter: 0,
getItems() {
return new Promise((fulfill) => {
setTimeout(() => {
fulfill(API.counter++);
})
});
},
};
new Vue({
el: "#app",
data: {
interval: false,
data: '',
},
methods: {
fetchThings() {
API.getItems().then((data) => {
this.data = data;
});
},
},
created() {
this.fetchThings();
this.interval = setInterval(this.fetchThings, 1000);
},
});
body {
background: #20262E;
padding: 20px;
font-family: Helvetica;
}
#app {
background: #fff;
border-radius: 4px;
padding: 20px;
transition: all 0.2s;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.min.js"></script>
<div id="app">
<pre>
{{data}}
</pre>
</div>
And jsfiddle