Is it possible to concatenate class properties in vuejs? - vue.js

General Question: I was wondering if I can concatenate class properties based on condition. See pseudo-code in the v-forline.
My use-case: I want to align all images that have an even index at the right side.
If I use flex flex-row-reverse for the parent section I get the images aligned on the right. But I don't know how to construct the class in such a way that I do not have to repeat the code for the child elements.
<section
v-for="(quote, index) of $frontmatter.quotes :class="lg:flex my-4 mx-12 overflow-auto" + {even: index % 2, odd: !(index % 2)}"
>
<img
class="quote-image right rounded-full h-64 w-64 flex items-center justify-center shadow-md mx-auto lg:ml-16 lg:mr-10 mt-12"
:src="$withBase(quote.image)"
/>
<div class="px-8 pt-6">
<h3 class="text-primary font-bold mb-4">
{{ quote.title }}
</h3>
<p>
{{ quote.text }}
</p>
</div>
</section>
And call the class extension something like:
.even {
flex-row-reverse
}
Currently,I use this structure - however, I am not happy with that, as I have to repeat my code for the child elements...
<section
v-for="(quote, index) of $frontmatter.quotes"
class= "my-16 mx-24 overflow-auto"
>
<div v-if="index % 2"
class="lg:flex flex-row-reverse">
<img
class="quote-image rounded-full h-64 w-64 flex items-center justify-center shadow-md mx-auto lg:ml-16 lg:mr-10 mt-12"
:src="$withBase(quote.image)"
/>
<div class="px-8 pt-6">
<blockquote>
<h2 class="text-primary font-bold mb-4">
{{ quote.title }}
</h2>
</blockquote>
<p class="quote-text">
{{ quote.text }}
</p>
</div>
</div>
<div v-else
class="lg:flex">
<img
class="quote-image rounded-full h-64 w-64 flex items-center justify-center shadow-md mx-auto lg:ml-16 lg:mr-10 mt-12"
:src="$withBase(quote.image)"
/>
<div class="px-8 pt-6">
<blockquote>
<h2 class="text-primary font-bold mb-4">
{{ quote.title }}
</h2>
</blockquote>
<p class="quote-text">
{{ quote.text }}
</p>
</div>
</div>
</section>
It should look something like:

First, just to clarify the question I'm trying to answer. Given the following code:
<div v-if="index % 2" class="lg:flex flex-row-reverse">
... children ...
</div>
<div v-else class="lg:flex">
... identical children ...
</div>
Is there a way to avoid the v-if and conditionally add the flex-row-reverse class instead?
You've got a number of options here. Firstly, the attributes class and style have special behaviour that allows you to specify both a bound and static copy of the same attribute. You can't do that for other attributes. e.g.
<div
class="lg:flex"
:class="{ 'flex-row-reverse': index % 2 }"
>
So the class lg:flex is added as a static class whereas, flex-row-reverse is added conditionally. Vue will combine them as appropriate to create the class attribute of the finished DOM nodes.
There are a number of other ways this could be written. Here are a couple to ponder:
<div :class="{ 'lg:flex': true, 'flex-row-reverse': index % 2 }">
<div :class="['lg:flex', { 'flex-row-reverse': index % 2 }]">
Arrays can be nested arbitrarily deep. Plain strings will be treated as classes to add. Objects will be treated as collections of conditional classes with the class name as the property name and the condition the property value.
All of this is using Vue's support for conditionally adding classes. However, a bound expression is just arbitrary JavaScript so you could apply the conditionality using normal JavaScript syntax instead of having Vue do it for you.
This is generally clunkier. This has to be a single expression so you can't use if/else. However, you can use ?: instead:
<div :class="'lg:flex' + (index % 2 ? ' flex-row-reverse' : '')">
The official documentation for these various ways to build classes is here:
https://v2.vuejs.org/v2/guide/class-and-style.html#Binding-HTML-Classes

Since you are using classes, not <b-img> props, perhaps the class you need is float-right instead of right
<img v-if="index % 2"
class="quote-image float-right rounded-full h-64 w-64 shadow-md mx-auto lg:ml-16 lg:mr-10 mt-12"
:src="$withBase(quote.image)"
/>

Related

How to get ID from conditionally rendered item in modal component?

I have a conditionally-rendered notes and I want to delete specific note, depends on ID.
I have no problem with deleting without modal, but my problem is that I want to delete note only when modal is accepted.
There is modal component:
<DeleteModal
:modalVisible="modalVisible"
:restore="modalType"
#closeModal="modalVisible = false"
#deleteItem="deleteNote(note.id)"
#restoreItem="restore()"
/>
Delete method:
const deleteNote = (id) => {
Inertia.delete(route('notes.destroy', id));
getNotes();
};
and there is how I render notes:
<div class="my-4" v-for="note in notesForIssue" :key="note">
<div class="flex flex-row justify-between"></div>
<div class="bg-gray-100 w-full min-h-16 mt-2 rounded whitespace-normal">
<div class="px-8 py-4">
<p>{{ note.text }}</p>
<div class="flex flex-row items-center justify-between">
<div class="flex flex-row text-xs">
<span class="text-[#2563EB]">{{ note.updated_at }}</span>
<p class="ml-2">{{ __('history.by') }}</p>
<p class="font-bold ml-2 text-[#2563EB]">{{ note.updated_by }}</p>
</div>
<div #click="modalType=false; modalVisible=true;">
</div>
</div>
</div>
</div>
</div>
But I have no access to note.id here: #deleteItem="deleteNote(note.id)". Any ideas, how to solve this problem?
Try this approach.
When you click on the div, instead of set visibality of modal,
set selectedItem
set visibility
then you accept by modal, you have a `selectedItem' for delete
After delete, clear the selectedItem.
First the key attribute is not accpet object type variable. Second maybe you should check what is notesForIssue first ,then find out why you can't access note.id

Cannot execute scoped slot and function in the same event Vue

I have the following code which uses this VueTailwind package:
<t-dropdown>
<div
slot="trigger"
slot-scope="{mousedownHandler, focusHandler, blurHandler, keydownHandler}"
>
<button
id="reseller-menu"
aria-label="User menu"
aria-haspopup="true"
#mousedown="mousedownHandler"
#focus="focusHandler"
#blur="blurHandler"
#keydown="keydownHandler"
>
{{ $page.props.auth.user.reseller.name }}
<icon icon="chevron-down" class-name="ml-2" />
</button>
</div>
<div slot-scope="{ blurHandler }">
<span v-for="user in users" :key="reseller.id" role="menuitem" class="block px-6 cursor-pointer py-2 hover:bg-indigo-500 hover:text-white"
#click="changeUser(user.id); blurHandler">{{ user.name }}</span>
</div>
</t-dropdown>
However, the blurHandler is not executed whenever the changeUser(user.id) method (a method in the parent component which seems to execute fine) is added to the #click event. The only way I was able to solve this issue was to use two different events such as the following:
<span v-for="user in users" :key="reseller.id" role="menuitem" class="block px-6 cursor-pointer py-2 hover:bg-indigo-500 hover:text-white"
#click="changeUser(user.id)" #mouseup="blurHandler">{{ user.name }}</span>
How can I use both in the same event since this doesn't seem to work in this case?
When the v-on value is a function reference, the template compiler transforms it into a function call, passing the event argument:
<button #click="functionRef">
<!-- ...is transformed into: -->
<button #click="functionRef($event)">
But when the v-on value is an expression, the template compiler wraps it in a function:
<button #click="onClick(); functionRef">
<!-- ...is transformed into: -->
<button #click="() => { onClick(); functionRef; }">
Notice functionRef; is not a function call, and effectively does nothing. To actually invoke the function in that expression, add the parentheses to make it a call:
<button #click="onClick(); functionRef()">
So your markup should be:
<div slot-scope="{ blurHandler }">
<span v-for="user in users" ⋯ 👇
#click="changeUser(user.id); blurHandler()">
{{ user.name }}
</span>
</div>
Also note slot-scope has been deprecated for v-slot, as of 2.6.0.

Apply styling to certain items in a list in Vue

how should I go about applying an attribute to a class for styling only for certain elements in a list? It will be clearer in the example below:
<u-col
v-for="(item, i) in items"
:key="i"
:class="classNames.viewport"
>
data () {
return {
classNames: {
viewport: "col-lg-3 col-md-3 col-sm-3 col-6 d-flex justify-content-center"
}
}
}
Assuming i only goes from 0-3. Currently every item in v-for has the class viewport for styling. What I am hoping to achieve is to only apply an additional margin style mb-3 if i is greater than 1.
ie, the viewport becomes "col-lg-3 col-md-3 col-sm-3 col-6 d-flex justify-content-center mb-3" for items with i > 1, while viewport does not contain mb-3 for items with i < 1.
I thought about using a v-for and v-if together but I believe it is not a good practice to do so, https://www.codecheef.org/article/dont-use-v-if-with-v-for-elements.
In :class bind, you can check i and add mb-3 to class when i > 1:
<u-col
v-for="(item, i) in items"
:key="i"
:class="classNames.viewport + (i > 1 ? ' mb-3' : '')"
>
Try like this
:class="`${classNames.viewport} ${i > 1 ? 'mb-3' : ''}`"
You can use Vue's Class and Style bindings and pass it an object.
<u-col
v-for="(item, i) in items"
:key="i"
:class="{
'col-lg-3 col-md-3 col-sm-3 col-6 d-flex justify-content-center': true,
'mb-3': i > 1,
}"
/>

Nuxt.js/Vue.js dynamic images is not working

I'm getting some data from an API that contains an image and some other information. Now there's a problem in Nuxt js or maybe I'm wrong. Whenever I supply the path to the image in the :src it just doesn't work.
Here's my code:
<div v-for="(project, index) in projects" :key="project.p_id" class="swiper-slide">
<div :id="`index_${index}`" class="slide_wrapper">
<div class="background">
<img :src="getImgUrl(project.p_img_path)" alt="project cover image" />
</div>
<div class="details">
<div>
<div class="left">
<h2 style="color: black !impor tant">{{ project.p_label }}</h2>
<small>{{ project.p_date }}</small>
</div>
<div class="right">
<nuxt-link class="btn btn-secondary" to="/" #click="readMore">
<i class="fas fa-ellipsis-h"></i>
</nuxt-link>
</div>
</div>
</div>
</div>
</div>
script:
props: ['projects'],
methods: {
readMore() {},
getImgUrl(path) {
return '../../../' + path
},
},
Last thing, whenever I use required required('./assets/'+image.jpg') nuxt.js crushes in compilation always stops at 69%. I also tried required.context but it didn't work as well.
Any help will be appreciated. Thanks in advance.
In your template, change:
<img :src="getImgUrl(project.p_img_path)" alt="project cover image">
To:
<img :src="require(`../assets/${project.p_img_path}`)" alt="project cover image">
You shouldn’t need a method to return the path as a string, just use a template literal as above.
Ps. This assumes project.p_img_path = img/project_img/filename.jpg, so adjust as necessary.
The solution is that I had to put all the images inside the assets folder directly. Thank you all for your time.

Can I use parts of html template of a component in different part in parent component

I have a component which returns locations, then places, then hotels one after her other but I want these in 3 different parts like they show in tabs.
component code:
<template>
<li class="col-xs-12" :id="location.id">
<p class="col-xs-3">{{location.name}} - Days : </p>
<div class="col-xs-2"><input type="text" v-model="location.days"></div>
<div class="col-xs-4">
<!-- {{hotelset(location.id,hotels)}}-->
<!-- {{typeof (hotels)}}{{index}}-->
<v-select v-model="hotelselect[location.id]" name="addhotel" label="title"
#input="addtohotel(index,location.id)" :value="non" :options=hoteloptions[location.id] />
<!--{{hoteloptions}}-->
<!--{{$props}}-->
</div>
<button #click="remove" class="col-xs-3 btn btn-danger">Remove</button>
<div></div>
<div class="col-xs-12" v-for="day in parseInt(location.days)" :key="day">
Day {{day}}-
{{preselect(day,defaultt,location.id,place)}}
<v-select v-model="days[day]" name="addplaceloc" label="title" #input="addtoplaces(day,location.id)"
:value="defaultt" :options="localplace[location.id][day]" multiple />
<!--{{localplace[location.id][day]}}-->
<div v-for="(placeinfo, index) in objj[location.id]['day-'+day]['place']">Place: {{placeinfo['title']}}:
<label>From:</label><input type="text"
v-model="objj[location.id]['day-'+day]['place'][index]['from_time']">
<label>To:</label><input type="text" v-model="objj[location.id]['day-'+day]['place'][index]['to_time']">
<label>Remark:</label><input type="text"
v-model="objj[location.id]['day-'+day]['place'][index]['remark']">
<button #click="run"> yess</button>
</div>
</div>
</li>
<!--#click="remove"-->
in above code there are 2 v-select and 1 input listed one after the other as they are dependent to each other but I want it in different tabs in parent vue file. How to achieve it? tabs have its own style and html structure.
As #muka.gergely said, the only way to re-use HTML is to break it off into separate components.