Vue Pass parameters to dynamic page through a dynamic route - javascript

so I have a json file which contains a bunch of visual test results and I would like to have a column in the data table for each row which have a dynamic path for a vue page that showcases the details for each test.I've got most of it figured out except the dynamic routing and passing parameters to the dynamic page.Here's the datatable component that I created (I'm using vuetify and nuxt btw)
<template>
<div>
<v-text-field
v-model="search"
label="Search"
class="mx-4"
></v-text-field>
<v-data-table
:headers="headers"
:items="tests"
:items-per-page="15"
:search="search"
class="elevation-1"
>
<template v-slot:[`item.status`]="{ item }">
<v-chip
:color="getColor(item.status)"
dark
>
{{ item.status }}
</v-chip>
</template>
<template #[`item.details`]="{ item }">
<nuxt-link :to="`tests/${item.name}`">show details</nuxt-link>
</template>
</v-data-table>
</div>
</template>
<script>
import testtable from '../../VisualTests/test_07_03_2022_10_13_48/Tests_results.json';
export default {
data() {
return {
search: '',
tests: testtable,
headers: [
{
text: 'Test tag',
align: 'start',
sortable: true,
value: 'tag',
},
{ text: 'testname', value: 'name' },
{ text: 'status', value: 'status' },
{ text: 'details', value: 'details' },
]
}
},
methods: {
getColor (status) {
if (status=="failed") return 'red'
else if (status=="skipped/pending") return 'blue'
else return 'green'
}
}
}
</script>
<style lang="scss" scoped>
</style>
this is my nuxt router file :
import Vue from 'vue'
import Router from 'vue-router'
import { normalizeURL, decode } from 'ufo'
import { interopDefault } from './utils'
import scrollBehavior from './router.scrollBehavior.js'
const _05a6b87a = () => interopDefault(import('..\\pages\\tests\\_name\\index.vue' /* webpackChunkName: "pages/tests/_name/index" */))
const _f1328bfa = () => interopDefault(import('..\\pages\\index.vue' /* webpackChunkName: "pages/index" */))
const emptyFn = () => {}
Vue.use(Router)
export const routerOptions = {
mode: 'history',
base: '/',
linkActiveClass: 'nuxt-link-active',
linkExactActiveClass: 'nuxt-link-exact-active',
scrollBehavior,
routes: [{
path: "/tests/:name",
component: _05a6b87a,
name: "tests-name"
}, {
path: "/",
component: _f1328bfa,
name: "index"
}],
fallback: false
}
export function createRouter (ssrContext, config) {
const base = (config._app && config._app.basePath) || routerOptions.base
const router = new Router({ ...routerOptions, base })
// TODO: remove in Nuxt 3
const originalPush = router.push
router.push = function push (location, onComplete = emptyFn, onAbort) {
return originalPush.call(this, location, onComplete, onAbort)
}
const resolve = router.resolve.bind(router)
router.resolve = (to, current, append) => {
if (typeof to === 'string') {
to = normalizeURL(to)
}
return resolve(to, current, append)
}
return router
}
I want to pass the testname,tag and status to the dynamic page.
So far, I have only been able to pass 1 parameter(name).
my dynamic page is in a folder named tests inside pages with a nested '_name' folder that contains index.vue.
How can I pass all the parameters ?

Related

Vue 3 Get composable variables out of function without redefining refs

I'm working with a parent companyList component, a reusable Table component and an useFetch composable in vue 3.2
Without the Table component I had the following code:
companyList
<script setup>
import { computed } from 'vue';
import useFetch from '#/composables/useFetch';
import { formatEmail, formatPhone, formatEnterpriseNumber } from '#/utils/formatters';
const { response, isFetching, error } = useFetch('get', '/companies');
const companies = computed(() =>
response.value?.companies?.map((company) => ({
id: `#${company.id}`,
name: `${company.legal_entity_type} ${company.business_name}`,
enterprise_number: formatEnterpriseNumber(company.enterprise_number),
email: formatEmail(company.email),
phone: formatPhone(company.phone),
}))
);
</script>
In the Table component which contains pagination, sorting and a search, a watchEffect checks if the state has changed and triggers an emit from the parent component. In this case getCompanies. This looks like this:
companyList
<script setup>
const getCompanies = (search, sortKey, orderKey) => {
const { response, isFetching, error } = useFetch('get', '/companies', {
params: {
keyword: search,
sort_by: sortKey,
order_by: orderKey,
},
});
};
const companies = computed(() =>
response.value?.companies?.map((company) => ({
id: `#${company.id}`,
name: `${company.legal_entity_type} ${company.business_name}`,
enterprise_number: formatEnterpriseNumber(company.enterprise_number),
email: formatEmail(company.email),
phone: formatPhone(company.phone),
}))
);
</script>
<template>
<Spinner v-if="isFetching" size="medium" />
<ErrorMessage v-else-if="error" showReload :description="error" />
<NoDataMessage v-else-if="!companies || companies.length <= 0" />
<div v-else>
<Table :columns="tableColumns" :data="companies" #fetchData="getCompanies">
<template v-slot:id="{ item }">
<Badge>
{{ item.id }}
</Badge>
</template>
<template v-slot:actions="{ item }">
<router-link :to="{ name: 'clientDetails', params: { client_id: item.id } }" class="text-blue-500 lowercase"> {{ $tc('detail', 2) }} </router-link>
</template>
</Table>
</div>
</template>
Question: How can I get the response, isFetching and error out of the getCompanies function and use it inside the template tags? It feels like a waste of using a reusable system if I have to define refs to get them out. On top of that I can't use the same names. Is there another solution than this:
const local_response = ref(null);
const local_isFetching = ref(null);
const local_error = ref(null);
const getCompanies = (search, sortKey, orderKey) => {
const { response, isFetching, error } = useFetch('get', '/companies', {
params: {
keyword: search,
sort_by: sortKey,
order_by: orderKey,
},
});
local_response.value = response;
local_isFetching.value = isFetching;
local_error.value = error;
};
const companies = computed(() =>
local_response.value?.companies?.map((company) => ({
id: `#${company.id}`,
name: `${company.legal_entity_type} ${company.business_name}`,
enterprise_number: formatEnterpriseNumber(company.enterprise_number),
email: formatEmail(company.email),
phone: formatPhone(company.phone),
}))
);
useFetch provides an option to delay execution until you want to:
const { execute: getCompanies, response, isFetching, error } = useFetch('get',
'/companies', {
immediate: false, // defer execution until execute is called
params: {
keyword: search,
sort_by: sortKey,
order_by: orderKey,
},
});
// getCompanies, response, isFetching, and error will all be available to the template

Deleting an object in Vue, emitting from child to parent and to parent

I have created a simple to do app in VUE.
In order to delete a card (each card is an object with an id, title and description located in state in an App.vue component), I am passing and id from App as a prop to TaskList and to the button (delete) in the Task component. Then in order trigger a deleteTask function, again I am emmiting an id from Task to TaskList and then to App.
This approach works. However, is that kind of long chain of emmiting is considered as good practice? Is there a better way to do it?
App.vue
<template>
<div>
<TaskList :tasks="tasks" #id="deleteTask"/>
<Form :length="tasks.length" #addTask="addNewTask" />
</div>
</template>
<script>
import TaskList from './components/TaskList';
import Form from './components/Form';
export default {
name: 'App',
components: { TaskList, Form },
data() {
return {
tasks: [
{
id: 1,
title: 'Hello World',
description: 'this is the world'
},
{
id: 2,
title: 'Hello Mars',
description: 'this is the mars'
},
{
id: 3,
title: 'Hello Jupiter',
description: 'this is the jupiter'
}
]
}
},
methods: {
addNewTask(taskObject) {
const listOfTasks = this.tasks.concat(taskObject);
this.tasks = listOfTasks;
},
deleteTask(id) {
const filteredList = this.tasks.filter(element => {
return element.id != id;
})
this.tasks = filteredList;
}
}
}
</script>
TaskList.vue
<template>
<div class="taskList" v-bind:key="task" v-for="task in tasks">
<Task :title="task.title" :description="task.description" :id="task.id" #id="sendId"/>
</div>
</template>
<script>
import Task from './Task';
export default {
props: ['tasks'],
components: { Task },
methods: {
sendId(id) {
this.$emit('id', id);
console.log(id)
}
}
}
</script>
Task.vue
<template>
<div class="task">
<h1>{{ title }}</h1>
<p>{{ description }}</p>
<button #click="passId">Delete</button>
</div>
</template>
<script>
export default {
props: ['title', 'description', 'id'],
methods: {
passId() {
this.$emit('id', this.id);
}
}
}
</script>
One sure way of reducing this chain of data transfer is by using Vuex, But if you don't want to use that you can also use an "EventBus"
NOTE - Still you will have to pass the id from parent to child
Creating event bus
// src > eventBus.js
import Vue from 'vue'
export default new Vue()
Emit the event when the user clicks on the delete button
// Task.vue
<template>
<div class="task">
<h1>{{ title }}</h1>
<p>{{ description }}</p>
<button #click="passId">Delete</button>
</div>
</template>
<script>
import EventBus from 'path/to/eventBus'
export default {
props: ['title', 'description', 'id'],
methods: {
passId() {
EventBus.$emit('delete-task', this.id);
}
}
}
</script>
Listen to the event on the topmost parent
<template>
<div>
<TaskList :tasks="tasks" #id="deleteTask"/>
<Form :length="tasks.length" #addTask="addNewTask" />
</div>
</template>
<script>
import TaskList from './components/TaskList';
import Form from './components/Form';
import EventBus from 'path/to/eventBus.js'
export default {
name: 'App',
components: { TaskList, Form },
data() {
return {
tasks: [
{
id: 1,
title: 'Hello World',
description: 'this is the world'
},
{
id: 2,
title: 'Hello Mars',
description: 'this is the mars'
},
{
id: 3,
title: 'Hello Jupiter',
description: 'this is the jupiter'
}
]
}
},
mounted(){
// Listening to the delete-task event
EventBus.$on('delete-task', (id) => {
this.deleteTask(id)
})
},
methods: {
addNewTask(taskObject) {
const listOfTasks = this.tasks.concat(taskObject);
this.tasks = listOfTasks;
},
deleteTask(id) {
const filteredList = this.tasks.filter(element => {
return element.id != id;
})
this.tasks = filteredList;
}
}
}
</script>

Can't access the object from the props inside created method

I got an object from API resource and put it inside the property, then children component can't access the props's object inside created method to assign it values inside my data properties as arrays and strings
when i try to console the props from child component i found my items object inside it
"This is my parent component"
<template>
<v-container grid-list-xl fill-height>
<v-layout row wrap>
<v-flex xs6 offset-xs3>
<message-box :items="source" v-if="source.length > 0"></message-box>
</v-flex>
</v-layout>
</v-container>
</template>
<script>
<script>
import MessageBox from './MessageBox'
export default {
components:{MessageBox},
data() {
return {
room_id: 1,
source: {},
};
},
created(){
var app = this;
axios.get(`/api/room/${app.room_id}/message`)
.then(res => app.source = res.data.data);
}
};
</script>
</script>
"This is my child component"
<template>
<div>
<beautiful-chat
:participants="participants"
:titleImageUrl="titleImageUrl"
:onMessageWasSent="onMessageWasSent"
:messageList="messageList.messageList"
:newMessagesCount="newMessagesCount"
:isOpen="isChatOpen"
:close="closeChat"
:icons="icons"
:open="openChat"
:showEmoji="true"
:showFile="true"
:showTypingIndicator="showTypingIndicator"
:colors="colors"
:alwaysScrollToBottom="alwaysScrollToBottom"
:messageStyling="messageStyling"
#onType="handleOnType"
#edit="editMessage"
/>
</div>
</template>
<script>
import CloseIcon from "vue-beautiful-chat/src/assets/close-icon.png";
import OpenIcon from "vue-beautiful-chat/src/assets/logo-no-bg.svg";
import FileIcon from "vue-beautiful-chat/src/assets/file.svg";
import CloseIconSvg from "vue-beautiful-chat/src/assets/close.svg";
export default {
props: ['items'],
data() {
return {
room_id: 1,
participants:[],
messageList: [],
limit: 7,
busy: false,
auth_uid: User.id(),
titleImageUrl:
"https://a.slack-edge.com/66f9/img/avatars-teams/ava_0001-34.png",
newMessagesCount: 0,
isChatOpen: false,
alwaysScrollToBottom: false, // when set to true always scrolls the chat to the bottom when new events are in (new message, user starts typing...)
messageStyling: true,
showTypingIndicator: "",
icons: {
open: {
img: OpenIcon,
name: "default"
},
close: {
img: CloseIcon,
name: "default"
},
file: {
img: FileIcon,
name: "default"
},
closeSvg: {
img: CloseIconSvg,
name: "default"
}
},
colors: {
header: {
bg: "#4e8cff",
text: "#ffffff"
},
launcher: {
bg: "#4e8cff"
},
messageList: {
bg: "#ffffff"
},
sentMessage: {
bg: "#4e8cff",
text: "#ffffff"
},
receivedMessage: {
bg: "#eaeaea",
text: "#222222"
},
userInput: {
bg: "#f4f7f9",
text: "#565867"
}
}
};
},
methods: {
sendMessage(text) {
if (text.length > 0) {
this.newMessagesCount = this.isChatOpen
? this.newMessagesCount
: this.newMessagesCount + 1;
this.onMessageWasSent({
author: "support",
type: "text",
data: { text }
});
axios
.post(`/api/room/${this.room_id}/message`, { body: text })
.then(res => console.log("message sent"));
}
},
onMessageWasSent(message) {
// called when the user sends a message
this.messageList = [...this.messageList, message];
},
openChat() {
// called when the user clicks on the fab button to open the chat
this.isChatOpen = true;
this.newMessagesCount = 0;
},
closeChat() {
// called when the user clicks on the botton to close the chat
this.isChatOpen = false;
},
handleScrollToTop() {
// called when the user scrolls message list to top
// leverage pagination for loading another page of messages
},
handleOnType() {
console.log("Emit typing event");
},
editMessage(message) {
const m = this.messageList.find(m => m.id === message.id);
m.isEdited = true;
m.data.text = message.data.text;
},
},
created(){
// console.log(this.$props.items)
Array.prototype.forEach.call(this.$props.items, child => {
this.participants = child.participants;
// console.log(this.participants)
this.messageList = child.body;
// console.log(this.messageList)
});
},
computed:{
itemarr(){
return this.$props.items
}
}
};
</script>
"The console error is TypeError: Array.prototype.forEach called on null or undefined"
"The output of my items object is {__ob__: Observer}"
You can use v-if to solve your problem. You need to wait for ajax response to render child component
<template>
<v-container grid-list-xl fill-height>
<v-layout row wrap>
<v-flex xs6 offset-xs3>
<message-box v-if="sourceLength > 0" :items="source"></message-box>
</v-flex>
</v-layout>
</v-container>
</template>
<script>
<script>
import MessageBox from './MessageBox'
export default {
components:{MessageBox},
data() {
return {
room_id: 1,
source: {},
};
},
created(){
var app = this;
axios.get(`/api/room/${app.room_id}/message`)
.then(res => app.source = res.data.data);
},
get sourceLength() {
return Object.keys(this.source).length;
}
};
</script>
</script>

VueJS How to pass props between two components using <router-view>

I'm new to vue.js and I'm tryeing to build a little application, where I in one case need to pass a prop between two components. For some reason it does not work and I don't know why.
Here is the first component, the Playlist.Vue component:
<template>
<div class="playlists-con">
<div class="playlists">
<h1>Available Playlists for category: {{id}}</h1>
<ul>
<router-link v-for="playlist in playlists" :to="`details/${playlist.id}`" tag="li" active-class="active" exact>
<div class="playlist-wrapper">
<div class="imgWrap">
<img :src="playlist.images[0].url" />
</div>
<a>{{playlist.name}}</a>
</div>
</router-link>
</ul>
</div>
<div>
<router-view category="id"></router-view>
</div>
</div>
</template>
<script>
export default {
data() {
return {
id: this.$route.params.id,
playlists : []
}
},
watch: {
'$route'(to, from) {
this.id = to.params.id
}
},
methods: {
fetchPlaylist() {
this.$http.get('' + this.id + '/playlists')
.then(response => {
return response.json()
})
.then(data => {
const playlist_items = data.playlists.items;
for (let key in playlist_items) {
this.playlists.push(playlist_items[key])
}
})
}
},
created() {
this.fetchPlaylist();
}
}
</script>
from the Playlist component, I'm supposed to be able to get to the Playlist details. I also want to pass the category prop to the PlaylistDetails.vue, so I tried to do <router-view category="id"></router-view> - but that does not work.
PlaylistDetails.vue component (where I want to display the category prop, passed from the Playlist.vue component) :
<template>
<div class="playlist-details">
<router-link :to="`/categories/${category}`">Go to playlists</router-link>
<h1>Playlist Details for Playlist: <span class="playlist-name">{{playlistName}}</span></h1>
<h1>category: {{ category }}</h1>
<ul>
<li v-for="track in tracks">
<p>{{ track.track.artists[0].name}} - {{ track.track.name }}</p>
</li>
</ul>
</div>
</template>
<script>
export default {
props: ['category'],
data() {
return {
id: this.$route.params.id,
tracks : [],
playlistName: ''
}
},
watch: {
'$route'(to, from) {
this.path = from.params.path
}
},
beforeRouteEnter(to, from, next) {
if (true) {
next();
} else {
next(false);
}
},
methods: {
fetchPlaylistDetails() {
this.$http.get('https://api.spotify.com/v1/users/spotify/playlists/' + this.id)
.then(response => {
return response.json()
})
.then(data => {
const playlist_tracks = data.tracks.items;
for (let key in playlist_tracks) {
this.tracks.push(playlist_tracks[key])
}
this.playlistName = data.name;
})
}
},
created() {
this.fetchPlaylistDetails();
}
}
</script>
What am I doing wrong?
UPDATE
Here is my router configuration:
export const routes = [
{
path: '', default: App
},
{
path: '/categories/:id/playlists', props: true, component: Playlists
},
{
path: '/categories/:id/details/:id', component: PlaylistDetails, props: true, beforeEnter: (to, from, next) => {
next();
}},
{path: '*', redirect: '/'}
]
You are half way there, you defined props:true on the route, which means every dynamic property that is matched in the url would be passed as a prop, so :
//this will pass 'id' as a prop to the playlist component
{
path: '/categories/:id/playlists', props: true, component: Playlists
},
So inside the playlist component you'll have this:
props: ['id'],
data() {
return {
playlists : []
}
},
The same is true for the details component:
//change the name of the param to distinguish it from the category id
{
path: '/categories/:id/details/:detailsId', component: PlaylistDetails, props: true, beforeEnter: (to, from, next) => {
next();
}},
And in PlaylistDetails.vue:
props: ['detailsId'],
....
methods: {
fetchPlaylistDetails() {
this.$http.get('https://api.spotify.com/v1/users/spotify/playlists/' + this.detailsId)
.then(response => {
return response.json()
})
.then(data => {
const playlist_tracks = data.tracks.items;
for (let key in playlist_tracks) {
this.tracks.push(playlist_tracks[key])
}
this.playlistName = data.name;
})
}
},
There're 2 ways to pass data between non-parent components. I would recommend to take a look at them, before trying solve issue with router-view:
Using Vue.bus
Using Vuex

Vue.js replace hashtags in text with <router-link>

I receive some text from server which may contain some hashtags and when displaying this text I would like to convert these tags with links.
Example text is: "Today #weather is very nice"
Following code converts the string to
Today <router-link to="/tag/weather">#weather</router-link> is very nice
but it is not rendered again to <a> tag.
<template>
<p v-html="content"/>
</template>
<script>
export default {
methods: {
convertHashTags: function(str) {
return str.replace(/#([\w]+)/g,'<router-link to="/tag/$1">#$1</router-link>')
}
},
data() {
return{
content: 'Today #weather is very nice'
};
}
</script>
How can I re-render content?
I tried https://codepen.io/movii/pen/WORoyg this approach but it expects whole string to be a single link not some text and some links.
Your code seems to fit ok into the dynamic-link component.
console.clear()
const Foo = { template: '<div>foo</div>' }
const Bar = { template: '<div>bar</div>' }
const Weather = { template: '<div>weather</div>' }
const routes = [
{ path: '/foo', component: Foo },
{ path: '/bar', component: Bar },
{ path: '/tag/weather', component: Weather },
]
Vue.component('dynamic-link', {
template: '<component v-bind:is="transformed"></component>',
props: ['text'],
methods: {
convertHashTags: function(str) {
const spanned = `<span>${str}</span>`
return spanned.replace(/#([\w]+)/g,'<router-link to="/tag/$1">#$1</router-link>')
}
},
computed: {
transformed () {
const template = this.convertHashTags(this.text);
return {
template: template,
props: this.$options.props
}
}
}
})
const router = new VueRouter({ routes })
const app = new Vue({
router,
data () {
return {
text: 'Today #weather is very nice'
}
}
}).$mount('#app');
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.1.10/vue.min.js"></script>
<script src="https://unpkg.com/vue-router/dist/vue-router.js"></script>
<div id="app">
<router-link to="/bar">Go to Bar</router-link> |
<dynamic-link :text="text"></dynamic-link>
<router-view></router-view>
</div>

Categories

Resources