In a vue app, I want to use a variable defined from a param, passed through the URL, in the template section. But when I try to use it, I get an undefined error when used in the template (though it is used in the script section and shows as defined).
I thought returning the variable would let me use it in the template section, but I get a duplicate key warning.
<template>
<div>
<router-link
:to="{ path: `/${m}`}" //this shows as undefined
>Link</router-link>
</div>
</template>
<script>
export default {
name: "template",
props: ["m"],
setup() {
const route = useRoute();
const m = ref(route.params.m);
console.log(m.value) //this works
return {
m //This shows as a duplicate key
};
},
};
</script>
Change variable name m in setup().
Related
I am trying to automatically add UTM tracking parameters to all nuxt links, so I made a component for this (let me know if there's a better way!).
<template>
<nuxt-link :to="url" :target="$attrs.target"><slot></slot></nuxt-link>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
name: 'ArticleLink',
props: {
to: {
type: String,
required: true,
},
date: {
type: Number,
required: true,
},
},
computed: {
url(): string {
const url = this.to.startsWith('/')
? `${process.env.VUE_APP_ORIGIN}${this.to}`
: this.to;
const urlObject = new URL(url);
urlObject.searchParams.append('utm_source', 'article');
urlObject.searchParams.append('date', this.date);
console.log(this.to, urlObject.href);
return urlObject.href;
},
},
});
</script>
The problem is when I use it on relative links, the to property for some reason combines the initial href, with the calculated one:
<article-link date="20221109" to="/">link</article-link>
this is rendered as:
http://localhost:3000/current/path/http://localhost:3000/?utm_source=article&date-20221109`
What's strange is the console.log shows the correct conversion from / to http://localhost:3000/?utm_source=article&date=20221109
Also strange is if I use Vue devtools to inspect the <NuxtLink> it shows a correct to prop.
Seems I'm using vue router links incorrectly (see: https://github.com/vuejs/vue-router/issues/1131)
When I pass an absolute URL to the :to prop, it adds it to the end of the current URL. So I have to return urlObject.pathname + urlObject.search instead of returning urlObject.href
:shrug:
I want to pass data to another page and I use the following code:
this.$router.push({ path: '/guard/foreigner-list', params: data});
Then I expect item is equal to data, but item is null
let item = this.$route.params;
You did not posted the entire code that is related to the process of changing route. But according to Vue Router documentation:
params are ignored if a path is provided, which is not the case for query, as shown in the examples. Instead, you need to provide the name of the route or manually specify the whole path with any parameter
So if you have defined a route called user in your router.js file like below:
import User from "../views/User"
const routes = [
{
path: '/user/:id',
name: 'User',
component: User
}
]
Then you can navigate programmatically from Home.vue to User.vue with the codes below:
Home.vue:
<template>
<div class="home">
<button #click="navigFunc">click to navigate</button>
</div>
</template>
<script>
export default {
name: 'Home',
methods: {
navigFunc: function () {
const id = '123';
// using "name" not "path"
this.$router.push({ name: 'User', params: { id } });
}
}
}
</script>
User.vue:
<template>
<section>
<h1>User page</h1>
</section>
</template>
<script>
export default {
name: "User",
mounted() {
/* access the params like this */
console.log(this.$route.params)
}
}
</script>
<style scoped>
</style>
Notice that the variable I defined (id), is the same as the params that was defined in router.js (path: '/user/:id').
Start from vue-router#4.1.4 (2022-08-22) passing object through params is no longer a viable option, since it is consider as anti-pattern.
However,
there are multiple alternatives to this anti-pattern:
Putting the data in a store like pinia: this is relevant if the data is used across multiple pages
Move the data to an actual param by defining it on the route's path or pass it as query params: this is relevant if you have small pieces of data that can fit in the URL and should be preserved when reloading the page
Pass the data as state to save it to the History API state: ...
Pass it as a new property to to.meta during navigation guards: ...
I am using following snippet to open a link in default browser.
<template>
<div>
<a #click.prevent="fireUpLink">External Link</a>
</div>
</template>
.
<script>
/* global nw */
export default {
methods: {
fireUpLink: function() {
nw.Shell.openExternal("http://example.com/");
}
}
};
</script>
But lets say if I have thousands of links, this solution is not scalable. Is there any better way?
In a Vue SFC, it expects a referenced variable to be defined or imported in the component, or be global. If you reference it from the global window object, it should work.
window.nw.Shell.openExternal('http://example.com');
For Vue, as shown by Max, <a #click.prevent="window.nw.Shell.openExternal('http://example.com')">Link</a> works.
You could also just create a component:
<template>
<a
:href="url"
class="link"
#click.prevent="openExternal"
><slot></slot></a>
</template>
<script>
export default {
name: 'ExternalLink',
props: {
url: {
type: String,
required: true
}
},
methods: {
openExternal: function () {
window.nw.Shell.openExternal(this.url);
}
}
};
</script>
Then just reference it like this:
<external-link url="http://example.com">Link</external-link>
Alternatively you could create a mixin that has the openExternal method in it, and globally install it across all components, so you can just do <a #click.prevent="openExternal('http://example.com')>
If you are using something other than Vue, which does not use a Virtual DOM, then you could just add a class="external-link" then target all elements on the page with that class and handle them.
$('.external-link').click(function (evt) {
// Prevent the link from loading in NW.js
evt.preventDefault();
// Get the `href` URL for the current link
let url = $(this).attr('href');
// Launch the user's default browser and load the URL for the link they clicked
window.nw.Shell.openExternal(url);
});
I'm a beginner in VUE and donnow this one is the correct syntax. I need the variable {{name}} to be set from a page. Which means I need to change the value of the variable page to page. How can I achieve that? Help me guys.
My "Layout" Code is like below -
<template>
<div class="login-page">
<div class="col1">{{ name }}</div>
<div class="col2">
<div class="content-box">
<nuxt />
</div>
</div>
</div>
</template>
<script>
export default {
props: ['name']
}
</script>
And my "Page" code is following -
<template>
<div>Welcome</div>
</template>
<script>
export default {
layout: 'login',
data: function() {
return {
name: 'Victor'
}
}
}
</script>
this can be achieved by using the vuex module. The layout have access to the vuex store, so once a page is open, you can call a mutation to set the page name and listen the name state in the layout component.
First the Vuex module, we can add a module by creating a file in the store folder,
in this case we are creating the page module:
// page.js file in the store folder
const state = {
name: ''
}
const mutations = {
setName(state, name) {
state.name = name
}
}
const getters = {
getName: (state) => state.name
}
export default {
state,
mutations,
getters
}
Now we can use the setPageName mutation to set the pageName value once a page reach the created hook (also can be the mounted hook):
// Page.vue page
<template>
<div>Welcome</div>
</template>
<script>
export default {
layout: 'login',
created() {
this.$store.commit('page/setName', 'Hello')
},
}
</script>
And in the layout component we have the computed property pageName (or name if we want):
<template>
<div class="login-page">
<div class="col1">{{ name }}</div>
<div class="col2">
<div class="content-box">
<nuxt />
</div>
</div>
</div>
</template>
<script>
export default {
computed: {
name() {
return this.$store.getters['page/getName']
}
}
}
</script>
And it's done!
Answer to your question in the commets:
The idea behind modules is keep the related information to some functionality in one place. I.e Let's say you want to have name, title and subtitle for each page, so the page module state variable will be:
const state = { name: '', title: '', subtitle: ''}
Each variable can be updated with a mutation, declaring:
const mutations = {
setName(state, name) {
state.name = name
},
setPageTitle(state, title) {
state.title = title
},
setPageSubtitle(state, subtitle) {
state.subtitle = subtitle
},
}
And their values can be updated from any page with:
this.$store.commit('page/setPageTitle', 'A page title')
The same if you want to read the value:
computed: {
title() {
// you can get the variable state without a getter
// ['page'] is the module name, nuxt create the module name
// using the file name page.js
return this.$store.state['page'].title
}
}
The getters are good for format or filter information.
A new module can be added anytime if required, the idea behind vuex and the modules is to have a place with the information that is required in many places through the application, in one place. I.e. the application theme information, if the user select the light or dark theme, maybe the colors can be changed. You can read more about vuex with nuxt here: https://nuxtjs.org/guide/vuex-store/ and https://vuex.vuejs.org/
I have a little Loading component, whose default text I want to be 'Loading...'. Good candidate for slots, so I have something like this as my template:
<p class="loading"><i class="fa fa-spinner fa-spin"></i><slot>Loading...</slot></p>
That allows me to change the loading message with e.g. <loading>Searching...</loading>. The behaviour I would like, though, is not just to display the default message if no slot content is supplied, but also if the slot content is null or blank. At the moment if I do e.g.<loading>{{loadingMessage}}</loading> and loadingMessage is null, no text is displayed (where I want the default text to be displayed). So ideally I need to test this.$slots.default. This tells me whether content was passed in, but how do I find whether or not it was empty? this.$slots.default.text returns undefined.
You'd need a computed property which checks for this.$slots. With a default slot you'd check this.$slots.default, and with a named slot just replace default with the slot name.
computed: {
slotPassed() {
return !!this.$slots.default[0].text.length
}
}
And then use it in your template:
<template>
<div>
<slot v-if="slotPassed">Loading...</slot>
<p v-else>Searching...</p>
</div>
</template>
You can see a small example here. Notice how fallback content is displayed and not "default content", which is inside the slot.
Edit:
My wording could've been better. What you need to do is check for $slots.X value, but computed property is a way to check that. You could also just write the slot check in your template:
<template>
<div>
<slot v-if="!!$slots.default[0].text">Loading...</slot>
<p v-else>Searching...</p>
</div>
</template>
Edit 2: As pointed out by #GoogleMac in the comments, checking for a slot's text property fails for renderless components (e.g. <transition>, <keep-alive>, ...), so the check they suggested is:
!!this.$slots.default && !!this.$slots.default[0]
// or..
!!(this.$slots.default || [])[0]
#kano's answer works well, but there's a gotcha: this.$slots isn't reactive, so if it starts out being false, and then becomes true, any computed property won't update.
The solution is to not rely on a computed value but instead on created and beforeUpdated (as #MathewSonke points out):
export default {
name: "YourComponentWithDynamicSlot",
data() {
return {
showFooter: false,
showHeader: false,
};
},
created() {
this.setShowSlots();
},
beforeUpdate() {
this.setShowSlots();
},
methods: {
setShowSlots() {
this.showFooter = this.$slots.footer?.[0];
this.showHeader = this.$slots.header?.[0];
},
},
};
UPDATE: Vue 3 (Composition API)
For Vue 3, it seems that the way to check whether a slot has content has changed (using the new composition API):
import { computed, defineComponent } from "vue";
export default defineComponent({
setup(_, { slots }) {
const showHeader = computed(() => !!slots.header);
return {
showHeader,
};
},
});
note: I can't find any documentation on this, so take it with a pinch of salt, but seems to work in my very limited testing.
this.$slots can be checked to see if a slot has been used.
It is important to note that this.$slots is not reactive. This could cause problems when using this.$slots in a computed value.
https://v2.vuejs.org/v2/api/?redirect=true#:~:text=Please%20note%20that%20slots%20are%20not%20reactive.
This means we need to ensure that this.slots is checked whenever the component re-renders. We can do this simply by using a method instead of a computed property.
https://v2.vuejs.org/v2/guide/computed.html?redirect=true#:~:text=In%20comparison%2C%20a%20method%20invocation%20will%20always%20run%20the%20function%20whenever%20a%20re%2Drender%20happens
<template>
<div>
<slot v-if="hasHeading" name="heading"/>
</div>
</template>
<script>
export default{
name: "some component",
methods: {
hasHeading(){ return !!this.slots.heading}
}
}
</script>