Conditional wrapper rendering in vue - vue.js

I'm making a link/button component which either can have a button or an anchor wrapper, a text and an optional icon. My template code below is currently rendering either an anchor or a button (with the exact same content) based on an if statement on the wrapper element, resulting in duplicate code.
<template>
<a v-if="link" v-bind:href="url" class="btn" :class="modifier" :id="id" role="button" :disabled="disabled">
{{buttonText}}
<svg class="icon" v-if="icon" :class="iconModifier">
<use v-bind="{ 'xlink:href':'#sprite-' + icon }"></use>
</svg>
</a>
<button v-else type="button" class="btn" :class="modifier" :id="id" :disabled="disabled">
{{buttonText}}
<svg class="icon" v-if="icon" :class="iconModifier">
<use v-bind="{ 'xlink:href':'#sprite-' + icon }"></use>
</svg>
</button>
</template>
Is there a more clean way for wrapping my buttonText and icon inside either an anchor or button?

I've solved my issue by intensive Google-ing! Found this issue regarding Vue on Github which pointed me in the right direction.
Small piece of backstory
I'm using Vue in combination with Storybook to build a component library in which a button can either be a button or an anchor. All buttons look alike (apart from color) and can be used for submitting or linking. To keep my folder structure ordered, I would like a solution that generates a multiple buttons types (with or without link) from one single file.
Solution
Using computed properties I'm able to "calculate" the necessary tag, based on the url property of my component. When a url is passed, I know that my button has to link to another page. If there is no url property it should submit something or preform a custom click handler (not in the sample code below).
I've created the returnComponentTag computed property to avoid placing any complex or bulky logic (like my original solution) in my template. This returns either an a or a button tag based on the existence of the url property.
Next, as suggested by ajobi, using the :is attribute I'm able to define the component tag based on the result of my computed property. Below a stripped sample of my final (and working) solution:
<template>
<component :is="returnComponentTag" v-bind:href="url ? url : ''" class="btn" :class="modifier" :id="id">
{{buttonText}}
</component>
</template>
<script>
export default {
name: "Button",
props: {
id: {
type: Number
},
buttonText: {
type: String,
required: true,
default: "Button"
},
modifier: {
type: String,
default: "btn-cta-01"
},
url: {
type: String,
default: ""
}
},
computed: {
returnComponentTag() {
return this.url ? "a" : "button"
}
}
};
</script>

You could extract the wrapping element into a dedicated component.
<template>
<a v-if="link" v-bind:href="url" class="btn" :class="modifier" :id="id" role="button" :disabled="disabled">
<slot></slot>
</a>
<button v-else type="button" class="btn" :class="modifier" :id="id" :disabled="disabled">
<slot></slot>
</button>
</template>
// You would use it like this
<SomeComponent /* your props here */ >
{{buttonText}}
<svg class="icon" v-if="icon" :class="iconModifier">
<use v-bind="{ 'xlink:href':'#sprite-' + icon }"></use>
</svg>
</SomeComponent>

There are multiple ways of doing this. Two examples would be the following based on the point of view:
You are defining two different components (Button or Anchor) and want to use a wrapper to render either one of them.
You could seperate the Wrapper Content into two components so that the wrapper only decides on which of the components to render (either the Button or the Anchor).
The problem with this approach could be you will have doubled code for methods and styling for the button and anchor component.
You are defining the content as a component and use the wrapper to define what to wrap the content in.
See Answer of https://stackoverflow.com/a/60052780/11930769
It would be great to know, why you would want to achive this. Maybe there are better solutions for your usecase. Cheers!

Related

Vue3: Check if event listener is bound to component instance

I have a reusable Badge component. I want to be able to add a close/delete button when an onDelete event listener is present on the component instance.
<template>
<div class="flex inline-flex items-center px-2.5 py-0.5 text-xs font-medium select-none" :class="[square ? '' : 'rounded-full']">
<slot />
<button class="cursor-pointer ml-2" #click="$emit('onDelete')">
<XIcon class="flex-shrink-0 h-3.5 w-3.5 text-gray-400 hover:text-gray-500" aria-hidden="true" />
</button>
</div>
</template>
<script>
import { XIcon } from '#heroicons/vue/solid';
export default {
props: {
color: { type: String },
square: { type: Boolean, default: false },
},
components: {
XIcon,
},
emits: ['onDelete'],
};
</script>
If I add a v-if statement to the button, the emit event is executed immediately
<button v-if="$emit('onDelete')" class="cursor-pointer ml-2" #click="$emit('onDelete')">
I'm using Vue 3
UPDATE: If your component is using the new emits option in Vue3, which is the recommended best practice from the Vue3 docs, the event listeners will not be apart of the $attrs. An issue will be submitted to the Vue team for clarification and guidance on why this behaves this way.
I have simplified your example above in StackBlitz to isolate the functionality you are after.
Important note, I am using Vue 3.2.26.
In Vue3 $listeners were removed.
Event listeners are now part of $attrs. However simply console logging this.$attrs in Badge won't display the key you are looking for, they are within targets but accessible by prepending on to the bound event name. In your case in the Badge component you will use onOnDelete.
Complete working example with Two Badges. One will display because it has a bound event onDelete, the other will not due to the fact that the bound onDelete is not present.
https://stackblitz.com/edit/vue-8b6exq?devtoolsheight=33&file=src/components/Badge.vue

Vue Bootstrap, how to interact with plus/minus icon on dynamic generated collapse content separately

I have a VueJS view that creates collapsed contents using Bootstrap Vue Collapse Component.
The data is dynamic and can contains hundreds of items, which is why you see in the code below it was created via a v-for loop in Vue.
<div class="inventory-detail" v-for="(partNumberGroup,index) in inventory" :key="index" >
<b-button block v-b-toggle="partNumberGroup.partNumber" v-bind:id="partNumberGroup.partNumber" variant="primary"
#click="(evt) =>{isActive = !isActive && evt.target.id == partNumberGroup.partNumber}">
<i v-bind:id="partNumberGroup.partNumber" class="float-right fa" :class="{ 'fa-plus': !isActive, 'fa-minus': isActive }"></i>
{{ partNumberGroup.partNumber }}
</b-button>
<div class="inventory-detail__card" v-for="item in partNumberGroup.items">
<b-collapse v-bind:id="partNumberGroup.partNumber" >
<b-card>
<!--Accordion/Collapse content -->
</b-card>
</b-collapse>
</div>
</div>
This works fairly well in that I can individually expand and collapse each content separately. However, the one issue I'm facing is each time I click the icon fa-minus (-) orfa-plus (+), all of them changed as per the images below.
Any tips on how I should implementing this? in my code I tried the dynamic CSS class switching but I still lack the ability to switch on specific element.
I feel like the solution to this is to somehow conditionally apply dynamic CSS class or somehow able to use the attribute 'aria-expanded'.
You can try something like this. Whenever somebody clicks on the icon, set its index as activeIndex (using the setActiveIndex method). Then you can set the class accordingly by comparing the activeIndex with current index
<i
#click="setActiveIndex(index)"
v-bind:id="partNumberGroup.partNumber"
class="float-right fa"
:class="{ 'fa-plus': !isActive(index), 'fa-minus': isActive(index) }">.
</i>
then in the script part:
...
data() {
return {
activeIndex: -1
}
},
methods: {
/* set active index on click */
setActiveIndex(index) {
this.activeIndex = index;
},
/* check if index is active or not */
isActive(index) {
return index === this.activeIndex;
}
}

vue.js "TypeError: Cannot read property 'path' of undefined"

I know this is a common question, but I have been going through my files now so many times without being able to the locate the error.
I am getting this error when I try to route to my components in my navigation menu.
My app.vue file:
<template>
<div id="app">
<Navbar
:nav-links="navLinks"
/>
<router-view/>
</div>
</template>
<script>
import Navbar from '#/components/Navbar'
export default {
components: {
Navbar
},
data: () => ({
navLinks: [
{
text: 'Home',
path: '/home'
},
{
text: 'About',
path: '/about'
},
{
text: 'Contact',
path: '/contact'
}
]
})
}
</script>
My Navbar component (This is where the error happens)
<template>
<nav>
<ul>
<li v-for="{link, index} in navLinks" :key="index"
#mouseover="hover = true"
#mouseleave="hover = false">
<router-link :to="link.path">
{{ link.text }}
</router-link>
</li>
</ul>
</nav>
</template>
<script>
export default {
props: ['navLinks'],
data(){
return {
hover: false,
}
}
}
</script>
How do I fix this?
<li v-for="{link, index} in navLinks" :key="index"...
should be
<li v-for="(link, index) in navLinks" :key="index"...
As it's now (destructured), link refers to a link property inside the object, not the object itself. Additionally, index is probably undefined, since the navLinks objects probably don't have an explicit property index. Therefore Vue might also complain about using invalid indexes in v-for.
Since you're only using the path prop, you could actually use destructuring, like this:
<li v-for="({ path }, index) in navLinks" :key="index"
#mouseover="hover = true"
#mouseleave="hover = false">
<router-link :to="path">
</li>
Another, unrelated note: hover property is currently being shared across all navLinks. If you expect it to somehow be related to the currently hovered element, yo uhave to save that separately (probably inside the navLink itself).
As for :nav-links="navLinks", what you've done is not only perfectly legal, but the recommended way of doing it (it's according to the HTML spec). Using :navLinks="navLinks" relies on Vue's HTML parser, which converts it to nav-links behind the scenes - inspect the HTML element and you'll notice it).
If you want to get into the details, you could have a look at this discussion on the subject. The result was: use either, but if you use camelCase it will be inconsistent with the rendered markup. If you use kebab-case, it will be consistent with rendered markup, so you won't have to deal with this difference when writing tests, for example, should you ever need to select elements by their attributes (jest converts camelCase to lowercase - hence it's inconsistent with the rendered markup, so the tests start passing/failing based on whether mount or shallowMount is used. Goes without saying, that's not really a good testing setup. )
The same exact discussion goes for using <SomeComponent /> vs <some-component />. While both work, using first needs to be addressed when writing tests if you need to select stubbed subcomponents.
Besides, vue/attribute-hyphenation (the way you did it) is part of the following vue linting presets:
plugin:vue/strongly-recommended
plugin:vue/vue3-recommended
plugin:vue/recommended
A prop in the Navbar component is named navLinks but you access it outside as nav-links.
This should work:
:navLinks="navLinks"
Incorrect syntax for v-for with {}. Use ():
li v-for="(link, index) in navLinks
You have done two mistakes here.
one is:
<template>
<div id="app">
<Navbar
:nav-links="navLinks"
/>
<router-view/>
</div>
Here you are binding with different name(nav-links), you should keep same name with which you are binding data and the name inside the props(navLinks).
Both names should be same.
Second one:
v-for="{link, index} in navLinks"
The syntax is wrong, the correct syntax should be
v-for="(link, index) in navLinks"

how to select whole row in ant-design-vue table?

i have an ant-design-vue table which has the function to select the row and open a new drawer with the record.
this is the template of the table.
<a-table
:dataSource="employeeData"
:rowKey="record => record.sgid"
:pagination="{ pageSize: size }"
:columns="columns"
:loading="loading"
:scroll="{ x: 1300, y: 400 }"
:rowSelection="{selectedRowKeys: selectedRowKeys, onChange: onSelectChange}"
>
<template slot="name" slot-scope="text, record">
<div class="click-event" #click="select(record)">{{ text }}</div>
</template>
<template slot="id" slot-scope="text, record">
<div class="click-event" #click="select(record)">{{ text }}</div>
</template>
<template slot="mobile" slot-scope="text, record">
<div class="click-event" #click="select(record)">{{ text }}</div>
</template>
</a-table>
with this code i add a <div> to all slot so that user can click anywhere in every column. but there is still some empty space between two column in a row that cannot be click. what should i do to make the user can click on a row to run the select function?
Wrap your column around a Link for redirection instead of an HTML anchor tag <a> <slots /> </a>.
react-router-dom
You could use Link from react-router-dom and use it to trigger your component or container. This will also isolate your slot function logic in a separate component or container
import { Link } from 'react-router-dom';
...
<Link to={`/redirect/to/slot-function-component/`}>
<Row Content />
</Link>
...
It is important to register this component in a routes.js
Ant Design: Anchor Component
If you're going to stick with Ant Design, then you might wanna explore Anchor Component API. Anchor Props and Link Props provides you with lots of options to implement your use case in multiple ways.
Explore the API and see what suits your requirements.

Why does my Vue component require :key?

I have a small Vue.js component which displays a favorite star icon. Clicking on the icon favorites/unfavorites the element. So far I have only implemented the UI part, which looks like this:
<template>
<div :key="favorite">
<a v-on:click="toggleFavorite" style="cursor: pointer">
<i v-show="favorite" class="text-warning fas fa-star"></i>
<i v-show="!favorite" class="text-warning far fa-star"></i>
</a>
</div>
</template>
<script>
export default {
data() {
return {
favorite: true,
}
},
mounted() {
},
methods: {
toggleFavorite() {
this.favorite = !this.favorite
}
},
props: ['team-id'],
}
</script>
<style scoped>
</style>
As you can see, the logic is pretty simple.
This works well, but one thing that bothers me is that, if I remove the :key property from my template, the icon is not updated when I click on it (even though I have checked that the underlying property is indeed updated correctly). Adding :key makes it work, I imagine because it forces Vue.js to completely re-render the component when favorite is updated.
Why is this happening? I'm fairly new to the world of JS frameworks, so forgive any obvious stuff I might be missing. I did some research online but couldn't find an explanation. I just want to make sure I'm doing things the right way and not merely hacking around the issue here.
Vue patches with the virtual DOM whenever it is necessary. That is, whenever vue detects the changes on the DOM, it patches them for faster performance. And patching in the DOM will not change the icon or image. You need to replace the DOM instead.
Thus, vue provides the way for us whenever we need to change the DOM by replacing method, we can use :key binding.
So, :key binding can be used to force replacement of an element/component instead of reusing it.
The following whole html div will be replaced whenever there is change in favorite data as we're :key binding on it:
<div :key="favorite">
<a v-on:click="toggleFavorite" style="cursor: pointer">
<i v-show="favorite" class="text-warning fas fa-star"></i>
<i v-show="!favorite" class="text-warning far fa-star"></i>
</a>
</div>
This is why vue forcefully allows us to use :key binding inside a loop as there's need of replacing the elements inside the loop whenever it detects the changes in the data. This is made compulsory from 2.2.0+ and ESLint also have implemented this feature so that if you miss :key binding inside the loop, then you'll see the error on that line when you use editor that supports eslint, so that you can fix the error.
Just an opinion, the strict requirement of the :key binding should be removed from the vue as we might want a loop of predefined data and don't want to change the DOM but we still use the v-for loop for listing bigger data. But it might be rare case though.
Read carefully on the documentation for :key binding and then you'll have an idea.
The :key binding can be useful when you want to:
Properly trigger lifecycle hooks of a component
Trigger transitions
Use :key binding to replace the DOM. Remember it slower the performance as it replace the whole DOM that is bound to the element.
Don't use :key binding when you don't want to replace the DOM or
you think there's no data changes detection required. This will
allow vue to perform better without :key binding.
Its seems to be a general issue of FontAwesome CSS regardless the framework.
There is an issue on github and here the same issue with react https://github.com/FortAwesome/Font-Awesome/issues/11967
To prove that, here is a simplified version of the same example but using bootstrap icons
new Vue({
el: '#app',
data() {
return {
fav: true
}
}
});
<script
src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.13/vue.js"
></script>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">
<div id="app">
<div>
<a v-on:click="fav = !fav" style="cursor: pointer">
<i v-show="fav" class="glyphicon glyphicon-star"></i>
<i v-show="!fav" class="glyphicon glyphicon-star-empty"></i>
</a>
</div>
</div>
You shouldn't need the :key, it's only necessary in v-for loops. I would suggest you remove it and replace your v-show with a v-if and v-else directive.
<i v-if="favorite" class="text-warning fas fa-star"></i>
<i v-else class="text-warning far fa-star"></i>
v-if removes and addes the section to the DOM whereas v-show just hides it so this way well resolve your issue
Ok I think the problem here is that you're changing your root data object. To preserve reactivity, you shouldn't change the root data object after you've instantiated Vue.
Here is your code in a simple Vue. I didn't need :key to make it work. I would keep :key for inside loops.
markup
<div id="vueRoot">
<a v-on:click="toggleFavorite" style="cursor: pointer">
<i v-show="store.favorite" class="text-warning fas fa-star">Fav</i>
<i v-show="!store.favorite" class="text-warning far fa-star">Not fav</i>
</a>
</div>
code
vm = new Vue({
el : "#vueRoot",
data() {
return { store :{
favorite: true
}}
},
mounted() {
},
methods: {
toggleFavorite() {
this.store.favorite = !this.store.favorite
}
}
}
);
This is a working example with minimal changes. From what you've showed us, you should just have <i> element, then do what you want with a dynamic class list, like...
<i :class="['text-warning','fa-star',store.favorite?'fas':'far']"></i>