Way to remove custom component code repetition in Vue? - vue.js

My custom component code:
<template>
<div class="event">
<div class="title text">
{{ title }}
</div>
<div class="date text">
{{ date }}
</div>
<div class="time text">
{{ time }}
</div>
<div class="address text">
{{ address }}
</div>
<div class="learn-more text">
Learn more!
</div>
</div>
<script>
export default {
name: 'EventCard',
props: {
title: {
type: String,
required: true,
default: ''
},
date: {
type: String,
required: false,
default: ''
},
time: {
type: String,
required: true,
default: ''
},
address: {
type: String,
required: true,
default: ''
},
type: {
type: String,
required: true,
default: ''
}
}
}
with some styling as well.
I am then using this custom component in an EventDiscoveryPage.vue file where I have the code written as such:
<div class="events">
<EventCard class="food-event" :title="'food event'" :date="'date-test'" :time="'time-test'" :address="'address-test'" :type="'music'" />
<EventCard class="middle-row-event music-event" :title="'music event'" :date="'date-test'" :time="'time-test'" :address="'address-test'" :type="'music'" />
<EventCard class="music-event" :title="'music event'" :date="'date-test'" :time="'time-test'" :address="'address-test'" :type="'music'" />
<EventCard class="comedy-event" :title="'comedy event'" :date="'date-test'" :time="'time-test'" :address="'address-test'" :type="'music'" />
<EventCard class="middle-row-event music-event" :title="'music event'" :date="'date-test'" :time="'time-test'" :address="'address-test'" :type="'music'" />
<EventCard class="food-event" :title="'food event'" :date="'date-test'" :time="'time-test'" :address="'address-test'" :type="'music'" />
<EventCard class="music-event" :title="'music event'" :date="'date-test'" :time="'time-test'" :address="'address-test'" :type="'music'" />
<EventCard class="middle-row-event comedy-event" :title="'comedy event'" :date="'date-test'" :time="'time-test'" :address="'address-test'" :type="'music'" />
<EventCard class="comedy-event" :title="'comedy event'" :date="'date-test'" :time="'time-test'" :address="'address-test'" :type="'music'" />
</div>
As of right now, the code works as intended, but I am wanting to reduce this seemingly redundant code and was wondering if I am able to use a v-for in some way to automate this process a little bit more, but I wasn't sure because I saw that v-for required preset data? I wasn't able to figure out using the documentation and was wondering if anyone had any recommendations.

You can refactor your code using a v-for iterating over an array of objects like this:
<template>
<div class="events">
<EventCard v-for="event in events" :key="event.title" :class="event.class" :title="event.title" :date="event.date" :time="event.time" :address="event.address" :type="event.type" />
</div>
</template>
<script>
export default {
data () {
return {
events: [
{ class: 'food-event', title: 'food event', date: 'date-test', time: 'time-test', address: 'address-test', type: 'music' },
{ class: '...', title: '...', date: '...', time: '...', address: '...', type: '...' },
...
]
}
}
}
</script>

You can reduce all of it to one object in your EventDiscoveryPage.vue like this and then add the objects to an array and loop over the array:
<script setup>
// Factory Function to build objects (You can easily add types as well)
function createCard( class, title, date, time, address, type ) {
return {
class,
title,
date,
time,
address,
type
}
}
const food = createCard('food-event', 'food event', 'date-test', 'time-test', 'address-test', 'music')
const otherEvent = createCard( class, title, date, time, address, type )
// Add each event to the cards array
let cards = [food, otherEvent]
</script>
<div class="events">
<EventCard v-for="card in cards" :class="card.class" :data="card" />
</div>

Related

Pass data from component

I'm fairly new to Vue and I'm trying to pass data from a component to a view. I'm not sure if I'm using props right. I have a dialog and when I save, I want to insert the data to the database. I also want to reuse the addCustomer() function that's why I didn't place the function in the component.
pages/customers.vue
<template>
<div>
<div class="items-center justify-between md:flex">
<Heading
title="Customers"
desc="The list of customers or companies you work with."
/>
<button #click="openModal" class="btn-primary">Add New Customer</button>
</div>
<CustomerList class="mt-4" :customers="customers" />
</div>
<CustomerDialog
:is-open="isOpen"
:close-modal="closeModal"
:open-modal="openModal"
:name="name"
:address="address"
:email="email"
:add-customer="addCustomer"
/>
</template>
<script setup>
const client = useSupabaseClient();
const name = ref("");
const address = ref("");
const email = ref("");
const isOpen = ref(false);
function closeModal() {
isOpen.value = false;
}
function openModal() {
isOpen.value = true;
}
const { data: customers } = await useAsyncData("customers", async () => {
const { data } = await client.from("customers").select("*");
return data;
});
async function addCustomer() {
if (name.value == "" || address.value == "" || email.value == "") return;
const { data } = await client.from("customers").upsert({
name: name.value,
address: address.value,
email: email.value,
});
customers.value.push(data[0]);
name.value = "";
address.value = "";
email.value = "";
closeModal();
}
</script>
components/customer/Dialog.vue
<template>
<Dialog as="div" #close="closeModal" class="relative z-10">
<input type="text" id="name" v-model="name" />
<input type="text" id="address" v-model="address" />
<input type="email" id="email" v-model="email" />
<button type="button" #click="addCustomer">Save</button>
<button type="button" #click="closeModal">Cancel</button>
</Dialog>
</template>
<script setup>
defineProps([
"name",
"address",
"email",
"addCustomer",
"isOpen",
"closeModal",
"openModal",
]);
</script>
EDIT: The Cancel button in the Dialog works while Save button doesn't.
You cannot bind props directly to the v-model directive, in your case you've to use Multiple v-model bindings
<template>
<Dialog as="div" #close="closeModal" class="relative z-10">
<input type="text" id="name" :value="name" #input="$emit('update:name', $event.target.value)"/>
<input type="text" id="address" :value="adress" #input="$emit('update:address', $event.target.value)" />
<input type="email" id="email" :value="email" #input="$emit('update:email', $event.target.value)" />
<button type="button" #click="$emit('add-customer')">Save</button>
<button type="button" #click="closeModal">Cancel</button>
</Dialog>
</template>
<script setup>
defineProps([
"name",
"address",
"email",
"addCustomer",
"isOpen",
"closeModal",
"openModal",
]);
defineEmits(['update:name', 'update:email','update:address','add-customer'])
</script>
in parent component :
<CustomerDialog
:is-open="isOpen"
:close-modal="closeModal"
:open-modal="openModal"
v-model:name="name"
v-model:address="address"
v-model:email="email"
#add-customer="addCustomer"
/>
As addCustomer method is available in parent. What you can do is that emit an event on Save button click from the dialog component and then capture the event in parent and invoke addCustomer method.
I just created a below code snippet with Vue 2.* just for your understanding purpose.
Vue.component('customerdialog', {
data() {
return {
customerDetailObj: {}
}
},
props: ['name', 'address', 'email'],
mounted() {
this.customerDetailObj = {
name: this.name,
address: this.address,
email: this.email
}
},
template: `<div><input type="text" id="name" v-model="customerDetailObj.name" />
<input type="text" id="address" v-model="customerDetailObj.address" />
<input type="email" id="email" v-model="customerDetailObj.email" />
<button type="button" #click="$emit('add-customer', customerDetailObj)">Save</button></div>`
});
var app = new Vue({
el: '#app',
data: {
name: 'Alpha',
address: 'Street 1',
email: 'alpha#testmail.com',
customerList: []
},
methods: {
addCustomer(customerObj) {
// Created user details
this.customerList.push(customerObj);
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<CustomerDialog :name="name"
:address="address"
:email="email" #add-customer="addCustomer($event)"></CustomerDialog>
<pre>{{ customerList }}</pre>
</div>
You can defineEmits
<script setup>
import {
TransitionRoot,
TransitionChild,
Dialog,
DialogPanel,
DialogTitle,
} from "#headlessui/vue";
defineProps([
"name",
"address",
"email",
"addCustomer",
"isOpen",
"closeModal",
"openModal",
]);
let emit = defineEmits(["add"])
</script>
Or better with typescript:
<script lang="ts" setup>
import {
TransitionRoot,
TransitionChild,
Dialog,
DialogPanel,
DialogTitle,
} from "#headlessui/vue";
defineProps([
"name",
"address",
"email",
"addCustomer",
"isOpen",
"closeModal",
"openModal",
]);
let emit = defineEmits<{ (name: "add"): void }>()
</script>
Now you can use it in your button:
<button
type="button"
class="inline-flex justify-center px-4 py-2 text-sm font-medium text-red-900 bg-red-100 border border-transparent rounded-md hover:bg-red-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-red-500 focus-visible:ring-offset-2"
#click="emit('add')"
>
Save
</button>
Now you can listen to this add event and trigger your addCustomer function
<CustomerDialog
:is-open="isOpen"
:close-modal="closeModal"
:open-modal="openModal"
:name="name"
:address="address"
:email="email"
#add="addCustomer"
/>
You'll want to use event emitters:
In the dialog:
<button #click="$emit('addCustomer')">save</button>
In customers.vue:
<CustomerDialog
:is-open="isOpen"
:close-modal="closeModal"
:open-modal="openModal"
:name="name"
:address="address"
:email="email"
#add-customer="addCustomer"
/>

Vue 3 watch property. Unexpected behavior

Not sure why I'm getting unexpected results here. When I select a product I'd like to have one more dynamic select and input instead I get like +100 of them
<template v-for="product in form.selectedProducts">
<div class="col-span-3">
<Label value="Products" />
<Select :options="products" v-model="product.id" label="trade_name" class="mt-1" />
</div>
<div class="col-span-3">
<Label value="Quantity" />
<Input type="number" v-model="product.quantity" class="block w-full mt-1" />
</div>
</template>
<script setup>
import { useForm } from '#inertiajs/inertia-vue3';
import { watch } from "vue";
const form = useForm({
selectedProducts: [{
id: -1,
quantity: ''
}]
});
watch(form.selectedProducts, (value) => {
form.selectedProducts.push({
id: -1,
quantity: ''
})
})
</script>

Prefilled input that allows the user to add input

I have an input that I want to prefill with https:// so all the user have to enter is the domain. Here's is the input
<div>
<form-label for="form.website">Website</form-label>
<form-input
v-model="form.website"
id="form.website"
type="url"
class="block w-full mt-1"
placeholder="https://example.com"
/>
<form-input-error
class="block w-full"
v-if="form.hasErrors && form.errors['website']"
:message="form.errors['website']"
/>
</div>
If I add to the value attribute I can't type pass the input value.
Thanks for the help. I'm using laravel with inertia and vue.
You can prefill your field in data object:
new Vue({
el: '#demo',
data() {
return {
form: {website: '' }
}
},
methods: {
web() {
this.form.website = 'https://'
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="demo">
<label for="form.website">Website</form-label>
<input
v-model="form.website"
id="form.website"
type="url"
class="block w-full mt-1"
placeholder="https://example.com"
#focus="web"
/>
<form-input-error
class="block w-full"
v-if="form.hasErrors && form.errors['website']"
:message="form.errors['website']"
/>
</div>

Vue-formulate - Group item collapsible / toggle collapse

Is there a possibility to make group item collapsible?
<FormulateInput type="group" name="employments" :repeatable="true" label="Employments"
add-label="+ Add Employment" #default="groupProps">
<!-- Clickable area -->
<div class="group text-sm font-semibold py-2 cursor-pointer relative" #click="groupProps.showForm">
....
</div>
<!-- Nested form: must be collapsible accordion -->
<div class="nested-form" v-show="groupProps.showForm">
....
</div>
</FormulateInput>
I thought to add showForm property to the group context.
For this I need to do Custom input types or is there some other way?
If anyone has any other ideas?
Thanks
I figured it out with the gist of #jpschroeder.
CollapsableGroupItem.vue:
<template>
<div class="group-item" :data-is-open="itemId === showIndex">
<div class="group group-item-title text-sm font-semibold py-2 cursor-pointer relative hover:text-blue-400" #click="toggleBody">
<slot name="title" v-bind="groupItem">#{{ context.index }}</slot>
</div>
<div class="group-item-body" v-show="itemId === showIndex">
<slot name="body">
<FormulateInput type="pelleditor" name="description" label="Description"/>
</slot>
</div>
</div>
</template>
<script>
export default {
name: "CollapsableGroupItem",
props: {
context: {
type: Object,
required: true,
},
showIndex: {
type: [Number, String],
required: true,
},
groupItem: {
type: Object,
required: true,
},
},
data () {
return {
itemId: this.context.name + this.context.index
}
},
created: function () {
// show current item
this.$emit("open", this.itemId);
},
methods: {
toggleBody() {
if (this.itemId === this.showIndex) {
// dont show anything
this.$emit("open", -1);
} else {
// show this one
this.$emit("open", this.itemId);
}
},
}
};
FormTemplate.vue:
<CollapsableGroupItem
:context="context"
:show-index="showIndex"
:group-item="educations[context.index]"
#open="showIndex = $event"
>
<template v-slot:title="education">
<span v-if="education.institution || education.degree"
>
{{ education.institution }}
<span v-if="education.institution && education.degree">at</span>
{{ education.degree }}
</span>
...
</template>
<template v-slot:body>
...
</template>
</CollapsableGroupItem>
Maybe it will help someone else or will be useful 😀

Logo to the left, form to the right

I am using Bulma and Vue, and I am trying to create a header for the site that consists of a logo on the left and a login form on the right.
This gives me a logo on the left, and then from the end of the logo until the end of the screen on the right, I have the elements shown there.
How do I do what I want? Thanks.
Template
<header>
<div class="navbar">
<a class="navbar-brand" href="/">FreeSongsâ„¢</a>
<form class="navbar-menu" #submit.prevent="signin" accept-charset="utf-8" autocomplete="on">
<div class="field-body ">
<FormField type="email" required="required" :tabindex="1" placeholder="Email" name="login[email]" autocomplete="email" v-model="stageName" v-validate="'required'" autocapitalize="off" autofocus="autofocus"></FormField>
<FormField type="password" required="required" :tabindex="2" placeholder="Password" name="login[password]" autocomplete="current-password" v-model="email" v-validate="'required|email'"></FormField>
<button class="button is-success" tabindex="3" type="submit" id="signin">Sign in</button>
<a class="btn btn-link" tabindex="4" href="/forgot">Forgot password?</a>
</div>
</form>
</div>
</header>
FormField Component
<template>
<div class="field">
<label v-if="label" class="label" :for="id">{{label}}</label>
<input :type="type" class="input" :class="{'is-danger':this.$validator.errors.has(label)}" :tabindex="tabindex" :name="name" :id="id" :autocomplete="autocomplete" :value="value" #input="updateValue" #change="updateValue" #blur="$emit('blur')" :disabled="disabled" :required="required" :placeholder="placeholder" />
<span v-show="this.$validator.errors.has(label)" class="subtitle is-6 has-text-danger">{{ this.$parent.errors.first(label) }}</span>
</div>
</template>
<script>
export default {
name: "FormField",
//inject: ['$validator'],
inject: {
$validator: '$validator'
},
$_veeValidate: {
name() {
return this.label;
},
// fetch the current value from the innerValue defined in the component data.
value() {
return this.value;
}
},
props: {
value: String,
placeholder:String,
id: {
type: String,
default: () => {
const rand = Math.floor((Math.random() * 10000) + 1); //TODO: Create enough margin so there won't be a chance it has the same ID as other elemnts. Change the method?
const id = `undefined_${Date.now()*rand}`; //${this._uid}
return id;
}
},
label: {
type: String,
required: false
},
type: {
type: String,
default: "text"
},
name: {
type: String,
required: true
},
autocomplete: {
type: String,
required: false
},
disabled: {
type: Boolean,
default: false
},
required:{
type:Boolean,
default:false
},
tabindex:{
type:Number
},
autocapitalize:{
type:String,
},
autofocus:{
type:Boolean
}
},
computed: {
},
created: function() {
console.log("Created");
},
mounted: function() {
console.log("Mounted");
},
methods: {
updateValue(e) {
this.$emit("input", e.target.value);
}
}
};
</script>
The documentation outlines how to do this:
https://bulma.io/documentation/components/navbar/
First, the navbar is split into two.
|navbar-brand|navbar-menu|
navbar-brand will always show on the left, the navbar-menu fills the rest of the space on the right.
Inside the navbar-menu, you can specify which side items will show with two more elements.
|navbar-start|navbar-end|
<nav class="navbar">
<div class="navbar-brand">
This is on the left of the bar.
</div>
<div class="navbar-menu">
This spans the rest of the space on the right of the bar.
<div class="navbar-start">
This is on the left.
<div class="navbar-item">Your items on the left</div>
</div>
<div class="navbar-end">
This is on the right.
<div class="navbar-item">Your items on the right</div>
</div>
</div>
</nav>