Dynamic editable columns with vuetify data table - vue.js

I am trying to make a vue data table component, I want to make it so that if you pass an array of column names, the values in those columns will be editable like in this example. I am currently trying to loop through the array prop passed to the component like as so...
<template
v-for="(column, index) in editableColumns"
v-slot:[getEditableColumn(column)]="props"
>
<v-edit-dialog
:return-value.sync="props.item.desk"
#save="save"
#cancel="cancel"
#open="open"
#close="close"
:key="index"
>
{{ props.item.desk }}
<template v-slot:input>
<v-text-field
v-model="props.item.desk"
label="Edit"
single-line
counter
></v-text-field>
</template>
</v-edit-dialog>
</template>
the get editable columns operates as such:
getEditableColumn(column) {
console.log(column);
return `item.${column}`;
},
it basically returns the value of the column I want to be editable as such item.columnName but the function never runs, I do have to mention that if I pass directly the name like this without a for loop it works, but I want this to work dynamically as I will use the table in multiple places with different column names, and I don't want to make them all columns editable editable.
Below I have attached the full code of the component.
<template>
<v-card>
<v-card-title>
{{ title || "" }}
<v-col>
<v-btn icon color="black" v-if="refresh">
<v-icon>mdi-refresh</v-icon>
</v-btn>
<v-btn icon color="black" v-if="exportExcel" #click="exportToXlsx">
<v-icon>mdi-microsoft-excel</v-icon>
</v-btn>
<v-btn icon color="black" v-if="exportPdf">
<v-icon>mdi-file-pdf-box</v-icon>
</v-btn>
<v-btn icon color="black" v-if="fontSizeControlls">
<v-icon>mdi-format-font-size-decrease</v-icon>
</v-btn>
<v-btn icon color="black" v-if="fontSizeControlls" #click="logSelected">
<v-icon>mdi-format-font-size-increase</v-icon>
</v-btn>
</v-col>
<v-spacer></v-spacer>
<v-col v-if="searchBar">
<v-text-field
v-model="search"
append-icon="mdi-magnify"
label="Search"
outlined
dense
></v-text-field>
</v-col>
</v-card-title>
<v-data-table
#input="(selected) => $emit('selected', selected)"
#click:row="rowClickFunction"
v-model="selected"
:headers="headers"
:items="data"
:search="search"
:show-select="showSelect"
:single-select="singleSelect"
:height="height"
:items-per-page="itemsPerPage"
:item-key="itemKey"
dense
>
<!-- Pass template elements inside the component call to render custom components inside the table -->
<template
v-for="slot in Object.keys($scopedSlots)"
:slot="slot"
slot-scope="scope"
>
<slot :name="slot" v-bind="scope" />
</template>
<template
v-for="(column, index) in editableColumns"
v-slot:[getEditableColumn(column)]="props"
>
<v-edit-dialog
:return-value.sync="props.item.desk"
#save="save"
#cancel="cancel"
#open="open"
#close="close"
:key="index"
>
{{ props.item.desk }}
<template v-slot:input>
<v-text-field
v-model="props.item.desk"
label="Edit"
single-line
counter
></v-text-field>
</template>
</v-edit-dialog>
</template>
</v-data-table>
</v-card>
</template>
<script>
import { utils, writeFile } from "xlsx";
export default {
props: {
headers: Array,
data: Array,
title: String,
height: String,
itemsPerPage: Number,
itemKey: String,
searchBar: { tpye: Boolean, default: false },
rowClickFunction: {
type: Function,
default: () => {},
},
editableColumns: {
type: Array,
},
refresh: {
type: Boolean,
default: false,
},
exportExcel: {
type: Boolean,
default: false,
},
exportPdf: {
type: Boolean,
default: false,
},
fontSizeControlls: {
type: Boolean,
default: false,
},
singleSelect: {
type: Boolean,
default: false,
},
showSelect: {
type: Boolean,
default: false,
},
xlsxName: {
type: String,
default: "Sheet.xlsx",
},
},
data() {
return {
search: "",
selected: [],
dialog: false,
};
},
methods: {
exportToXlsx() {
const worksheet = utils.json_to_sheet(this.data);
const workbook = utils.book_new();
utils.book_append_sheet(workbook, worksheet, "Data");
writeFile(workbook, this.xlsxName);
},
getEditableColumn(column) {
console.log(column);
return `item.${column}`;
},
logSelected() {
console.log(this.selected);
},
logRow(row) {
console.log(row);
console.log(this.selected);
},
getSlotName(slot) {
return `${slot}.slotName`;
},
save() {
this.snack = true;
this.snackColor = "success";
this.snackText = "Data saved";
},
cancel() {
this.snack = true;
this.snackColor = "error";
this.snackText = "Canceled";
},
open() {
this.snack = true;
this.snackColor = "info";
this.snackText = "Dialog opened";
},
close() {
console.log("Dialog closed");
},
},
};
</script>

The slot name must be one of the headers element's value

Related

How do I capture the value of the prop in the text field?

I have a prop and currently am able to get the data of the prop, Am trying a way to capture the item of the prop when saving the form.
Is there a way where i can take the value and pass if in a hidden text-area and bind the data to the vmodel?
Any help I appreciate.
<v-dialog v-model="dialog" persistent max-width="800">
<template v-slot:activator="{ on }">
<v-btn dark v-on="on" color="primary" round> Make payment </v-btn>
</template>
<v-card>
<v-card-title class="headline primary">
<span class="white--text">Add a new Doctor Payment Record {{ queueId }}</span>
<v-btn icon dark #click.native="dialog = false" absolute right>
<v-icon>mdi-close</v-icon>
</v-btn>
</v-card-title>
<v-card-text>
<users-search
:leave-selected="true"
idOnly
label="Select Doctor"
#results="setDoctor"
>
</users-search>
<div class="row px-3">
<v-autocomplete
class="px-3 col-sm-8"
v-model="expense.bank"
v-if="banks.data"
:items="banks.data"
outline
chips
label="Select bank"
item-text="name"
item-value="id"
>
</v-autocomplete>
<v-text-field
class="px-3 col-sm-8"
outline
flat
v-model="expense.amount"
type="number"
#input="expense.percentage()"
required
label="Amount *"
persistent-hint
/>
</div>
<v-text-field
class="px-3"
outline
flat
v-model="expense.total_paid"
required
label="amount paid"
persistent-hint
/>
<v-text-field
class="px-3"
outline
flat
:value="setQueue"
v-model="expense.queueId"
required
:label=queueId
persistent-hint
/>
<v-alert :value="true" type="error" v-if="errors.any()">
<div v-html="errors.display()"></div>
</v-alert>
<v-layout row wrap>
<v-flex xs12>
<v-btn
color="success"
:loading="saveLoader"
#click="recordExpense()"
>save</v-btn
>
</v-flex>
</v-layout>
</v-card-text>
</v-card>
</v-dialog>
</template>
<script>
import NewUser from "#finance/libs/users/NewUser";
import {mapActions, mapGetters} from "vuex";
export default {
props: [
'queueId'
],
data: () => ({
dialog: false,
expense: new NewUser(),
saveLoader: false,
}),
computed: {
...mapGetters({
banks: "getBanks",
}),
balance: function () {
return parseFloat(10);
},
submitted() {
return this.expense.form.submitted;
},
contaminated() {
return this.expense.form.errorDetected;
},
errors() {
return this.expense.form.errors;
},
},
watch: {
submitted(v) {
if (v) {
this.saveLoader = false;
}
},
contaminated() {
this.saveLoader = false;
},
},
methods: {
...mapActions({
fetchBanks: "setBanks",
}),
setDoctor(user) {
this.expense.doctor_id = user.id;
},
setQueue(){
console.log(this.queueId);
this.expense.queueId = this.queueId;
},
async recordExpense() {
this.saveLoader = true;
let response = await this.expense.saveExpense();
this.saveLoader = false;
if (response) {
this.dialog = false;
this.$emit("expenseCreated");
}
},
},
mounted() {
this.fetchBanks();
}
};
</script>
The prop queueId i also want to store it along with the user information from the form.
Try this one, it should work:
<template>
<textarea v-model="hiddenValue" :style="{ display: 'none' }"></textarea>
</template>
<script>
export default {
props: [ 'queueId' ],
data() {
return {
hiddenValue: this.queueId
}
}
}
</script>
In case you will no need the prop to be modified, please bind the texarea value to the prop directly:
<textarea hidden v-model="queueId" :style="{ display: 'none' }></textarea>

Is there any way to pass props to component without calling it

I have 3 components like: Parent, First-child and Second-child. And I am iterating First-child in Parent component in array(it is cards), and I want to call Second-child in Parent component with First-child's props(props of one card).
My Parent component looks like this(how I am calling First-child):
``
<CardComponent
v-for="card of cards"
:key="card.urlsId"
:cardImages="card.images"
:cardTitle="card.title"
:cardDescription="card.description"
:mediaRef="card.urlsId"
:dbRef="card.dbId"
:deleteBtn="true"
:imagesWithSlider="true"
/>
And my First child is:
<template>
<div class="cards">
<v-card class="card-container">
<div class="delete-btn">
<v-btn
v-if="deleteBtn"
class="mx-2"
fab
dark
small
#click="$emit('onOpenDeleteModal')"
>
<v-icon dark> mdi-delete </v-icon>
</v-btn>
</div>
<ImageSlider
v-if="imagesWithSlider"
:imagesArray="cardImages"
:arrowBtns="false"
/>
<div class="text-container">
<h3 class="card-title">{{ cardTitle }}</h3>
<p class="card-description">{{ cardDescription }}</p>
</div>
</v-card>
</div>
</template>
<script>
export default {
props: {
cardImages: {
type: Array,
default: null,
},
cardTitle: {
type: String,
default: 'Title',
},
cardDescription: {
type: String,
default: 'Description',
},
deleteBtn: {
type: Boolean,
},
imagesWithSlider: {
type: Boolean,
},
mediaRef: {
type: String,
default: '',
},
dbRef: {
type: String,
default: '',
},
deleteModalOpen: {
type: Boolean,
},
},
emits: ['onOpenDeleteModal', 'onCloseDeleteModal'],
}
</script>
And my Second-child is:
<template>
<v-card class="modal" :loading="newCard.loading ? true : false">
<v-card class="modal-header">
<h3 v-if="addCardModal" class="header-title">New card</h3>
<h3 v-if="deleteModal" class="header-title">Delete</h3>
<v-icon aria-hidden="false" width="100%" #click="$emit('closeModal')"
>mdi-close</v-icon
>
</v-card>
<!-- Delete Modal -->
<div v-if="deleteModal" class="modal-delete">
<h3>Are you really want to delete this card ?</h3>
<div class="modal-delete-btns">
<v-btn #click="$emit('closeModal')">Cancel</v-btn>
<v-btn color="error" #click="$emit('onDeleteCard')">Delete</v-btn>
</div>
</div>
<!-- Add New Card Modal -->
<form
v-if="addCardModal"
class="modal-container"
#submit.prevent="postNewCardToDb"
>
<v-file-input
v-model="newCard.cardImages"
:clearable="false"
multiple
show-size
label="Upload card images"
#change="previewImage"
>
</v-file-input>
<v-file-input
v-model="newCard.cardVideo"
:clearable="false"
show-size
label="Upload video"
>
</v-file-input>
<div v-if="newCard.cardImageUrls.length !== 0" class="preview-image">
<ImageSlider :imagesArray="newCard.cardImageUrls" :arrowBtns="true" />
</div>
<v-text-field
v-model="newCard.cardTitle"
label="Enter card title"
></v-text-field>
<v-text-field
v-model="newCard.cardSnippet"
label="Enter card description"
></v-text-field>
<v-btn type="submit" :loading="newCard.loading ? true : false" block
>POST</v-btn
>
</form>
</v-card>
</template>
<script>
import { v4 as uuidV4, v1 as uuidV1 } from 'uuid'
export default {
/* eslint-disable no-console */
props: {
addCardModal: {
type: Boolean,
},
deleteModal: {
type: Boolean,
},
},
emits: ['closeModal', 'onDeleteCard'],
data() {
return {
newCard: {
loading: false,
cardImages: [],
cardVideo: null,
cardImageUrls: [],
cardTitle: '',
cardSnippet: '',
},
}
},
methods: {
previewImage($event) {
for (const image of event.target.files) {
this.newCard.cardImageUrls.push(URL.createObjectURL(image))
}
},
async getMediaUrlsFromStorage(newCardData) {
const cardMediaRef = uuidV1()
const cardImagesRef = await this.$fire.storage
.ref('/albums_cards/')
.child(cardMediaRef)
const videoRef = await cardImagesRef.child(uuidV4())
if (this.newCard.cardVideo) {
await videoRef.put(this.newCard.cardVideo)
const videoUrl = await videoRef.getDownloadURL()
newCardData.video = videoUrl
}
newCardData.urlsId = cardMediaRef
const promiseArr = this.newCard.cardImages.map(async (image) => {
const imageRef = cardImagesRef.child(uuidV4())
await imageRef.put(image)
const imageUrl = await imageRef.getDownloadURL()
newCardData.images.push(imageUrl)
})
await Promise.all(promiseArr)
},
async postNewCardToDb() {
this.newCard.loading = true
const newCardData = {
urlsId: '',
title: this.newCard.cardTitle,
description: this.newCard.cardSnippet,
video: '',
images: [],
}
await this.getMediaUrlsFromStorage(newCardData)
await this.$fire.database.ref('albums/cards').push(newCardData)
console.log(newCardData)
this.newCard.loading = false
this.newCard.cardTitle = null
this.newCard.cardSnippet = null
this.newCard.cardImages = []
this.newCard.cardImageUrls = []
this.newCard.cardVideo = null
},
},
}
</script>
First-child is a card component and I need to pass props of each card to Second-child without calling it. I cant call Second-child in First-child because of iteration.
I hope I expleined it well

Add/Edit list item at parent from child component form

I am developing a recipe app. At my CreateRecipe component, I have child component to add ingredients to the recipe or edit existing ingredients. Ill start by showing the code and what i want to achieve and then the problem
Parent component:
<template>
...
<v-dialog v-model="AddIgredientsDialog" max-width="800px">
<template v-slot:activator="{ on, attrs }">
<v-btn color="secondary" dark v-bind="attrs" v-on="on">
Add addIngredients
</v-btn>
</template>
<AddItemsForm
#addIngredient="SaveNewIgredient"
:newIngredientsItem="editedIgredient"
/>
</v-dialog>
</template>
<script>
import AddItemsForm from "./AddItemsForm"; //Child Component
data: () => ({
AddIgredientsDialog:false,
article: {
headline: "",
img: "",
content: "",
subHeader: "",
description: "",
igredients: [], //List to add/edit item at AddItemsForm child component
preperation: []
},
editedIgredient: { //Item to use for editing or adding new item to article.igredients
title: "",
subIgredients: []
},
defaultItem: { //Item used for resetting editedIgredient item
title: "",
subIgredients: []
},
editedIndex: -1, helper variable for knowing whether i need to add new item or edit exiting item
}),
methods:{
editIngredients(item) {
this.editedIndex = this.article.igredients.indexOf(item);
this.editedIgredient = Object.assign({}, item);
this.AddIgredientsDialog = true;
},
SaveNewIgredient(newItem) { //Triggered on #click of save button at child component New item is the
//item passed from children
if (this.editedIndex > -1) {
this.editedIgredient = Object.assign({}, newItem);
Object.assign(
this.article.igredients[this.editedIndex],
this.editedIgredient
);
} else {
this.article.igredients.push(this.editedIgredient);
}
this.AddIgredientsDialog = false;
this.$nextTick(() => {
this.editedIgredient = Object.assign({}, this.defaultItem);
this.editedIndex = -1;
});
},
}
</script>
Child Component:
<template>
<v-card>
<v-card-title>
<span class="headline">Add Ingredients</span>
</v-card-title>
<v-card-text>
<v-text-field v-model="newIngredientsItem.title" placeholder="כותרת">
</v-text-field>
<v-row align="center">
<v-col sm="11">
<v-text-field
v-model="newIgredient"
placeholder="New Igredient"
#keyup.enter="addNewIgredient"
>
</v-text-field>
</v-col>
<v-col sm="1">
<v-btn icon #click="addNewIgredient">
<v-icon>
mdi-plus
</v-icon>
</v-btn>
</v-col>
<v-col class="mt-0 pt-0" cols="12">
<v-row no-gutters>
<v-col cols="12">
<v-card flat tile>
<template
v-for="(item, index) in newIngredientsItem.subIgredients"
>
<v-list-item :key="index" class="mr-0 pr-0">
<v-list-item-content>
<v-list-item-title>
<v-edit-dialog #click.native.stop>
{{ item }}
<v-text-field
slot="input"
v-model="newIngredientsItem.subIgredients[index]"
></v-text-field>
</v-edit-dialog>
</v-list-item-title>
</v-list-item-content>
<v-list-item-action>
<v-btn icon #click="removeIgredient(index)">
<v-icon small>
mdi-delete
</v-icon>
</v-btn>
</v-list-item-action>
</v-list-item>
<v-divider
v-if="index + 1 < newIngredientsItem.subIgredients.length"
:key="item + index"
></v-divider>
</template>
</v-card>
</v-col>
</v-row>
</v-col>
</v-row>
</v-card-text>
<v-card-actions>
<v-btn color="primary" #click="AddIngredients">
Save
</v-btn>
</v-card-actions>
</v-card>
</template>
<script>
export default {
props: {
newIngredientsItem: {
type: Object,
default() {
return {
title: "",
subIgredients: [ ]
};
}
}
},
data: () => ({
newIgredient: ""
}),
methods: {
addNewIgredient() {
this.newIngredientsItem.subIgredients.push(this.newIgredient);
this.newIgredient = "";
},
AddIngredients() {
this.$emit("addIngredient", this.newIngredientsItem);
},
removeIgredient(index) {
this.newIngredientsItem.subIgredients.splice(index, 1);
}
}
};
</script>
My Problem:
At the moment im only trying to use the SaveNewIgredient() method.
After 1st time of adding item the item is added as it should and the parent defaultItem state remain as is which is good:
defaultItem: {
title: "",
subIgredients: []
},
After adding a second item the defaultItem changes and takes the editedItem properties.
For example if i add at the second time
{
title:'Test 1',
subIgredients: [
'Test 1 - 1',
'Test 1 - 2',
'Test 1 - 3',
]
}
That is what the defaultItem will be and then this assignment causes a bug
this.editedIgredient = Object.assign({}, this.defaultItem);
because editedItem should be:
{
title: "",
subIgredients: []
}
I tried to solve your problem. To do this I modified and in some places simplified your code to keep only what was close to the SaveNewIgredient() function. So here is my code.
Parent Component (for me App.vue):
<template>
<AddItemsForm #addIngredient="SaveNewIgredient" />
</template>
<script>
import AddItemsForm from "./AddItemsForm"; //Child Component
export default {
name: "App",
components: { AddItemsForm },
data() {
return {
article: {
igredients: [], //List to add/edit item at AddItemsForm child component
},
editedIgredient: {
//Item to use for editing or adding new item to article.igredients
title: "",
subIgredients: [],
},
defaultItem: {
//Item used for resetting editedIgredient item
title: "",
subIgredients: [],
},
};
},
methods: {
SaveNewIgredient(newItem) {
console.log("Received: ", newItem);
this.editedIgredient = newItem;
this.article.igredients.push({ ...this.editedIgredient });
console.log("defaultClear: ", this.defaultItem);
console.log("infoItem: ", this.editedIgredient);
this.editedIgredient = this.defaultItem;
console.log("defaultClear: ", this.defaultItem);
console.log("editedWillClear: ", this.editedIgredient);
console.log("listFinal: ", this.article.igredients);
},
},
};
</script>
Child Component (for me AddItemsForm.vue):
<template>
<div>
<input v-model="newIgredient" placeholder="New Igredient" />
<button #click="addNewIgredient">ADD</button>
<div>
<button color="primary" #click="AddIngredients">Save</button>
</div>
</div>
</template>
<script>
export default {
props: {
IngredientsItem: {
type: Object,
default() {
return {
title: "",
subIgredients: [],
};
},
},
},
data() {
return {
newIgredient: "",
title: "TEST",
titleNbr: 0,
resetIgredient: false,
};
},
computed: {
newIngredientsItem() {
return this.IngredientsItem;
},
},
methods: {
addNewIgredient() {
if (this.resetIgredient === true) {
this.newIngredientsItem.subIgredients = [];
}
this.newIngredientsItem.subIgredients.push(this.newIgredient);
this.newIgredient = "";
this.resetIgredient = false;
console.log("ADD: ", this.newIngredientsItem.subIgredients);
},
AddIngredients() {
this.newIngredientsItem.title = this.title + this.titleNbr;
this.titleNbr++;
console.log("EMIT: ", this.newIngredientsItem);
this.$emit("addIngredient", this.newIngredientsItem);
this.resetIgredient = true;
},
},
};
</script>

Vuetify data table in single-select mode, selecting one row selects all others

I am trying to select one row in the table and emit the selected item.
Selecting one selects all but only the first encountered object is saved to the model (as selected variable).
Do you have any ideas, what i'm doing wrong?
<template>
<v-data-table
:headers="headers"
:items="items"
:search="search"
:loading="loading"
v-model="selected"
single-select
show-select
:options="{itemsPerPage:5}"
#item-selected="itemSelected"
>
<template v-slot:top>
<v-toolbar flat>
<v-text-field
v-model="search"
append-icon="mdi-magnify"
label="Search"
single-line
hide-details
></v-text-field>
</v-toolbar>
</template>
<template v-slot:item.name="{ item }">{{ item.name }}</template>
</v-data-table>
</template>
<script>
export default {
name: "variable-selector",
props: ["variables", "map", "index"],
data() {
return {
search: "",
selected: {},
loading: false,
items: [],
headers: [{ text: "Variable name", value: "name", sortable: true }]
};
},
methods: {
itemSelected(selection) {
if (selection.value) {
this.$emit("selected", selection.item); // it always emits var_2 object
} else {
this.$emit("selected", null);
}
},
updateItemsList(variables) {
this.items = Array.from(variables);
}
},
mounted() {
this.updateItemsList(this.variables);
},
watch: {
variables(newValue) {
this.loading = true;
this.updateItemsList(newValue);
this.loading = false;
}
}
};
</script>
Each object should be unique key value if u face error , you want manually tell each object is unique
just add
item-key="table_header_index"//or name
eg:
<v-data-table
:headers="headers"
:items="items"
show-select
item-key="table_header_index" <-------------------add this line
>
</v-data-table>
I had this issue and the realized my item-key="value" did not match any of the values in my header. Select one of your header values and it should work.
From the example in the docs I can see the following:
1.) selected should be an array, not an object
Selected holds all selected values. single-select property just determines if the length can be bigger than 1.
2.) if you use v-model you should not use #item-selected="itemSelected"
v-model is already 2 way binding. but you trigger an additional event and override the model (which should be an array) with an object or null
Solution
Make selected an array and remove #item-selected="itemSelected".
<template>
<v-data-table
:headers="headers"
:items="items"
:search="search"
:loading="loading"
v-model="selected"
single-select
show-select
:options="{itemsPerPage:5}"
>
<template v-slot:top>
<v-toolbar flat>
<v-text-field
v-model="search"
append-icon="mdi-magnify"
label="Search"
single-line
hide-details
></v-text-field>
</v-toolbar>
</template>
<template v-slot:item.name="{ item }">{{ item.name }}</template>
</v-data-table>
</template>
<script>
export default {
name: "variable-selector",
props: ["variables", "map", "index"],
data() {
return {
search: "",
selected: [],
loading: false,
items: [],
headers: [{ text: "Variable name", value: "name", sortable: true }]
};
},
methods: {
updateItemsList(variables) {
this.items = Array.from(variables);
}
},
mounted() {
this.updateItemsList(this.variables);
},
watch: {
variables(newValue) {
this.loading = true;
this.updateItemsList(newValue);
this.loading = false;
}
}
};
</script>
your data from back should have primary key. if it does it your table understand it; and if your data doesn't have primary key should write item-key for table.
good luck

Grabbing data from multiple child components Vue js

I'm breaking my head for a few days now, trying to figure out how to grab the data from child components.
Situation is like this.
I have one parent component called Post where user can select date, title, description and which can contain multiple instances of Event compontents.
Event component contains fields like title, description, attendees.
User should be able to add multiple Eventcomponents which means I have multiple components Event within the Post component.
So, I can't figure out how can I compose my components to have an array of Event objects inside my Post component which I can later on send to my API.
the structure of the post object I need is:
// Post.vue
{
"date": '',
"name": '',
"description": '',
"events": {
{
"title": '',
"description": '',
"attendees": ''
},
{
"title": '',
"description": '',
"attendees": ''
}
}
}
So, I don't know should and how I would use vuex for it. I've tried using $emit to pass the data but I couldn't find it fit to get the data into Post model.
Can someone point me where should I look for it?
EDIT #1: Added sample code
The code for the components:
<template>
<v-form>
<v-container>
<v-row>
<v-col
cols="12"
md="4"
>
<v-date-picker v-model="post.date" scrollable>
<v-spacer />
<v-btn text color="primary" #click="modal = false">
Cancel
</v-btn>
<v-btn text color="primary" #click="$refs.dialog.save(date)">
OK
</v-btn>
</v-date-picker>
</v-col>
<v-col
cols="12"
md="4"
>
<v-text-field
v-model="post.name"
label="name"
required
/>
</v-col>
<v-col
cols="12"
md="4"
>
<v-textarea
v-model="post.description"
name="description"
label="Description"
dense
value
rows="4"
hint
/>
</v-col>
</v-row>
<v-row>
<v-btn primary rounded #click="addLine">
Add Event
</v-btn>
<v-expansion-panels accordion>
<UserEvent
v-for="(line, index) in lines"
:key="index"
#addLine="addLine"
#removeLine="removeLine(index)"
/>
</v-expansion-panels>
</v-row>
</v-container>
</v-form>
</template>
<script>
import UserEvent from './partials/event'
export default {
name: 'Post',
components: { UserEvent },
data () {
return {
post: [],
lines: [],
blockRemoval: true
}
},
watch: {
lines () {
this.blockRemoval = this.lines.length <= 1
}
},
mounted () {
},
methods: {
addLine () {
const checkEmptyLines = this.lines.filter(line => line.number === null)
if (checkEmptyLines.length >= 1 && this.lines.length > 0) { return }
this.lines.push({
title: null,
description: null,
attendees: null
})
},
removeLine (lineId) {
if (!this.blockRemoval) { this.lines.splice(lineId, 1) }
}
}
}
</script>
And the child component UserEvent
// UserEvent.vue
<template>
<v-expansion-panel>
<v-expansion-panel-header>Event details</v-expansion-panel-header>
<v-expansion-panel-content>
<v-row>
<v-col cols="12" md="6">
<v-text-field
v-model="event.title"
label="Title"
required
/>
</v-col>
<v-col
cols="12"
md="6"
>
<v-text-field
v-model="event.atttendees"
label="Atendees"
required
/>
</v-col>
<v-col
cols="12"
md="12"
>
<v-textarea
v-model="event.description"
name="description"
label="Description"
dense
value
rows="4"
hint
/>
</v-col>
<v-col
cols="12"
md="3"
>
<div class="block float-right">
<v-btn #click="removeLine(index)" />
<v-btn v-if="index + 1 === lines.length" #click="addLine" />
</div>
</v-col>
</v-row>
</v-expansion-panel-content>
</v-expansion-panel>
</template>
<script>
export default {
name: 'UserEvent',
props: ['line', 'index'],
data () {
return {
event: []
}
},
methods: {
addLine () {
this.$emit('addLine')
},
removeLine (index) {
this.$emit('removeLine', index)
}
}
}
</script>
Here's an example with a similar structure what was posed in the question:
{
name: String,
events: [
title: String,
description: String,
],
}
This example allows the user to open a form to add a new event. When that form is submitted, the event data is added to the parent component's state.
Parent
<template>
<div>
<input v-model="name" />
<ul v-if="events.length">
<li v-for="(event, index) in events" :key="index">
<span>{{ event.title }}</span>
<span>{{ event.description }}</span>
</li>
</ul>
<Event v-if="isNewEventFormVisible" #submit="addEvent" />
<button v-else #click="showNewEventForm">add event</button>
</div>
</template>
import Event from '~/components/Event';
export default {
components: { Event },
data() {
return {
name: 'Example Post',
events: [],
isNewEventFormVisible: false,
};
},
methods: {
addEvent({ title, description }) {
this.isNewEventFormVisible = false;
this.events.push({ title, description });
// TODO: call you API here to update
},
showNewEventForm() {
this.isNewEventFormVisible = true;
},
},
};
Event
<template>
<form #submit.prevent="onSubmit">
<input v-model.trim="title" type="text" />
<br />
<textarea v-model.trim="description" />
<button type="submit">submit</button>
</form>
</template>
export default {
data() {
return {
title: '',
description: '',
};
},
methods: {
onSubmit() {
this.$emit('submit', {
title: this.title,
description: this.description,
});
},
},
};
You could imagine a more sophisticated version of this where events are editable. In that case, each Event could take props and bind them as values to its input instead of using v-models.