How to make a recursive menu using Quasar QexpansionItem - vue.js

I want create a component that it can to scale with a nested object structure using the QExpansionItem from Quasar Framework.
I made a recursive component to try achieve this but doesn't shows like i hope. The items are repeated in a wrong way and I don't know why.
I am using Quasar V1.0.5, the component that i used QexpansionItem
Here the menu object
[
{
name: '1',
icon: 'settings',
permission: 'configuration',
description: '1',
url: '',
children: [
{
name: '1.1',
permission: 'configuration',
url: '/insuranceTypes',
icon: 'add',
description: '1.1'
},
{
name: '1.2',
permission: 'configuration',
url: '/insuranceTypes2',
icon: 'phone',
description: '1.2'
}
]
}, {
name: '2',
icon: 'person',
permission: 'configuration',
url: 'contacts',
description: '2'
}
]
MenuComponent.vue where i call side-tree-menu component
<q-list
bordered
class="rounded-borders q-pt-md"
>
<side-tree-menu :menu="menu"></side-tree-menu>
</q-list>
SideTreeMenuComponent.vue
<template>
<div>
<q-expansion-item
expand-separator
:icon="item.icon"
:label="item.name"
:caption="item.description"
header-class="text-primary"
:key="item.name"
:to="item.url"
v-for="(item) in menu"
>
<template>
<side
v-for="(subitem) in item.children"
:key="subitem.name"
:menu="item.children"
>
</side>
</template>
</q-expansion-item>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
name: 'side',
props: ['menu', 'children'],
data () {
return {
isOpen: false,
algo: 0
}
},
mounted () {
console.log('menu', this.menu)
},
computed: {
...mapGetters('generals', ['can'])
}
}
</script>
The elements 1.1 and 1.2 are repeated and I don't know fix it

I got stuck at the same problem and did not find any solution online. I managed to get it working with the below approach. This could be helpful for someone in the future :)
I am adding here the 2 most important code files that will get this working. Rest of my setup is nothing more than what is created by the quasar create [project-name] CLI command.
When you create the project with the above command, you get the MainLayout.vue and EssentialLink.vue file. I have modified those to achieve the required result.
**My MainLayout.vue file - the template **
EssentialLink below is the component that renders the menu recursively using q-expansion-item inside the drawer on the main layout page.
<template>
<q-layout view="hHh Lpr lFf">
<q-header elevated>
<q-toolbar>
<q-btn flat dense round icon="menu" aria-label="Menu"
#click="leftDrawerOpen = !leftDrawerOpen" />
<q-toolbar-title>
{{appTitle}}
</q-toolbar-title>
<div>Release {{ appVersion }}</div>
</q-toolbar>
</q-header>
<q-drawer
v-model="leftDrawerOpen" show-if-above bordered
content-class="bg-grey-1">
<q-list>
<q-item-label
header
class="text-grey-8">
Essential Links
</q-item-label>
<EssentialLink
v-for="link in essentialLinks"
:key="link.title"
v-bind="link">
</EssentialLink>
</q-list>
</q-drawer>
<q-page-container>
<router-view />
</q-page-container>
</q-layout>
</template>
script section of MainLayout.vue file. Key properties to note - children and level.
<script>
import EssentialLink from 'components/EssentialLink.vue'
export default {
name: 'MainLayout',
components: {
EssentialLink
},
data () {
return {
appTitle: 'Project Name',appVersion: 'v0.1',leftDrawerOpen: false,
essentialLinks: [
{
title: 'Search', caption: 'quasar.dev', icon: 'school',
link: 'https://quasar.dev',
level: 0,
children: [{
title: 'Documents', caption: 'quasar.dev',icon: 'school',
link: 'https://quasar.dev',
level: 1,
children: [{
title: 'Search (level 3)',
caption: 'quasar.dev',
icon: 'school',
link: 'https://quasar.dev',
level: 2,
children: []
}]
}]
},
{
title: 'Github',caption: 'github.com/quasarframework',
icon: 'code',link: 'https://github.com/quasarframework',
level: 0,
children: [{
title: 'Github Level 2',caption: 'quasar.dev',icon: 'school',
link: 'https://quasar.dev',level: 1,
children: []
}]
},
{
title: 'Forum',caption: 'forum.quasar.dev',
icon: 'record_voice_over',link: 'https://forum.quasar.dev',
level: 0,
children: [{
title: 'Forum Level 2',caption: 'quasar.dev',icon: 'school',
link: 'https://quasar.dev',
level: 1,
children: []
}]
}
]
}
}
}
</script>
Finally the EssentialLink.vue component
The code below recursively calls itself when it encounters more than 1 item in its children property. The level property is used to indent the menus as you drill down.
<template>
<div>
<div v-if="children.length == 0">
<q-item clickable v-ripple :inset-level="level">
<q-item-section>{{title}}</q-item-section>
</q-item>
</div>
<div v-else>
<div v-if="children.length > 0">
<!-- {{children}} -->
<q-expansion-item
expand-separator
icon="mail"
:label="title"
:caption="caption"
:header-inset-level="level"
default-closed>
<EssentialLink
v-for="child in children"
:key="child"
v-bind="child">
</EssentialLink>
</q-expansion-item>
</div>
<div v-else>
<q-item clickable v-ripple :inset-level="level">
<q-item-section>{{title}}</q-item-section>
</q-item>
</div>
</div>
</div>
</template>
*script section of the EssentialLink.vue component
<script>
export default {
name: 'EssentialLink',
props: {
title: {
type: String,
required: true
},
caption: {
type: String,
default: ''
},
link: {
type: String,
default: '#'
},
icon: {
type: String,
default: ''
},
level: {
type: String,
default: ''
},
children: []
}
}
</script>
Final output looks like this (image)

Related

Children of NuxtLink rendered twice (hydration error?)

My hunch is that there is some hydration mismatch where the FontAwesomeIcon was not rendered on the server (only the span) and then on the client both child nodes of the NuxtLink were rendered (the svg and the span), prompting Nuxt to render the span twice.
The console does not return an error, though.
Any thoughts on how to debug this?
This is the Vue component:
<template>
<ul v-if="routes.length > 0" class="col-span-2 flex flex-col">
<li v-for="(item, i) in routes" :key="item.name">
<NuxtLink :to="item.path" target="_blank">
<FontAwesomeIcon :icon="item.icon" class="mr-3" fixed-width />
<span>{{ item.title }}</span>
</NuxtLink>
</li>
</ul>
</template>
<script lang="ts">
export default defineComponent({
props: {
links: {
type: Array,
default: () => ["instagram", "facebook", "email"],
},
},
computed: {
routes() {
return [
{
name: "instagram",
path: "https://www.instagram.com/insta.name/",
title: "Instagram",
icon: ["fab", "instagram"],
},
{
name: "facebook",
path: "https://www.facebook.com/fb.name",
title: "Facebook",
icon: ["fab", "facebook"],
},
{
name: "email",
path: "mailto:hello#example.com",
title: "Email",
icon: ["fas", "envelope"],
},
].filter((e) => this.links.includes(e.name));
},
},
});
</script>

Quasar: How to give props in q-table to parent component?

I have created TableTop5 component using q-table, so that I can reuse it in VisitorSummary.vue, but I have no clue how to get props which should be written in v-slot:header and v-slot:body.
VisitorSummary.vue
...
<div class="row card-wrap row-sec">
<table-top-5 title="검색어 TOP5" titleWord="검색어 TOP5"
:data="data" :columns="columns" :rowFirst="rank" // <-- it gives me errors..
:rowSec="keyword" :rowThird="visitor" :props="props" // <-- i can feel it isn't right..
>
</table-top-5>
</div>
...
<script lang="ts">
import Vue from 'vue';
import axios from 'axios';
import VueApexCharts from 'vue-apexcharts'
import NumberCount from 'components/card/NumberCount.vue'
import NumberCountSet from 'components/card/NumberCountSet.vue';
import TitleWithTooltip from 'components/card/TitleWithTooltip.vue'
import TableTop5 from 'components/table/TableTop5'
Vue.use(VueApexCharts)
Vue.component('apexchart', VueApexCharts)
export default Vue.extend({
components: {
NumberCountSet,
NumberCount,
TitleWithTooltip,
TableTop5,
},
data() {
return {
columns: [
{
name: 'rank',
required: true,
label: '순위',
align: 'center',
field: row => row.rank,
format: val => `${val}`,
sortable: true,
style: 'width: 10%;'
},
{ name: 'keyword', align: 'left', label: '검색어', field: 'keyword', sortable: true,
style: 'width: 70%; background:RGBA(0,178,45,0.05) ' },
{ name: 'visitor', align: 'center', label: '방문자수', field: 'visitor', sortable: true, style: 'width: 20%' },
],
data: [
{
rank: 1,
keyword: 'slimplanet',
visitor: 25,
},
{
rank: 2,
keyword: 'test',
visitor: 58,
},
{
rank: 3,
keyword: 'test',
visitor: 64,
},
{
rank: 4,
keyword: 'Slimplanet',
visitor: 72,
},
{
rank: 5,
keyword: 'test',
visitor: 18,
},
],
};
},
});
</script>
TableTop5.vue
<template>
<q-card class="card-item">
<q-card-section class="q-pa-none">
<div>
<q-table
:data="data"
:columns="columns"
row-key="name"
class="no-shadow q-pb-md q-px-md"
:hide-pagination="true"
>
<template v-slot:header="props">
<q-tr :props="props">
<q-th
v-for="col in props.cols"
:key="col.name"
:props="props"
style="font-weight: bold"
>
{{ col.label }}
</q-th>
</q-tr>
</template>
<template v-slot:body="props">
<q-tr :props="props">
<q-td key="rowFirst" :props="props">
{{ rowFirst }}
</q-td>
<q-td key="rowSec" :props="props">
{{ props.row.rowSec }}
</q-td>
<q-td key="rowThird" :props="props">
<q-badge color="black" outline>
{{ props.row.rowThird }}
</q-badge>
</q-td>
</q-tr>
</template>
</q-table>
</div>
</q-card-section>
</q-card>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
name: 'TableTop5',
props: {
title: {
type: String
},
titleWord: {
type: String
},
data: {
type: Array
},
columns: {
type: Array
},
rowFirst: {
type: String
},
rowSec: {
type: String
},
rowThird: {
type: String
},
props: {
type: Object
}
}
});
</script>
Or should i just not reuse and write q-table whenever I need it in a row?
This can be done in multiple ways. You can define it like this where the parent component takes care of rendering rows and columns. You can also define some columns that every table needs to have and have a slot for extra columns.
You could also iterate over the columns so you render them dynamically.
Either way, the advantage to this approach, is consistency and reusability, in case you have some complex logic inside the table for filtering and other stuff.
TableTop5
<template>
<q-table :data="data" :columns="columns">
<template #body="props">
<slot v-bind="props" name="columns"></slot>
</template>
</q-table>
</template>
<script>
export default {
name: 'TableTop5',
props: {
data: {
type: Array,
},
columns: {
type: Array,
},
},
};
</script>
VisitorSummary
<template>
<div>
...
<table-top5 :data="data" :columns="columns">
<template #columns="props">
<q-tr>
<q-td key="c1">
{{ props.row.c1 }}
</q-td>
<q-td key="c2">
{{ props.row.c2 }}
</q-td>
</q-tr>
</template>
</table-top5>
...
</div>
</template>
<script>
import TableTop5 from '#/TableTop5.vue';
export default {
name: 'VisitorSummary',
components: {
TableTop5,
},
data() {
return {
columns: [
{
name: 'c1',
label: 'Col 1',
field: 'c1',
align: 'left',
},
{
name: 'c1',
label: 'Col 2',
field: 'c2',
align: 'left',
},
],
data: [
{
c1: 'C1 data - row 1',
c2: 'C2 data - row 1',
},
{
c1: 'C1 data - row 2',
c2: 'C2 data - row 2',
},
],
};
},
};
</script>

Vue.js v3 passing data through router link

I have an object in Portfolio.vue like this:
data() {
return {
portfolio: {
front: [
{ description: '1. project ', src: require("../assets/sample.jpg"), slug: 'first'},
{ description: '2. project', src: require("../assets/sample.jpg"), slug: 'second' },
{ description: '3. project', src: require("../assets/sample.jpg"), slug: 'third' },
]
}
}
}
Portfolio.vue:
<div class="">
<div v-for="(data,index) in portfolio.front" :key="index">
<router-link :to="'/portfolio/'+data.slug"> <div class="element" :data-description="data.description">
<img :src="data.src " alt="">
</div>
</router-link>
</div>
</div>
PortfolioProduct.vue
<template>
<div>
<p>I want to take data to here. In here, i have to reach the data like this: portfolio.front.description</p>
</div>
</template>
<script>
export default {
props: {
},
}
</script>
My routes:
const routes = [
{
path: "/portfolio",
name: "Portfolio",
component: Portfolio,
},
{
path: "/portfolio/:id",
name: "PortfolioProduct",
component: PortfolioProduct,
props: true,
},
];
I want to take data from Portfolio.vue to PortfolioProduct.vue , i couldn't solve. I'm using vue js3 , if you help me i will be glad. Thank you
You could probably use the template literal, changing the router-link would be like so:
<router-link :to="`/portfolio/${data.slug}`"> <div class="element" :data-description="data.description">

Vuejs - v-bind.sync on resursive components (hierarchical list)

I have a hierarchical list component where child items have checkboxes. Checkbox actions(check/uncheck) must keep the parent component in sync with the checkbox's changed state. I cannot figure out how to achieve this using v-bind.sync recursively. My code is as below:
Menu.vue
This component holds the hierarchical list. (Only relevant code included)
HierarchicalCheckboxList is the component that displays the hierarchical list
Property 'value' holds the check/uncheck value (true/false)
Property 'children' contains the child list items
How do I define the .sync attribute on HierarchicalCheckboxList and with what parameter?
<template>
<div>
<HierarchicalCheckboxList
v-for="link in links"
#checked="primaryCheckChanged"
:key="link.id"
v-bind="link">
</HierarchicalCheckboxList>
</div>
</template>
<script>
import HierarchicalCheckboxList from 'components/HierarchicalCheckboxList'
data () {
return {
links: [{
id: 1,
title: 'Home',
caption: 'Feeds, Dashboard & more',
icon: 'account_box',
level: 0,
children: [{
id: 2,
title: 'Feeds',
icon: 'feeds',value: true,
level: 1,
children: [{
id: '3',
title: 'Dashboard',
icon: 'settings',
value: true,
level: 1
}]
}]
}]
}
},
methods: {
primaryCheckChanged (d) {
// A child's checked state is propogated till here
console.log(d)
}
}
</script>
HierarchicalCheckboxList.vue
This component calls itself recursively:
<template>
<div>
<div v-if="children != undefined && children.length == 0">
<!--/admin/user/user-->
<q-item clickable v-ripple :inset-level="level" :to="goto">
<q-item-section>
{{title}}
</q-item-section>
</q-item>
</div>
<div v-else>
<div v-if="children != undefined && children.length > 0">
<!-- {{children}} -->
<q-expansion-item
expand-separator
:icon="icon"
:label="title"
:caption="caption"
:header-inset-level="level"
default-closed>
<template v-slot:header>
<q-item-section>
{{ title }}
</q-item-section>
<q-item-section side>
<div class="row items-center">
<q-btn icon="add" dense flat color="secondary"></q-btn>
</div>
</q-item-section>
</template>
<HierarchicalCheckboxList
v-for="child in children"
:key="child.id"
#checked="primaryCheckChanged"
v-bind="child">
</HierarchicalCheckboxList>
</q-expansion-item>
</div>
<!-- to="/admin/user/user" -->
<div v-else>
<q-item clickable v-ripple :inset-level="level">
<q-item-section>
<q-checkbox :label="title" v-model="selection" />
</q-item-section>
</q-item>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'HierarchicalCheckboxList',
props: {
id: { type: String, required: true },
title: { type: String, required: false },
caption: { type: String, default: '' },
icon: { type: String, default: '' },
value: { type: Boolean, default: false },
level: { type: Number, default: 0 },
children: { type: Array }
},
data () {
return {
localValue: this.$props.value
}
},
computed: {
selection: {
get: function () {
return this.localValue
},
set: function (newvalue) {
this.localValue = newvalue
this.$emit('checked', this.localValue)
// or this.$emit('checked', {id: this.$props.id, value: this.localValue })
}
}
},
methods: {
primaryCheckChanged (d) {
this.$emit('checked', d)
}
}
}
</script>
What works so far
As a work-around I am able to get the checkbox state emitted with $emit('checked'), which I use to send it to the next process. But the parent's state is not updated until I refresh it back from the database.
How do I update the parent component's state using v-bind.sync recursively?
Appreciate any help!!
UI
Figured out how to do it after I broke the code down from the whole 2000 line code to a separate 'trial-n-error' code of 20 lines and then things became simple and clear.
Menu.vue
A few changes in the parent component in the HierarchicalCheckboxList declaration:
Note the sync property
<HierarchicalCheckboxList
v-for="child in children"
:key="child.id"
:u.sync="link.value"
v-bind="child">
</HierarchicalCheckboxList>
HierarchicalCheckboxList.vue
Change the same line of code in the child component (as its recursive)
<HierarchicalCheckboxList
v-for="child in children"
:key="child.id"
:u.sync="child.value"
v-bind="child">
</HierarchicalCheckboxList>
And in the computed set property, emit as below:
this.$emit('update:u', this.localValue)
That's it - parent n children components now stay in snyc.

Vue 3 - dynamically bind font awesome icons

I have a v-for loop, everything else seems to be working, but for some reason the icon doesn't show up.
How can I bind it correctly?
Note: I also return the icon and import it, and am already using font awesome in my project, so I know it's working. The problem is how I am binding it.
Imports:
import { ref } from '#vue/composition-api'
import {faCircle} from '#fortawesome/free-solid-svg-icons'
v-for loop:
<div
class="m-auto w-full flex flex-col justify-center items-center"
>
<div>
<fa :icon="item.icon" class="text-5xl text-gray-400" />
// the icon isn't showing but when I write {{item.icon}} I can see the text 'faCircle'
</div>
<div class="py-4 text-2xl font-semibold block">
{{ item.title }}
</div>
<div class="px-10">
<span class="text-base">
{{ item.text }}
</span>
</div>
</div>
Script:
export default {
setup() {
const items = ref([
{
title: 'bla',
text: 'bla',
icon: 'faCircle'
},
{
title: 'bla',
text: 'bla',
icon: 'faCircle'
},
{
title: 'bla',
text: 'bla',
icon: 'faCircle'
}
])
I returned the items:
return {
faCircle,
items
}
So I know what the Problem was. I wrote it as a String
Script
export default {
setup() {
const items = ref([
{
title: 'bla',
text: 'bla',
icon: 'faCircle'
},
{
title: 'bla',
text: 'bla',
icon: 'faCircle'
},
{
title: 'bla',
text: 'bla',
icon: 'faCircle'
}
])
ICON should be written WITHOUT Strings because we are importing it inside an expression
{
title: 'bla',
text: 'bla',
icon: faCircle
}