I am creating a table with alpinejs, it should returns new row if someone hit "click to add" link
I have this code:
<div class="mt-2" x-data="services()">
<tbody class="bg-gray-200" x-model="newService" x-show="services.length">
<template x-for="service in services" :key="service.id">
<td x-text="services.length" >
<a #click="addService()">Click to add more</a>
<script>
function services() {
return {
services: [],
,
newService: '',
addService() {
this.services.push({
id: this.services.length + 1,
body: this.newService,
completed: false
});
this.newService = '';
},
deleteService(service){
let position = this.services.indexOf(service);
this.services.splice(position, 1);
}
}
}
</script>
but instead of returning a new row it returns 23 rows at ones!!
how can I fix that?
Adding to the existing answer, here is a simple working example.
<link href="https://unpkg.com/tailwindcss#^1.0/dist/tailwind.min.css" rel="stylesheet">
<script src="//unpkg.com/alpinejs" defer></script>
<div class="mt-4 flex flex-col max-w-lg mx-auto" x-data="services()">
<div class="flex space-x-2">
<input type="text" x-model="newService" class="px-2 py-1 border flex-1 rounded" placeholder="Service">
<button class="border bg-blue-600 hover:bg-blue-700 text-white rounded px-4 py-1" #click="addService()">Add
service</button>
</div>
<div x-show="services.length">
<div class="rounded border mt-4 text-xs">
<table class="w-full">
<thead>
<tr>
<th class="bg-gray-100 font-normal text-gray-500 uppercase py-2">Services</th>
</tr>
</thead>
<tbody>
<template x-for="service in services" :key="service.id">
<tr class="border-t">
<td class="flex justify-between items-center px-4 py-1">
<span x-text="service.body"></span>
<button class="text-sm uppercase text-red-500 px-2 py-1 rounded hover:bg-red-100"
#click="deleteService(service.id)">x</button>
</td>
</template>
</tbody>
</table>
</div>
<button class="text-xs w-full mt-2 bg-red-100 text-red-400 px-2 py-2 rounded hover:bg-red-200" #click="services = []">Delete
All</button>
</div>
</div>
<script>
function services() {
return {
services: [{
id: 1,
body: 'Service 1',
completed: false
},
{
id: 2,
body: 'Service 2',
completed: false
}
],
newService: '',
addService() {
if (this.newService.length < 1) return;
this.services.push({
id: Date.now(),
body: this.newService,
completed: false
});
this.newService = '';
},
deleteService(serviceId) {
let position = this.services.findIndex(el => el.id == serviceId);
this.services.splice(position, 1);
}
}
}
</script>
With limited code and without a working example it is bit hard to give a concrete answer. By looking at the code, I think you are getting the existing number of rows in your table body.
addService() {
this.services.push({
id: this.services.length + 1,
body: this.newService,
completed: false
});
this.newService = '';
}
In this method you are selecting this.newService as your body. And looking at your HTML <tbody class="bg-gray-200" x-model="newService" x-show="services.length"> newService is the entire body of the existing table.
So you are getting the existing 23 rows.
Related
I'm relatively new to Vue JS. So I have this case on Form Submit.
This is what I've submitted in Console Log:
{
amount: [
monday: "123",
tuesday: "97438"
],
day_of_week: "perday",
started_at: "2022-09-14"
}
And what it received in Laravel Backend when I dump the data is this:
^ array:3 [
"started_at" => "2022-09-14"
"day_of_week" => "perday"
"amount" => []
]
So My Question Is, what did I do wrong? How is the "amount" part not included on the backend? It already looks right on the console.log. I do appreciate help here. I reckon I did a mistake on how to write the model name in the array part inside HTML, but still, I do not know how to do it right.
THANK YOU IN ADVANCE 🙏🏼🙏🏼🙏🏼
This is how I submit my form:
HTML Vue Page:
<div class="card-body p-9">
<div class="row mb-8">
<div class="col-xl-3">
<div class="fs-6 fw-semibold mt-2 mb-3">Type of Rate</div>
</div
<div class="col-xl-9 fv-row">
<VueMultiselect v-model="day_of_week" label="name" track-by="name" placeholder="Select Type" :options="type_of_day" :close-on-select="true">
<template slot="singleLabel" slot-scope="{ type_of_day }"><strong>{{ type_of_day.name }}</strong></template>
</VueMultiselect>
</div>
</div>
<div class="row mb-8">
<div class="col-xl-3">
<div class="fs-6 fw-semibold mt-2 mb-3">Started Date</div>
</div
<div class="col-xl-9 fv-row">
<div class="position-relative d-flex align-items-center">
<input v-model="ticketing.started_at" class="form-control form-control-solid ps-12" name="date" placeholder="Pick Start date" id="kt_datepicker_1" />
</div>
</div>
</div>
<div v-if="day_of_week">
<div v-if="day_of_week.value == 'allday'" class="row mb-8">
<div class="col-xl-3">
<div class="fs-6 fw-semibold mt-2 mb-3">Entrance Rate</div>
</div>
<div class="col-xl-9 fv-row">
<div class="input-group mb-5">
<span class="input-group-text">Rp.</span>
<input v-model="amount.allday" type="text" class="form-control" aria-label="Amount"/>
</div>
</div>
</div>
<div v-if="day_of_week.value == 'perday'" class="row mb-8">
<div class="col-12 mb-8">
<div class="fs-6 fw-bold mt-2 mb-3">Entrance Rate Per Day of Week</div>
</div>
<div class="col-xl-3">
<div class="fs-6 fw-semibold mt-2 mb-3">Monday</div>
</div>
<div class="col-xl-9 fv-row">
<div class="input-group mb-5">
<span class="input-group-text">Rp.</span>
<input v-model="amount.day.monday" type="text" class="form-control" aria-label="Amount"/>
</div>
</div>
<div class="col-xl-3">
<div class="fs-6 fw-semibold mt-2 mb-3">Tuesday</div>
</div>
<div class="col-xl-9 fv-row">
<div class="input-group mb-5">
<span class="input-group-text">Rp.</span>
<input v-model="amount.day.tuesday" type="text" class="form-control" aria-label="Amount"/>
</div>
</div>
</div>
</div>
</div>
<div class="card-footer d-flex justify-content-end pb-6 px-9">
<span #click="storeNewRate" class="btn btn-primary">Create New Rate</span>
</div>
</template>
This Is the script Part
<script>
import { onMounted, toRaw } from "vue";
import useTicketing from "../../../composable/ticketing";
export default {
data() {
const { storeTicketing, ticketing } = useTicketing()
const storeNewRate = async () => {
let type = this.day_of_week.value
let submittedAmount = []
type == 'allday' ? submittedAmount = this.amount.allday : submittedAmount = this.amount.day
this.ticketing.day_of_week = this.day_of_week.value
this.ticketing.amount = submittedAmount
console.log('submit: ', toRaw(ticketing))
await storeTicketing({...ticketing})
}
return {
type_of_day: [
{
"name": "All Days of Week",
"value": "allday"
},
{
"name": "Rate per Day",
"value": "perday"
},
],
started_at: '',
day_of_week: '',
amount: {
allday: '',
day: []
},
storeNewRate,
ticketing
}
}
}
</script>
And I have Separate file for handling the API:
export default function usePlan() {
const ticketing = reactive({});
const router = useRouter();
let toastr = Swal.mixin({
toast: true,
position: 'top-end',
showConfirmButton: false,
timer: 5000
})
const storeTicketing = async (data) => {
let errors = ''
try {
let response = await axios.post('/api/ticketing/create', data);
ticketing.value = response.data;
await toastr.fire("Success", "New Rate Has Been Created!", "success");
} catch (e) {
if(e.response.status === 422) {
for(const key in e.response.data.message) {
errors = e.response.data.message[key][0];
toastr.fire("Failed", errors, "error");
}
errors = ''
}
}
}
return {
storeTicketing,
ticketing
}
}
🙏🏼
i am looping through an array displaying songs I have uploaded into firebase. I am giving the user a button to edit the name of the song if they press the edit button. When button is pressed it show to edit details, however, except of only the one object expanding all of them expand to edit. How can I give each object its own version of the showForm Bool thanks.
Displaying songs:
<div v-for="song in watchSongs" :key="song.docID">
<div class="mt-[10px] border-solid border-2 border-gray-400 rounded-lg p-[5px] min-h-[40px]">
<p class="font-bold float-left">{{song.modifiedName}}</p>
<img #click.prevent="showForm = !showForm" src="#/assets/edit.png" alt="" class="w-[20px] float-right hover:cursor-pointer">
<img src="#/assets/close2.png" alt="" class="w-[20px] float-right hover:cursor-pointer">
<div v-if="this.showForm" class="pt-[30px]">
<p>song title</p>
<input type="text" placeholder="Enter Song Title" class="w-[100%] h-[30px] pl-[5px] mt-[10px] bg-transparent border-[1px] border-solid border-gray-300">
<p>Genre</p>
<input type="text" placeholder="Enter genre" class="w-[100%] h-[30px] pl-[5px] mt-[10px] bg-transparent border-[1px] border-solid border-gray-300">
<div class="flex mt-[20px] mb-[10px]">
<button class="bg-green-500 rounded-md w-[70px] mr-[5px]">Submit</button>
<button class="bg-gray-400 rounded-md w-[70px]">Go back</button>
</div>
</div>
</div>
The data:
export default {
name:"AppUploadDetails",
props: ['songs'],
data(){
return{
showForm:false,
}
},
computed:{
watchSongs(){
return this.songs;
}
}
}
Fixed by changed the v-if to v-if="song.showForm" and the click to #click.prevent="song.showForm"
Use an object to store your loop items, with the key being the docID.
Something like this:
showForm: {}
click.prevent="showForm[song.docID] = !showForm[song.docID]
I have a vuejs page and I am creating DOM inputs dynamically and I want to bind them with a set of 3 form variables to catch the data.
I don't know how many sets of inputs they will be created from the beginning to initialize the form entries.
The form is created with useForm on the setup() section of Vue.
Below are snips of the code.
<template>
<app-layout>
<template #header>
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ title }}
</h2>
</template>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-xl sm:rounded-lg">
<div class="audio_edit">
<form #submit.prevent="form.post('/pppaudio/newProcessedAudio')">
<div class="pp_title">
<label style="font-weight: bold">
Title:
</label>
<label for="title">{{ title }}</label>
</div>
<div class="pp_audio">
<div id="waveform"></div>
<div id="wave-timeline"></div>
<div class="playButton">
<Button data-action="play" type="button">
<span id="play">
<i class="glyphicon glyphicon-play"></i>
Play
</span>
<span id="pause" style="display: none">
<i class="glyphicon glyphicon-pause"></i>
Pause
</span>
</Button>
</div>
</div>
<div class="pp_transcript flex flex-col">
<label style="font-weight: bold">
Transcript:
</label>
{{ transcript }}
</div>
<div id="region_block" ref="reg_block" >
<div class="region">
<div id="copyTextButton" class="copyButton">
<Button v-on:click="getText" type="button">Copy Marked Text</Button>
</div>
<div class="pp_new_transcript flex flex-col">
<label style="font-weight: bold">
New Transcript:
</label>
<textarea id="selectedText" rows="5" class="selectedTextInput h-full" v-model="form.selectedText" />
</div>
<div class="pp_start-stop">
<label for="start" style="font-weight: bold">
Start:
</label>
<input id="start" v-model="form.start" disabled/>
<label for="stop" style="font-weight: bold">
Stop:
</label>
<input id="stop" v-model="form.stop" disabled/>
</div>
<div id="submit_button">
<Button>Submit</Button>
</div>
</div>
</div>
<div id="return_back">
<Button v-on:click="goBack" type="button">Back</Button>
</div>
</form>
</div>
</div>
</div>
</div>
</app-layout>
</template>
<script>
import AppLayout from "../../Layouts/AppLayout";
import Label from "../../Jetstream/Label";
import Button from "../../Jetstream/Button";
import {InputFacade} from 'vue-input-facade';
import WaveSurfer from 'wavesurfer.js';
import RegionPlugin from 'wavesurfer.js/dist/plugin/wavesurfer.regions.js';
import TimeLine from 'wavesurfer.js/dist/plugin/wavesurfer.timeline.js';
import {useForm} from '#inertiajs/inertia-vue3';
import Vue from 'vue'
export default {
setup() {
const form = useForm({
title: null,
selectedText: null,
start: null,
stop: null,
})
return {form}
},
components: {Button, Label, AppLayout, InputFacade},
methods: {
getText() {
this.newTranscript = window.getSelection().toString();
this.form.selectedText = this.newTranscript;
this.form.title = this.title;
},
copyValues(start, stop) {
this.form.start = start;
this.form.stop = stop;
},
goBack() {
let formData = new FormData();
formData.append('id', localStorage.id);
axios.post('/pppaudio/goBack',
formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
}
).then( (response) => {
if (response.data.status === 'success') {
window.location.href = response.data.url;
}
}).catch(error => {
console.log("ERRRR:: ", error.response.data);
});
}
},
mounted() {
if (localStorage.title) {
this.title = localStorage.title;
}
if (localStorage.path) {
this.path = localStorage.path;
}
if (localStorage.transcript) {
this.transcript = localStorage.transcript;
}
const wavesurfer = WaveSurfer.create({
container: '#waveform',
waveColor: 'violet',
progressColor: 'purple',
scrollParent: true,
plugins: [
RegionPlugin.create({
dragSelection: true
}),
TimeLine.create({
container: '#wave-timeline'
})
]
});
wavesurfer.load('../storage/preprocessed-audio/' + this.path);
let play = false;
wavesurfer.on('region-click', (region, e) => {
e.stopPropagation();
// Play on click, loop on shift click
if (play) {
wavesurfer.pause();
play = false;
} else {
region.play();
play = true;
}
// console.log(this.form.title);
this.copyValues(region.start, region.end);
});
wavesurfer.on('region-created', (region, e) => {
console.log(region.id.substring(10))
let regionNode = document.createElement("div")
regionNode.className = "region"
let regionTitle = document.createElement("div")
regionTitle.className = "regionTitle"
regionTitle.innerHTML = region.id
regionTitle.style.fontWeight = "bold"
regionNode.appendChild(regionTitle)
let color = () => Math.random() * 256 >> 0;
let col = `${color()}, ${color()}, ${color()}`
regionTitle.style.color = 'rgb('+col+')';
region.color = 'rgba('+ col + ', 0.1)';
let copyButtonDiv = document.createElement("div")
copyButtonDiv.className = "copyTextButton"
let copyButton = document.createElement("Button")
copyButton.className = "inline-flex items-center px-4 py-2 bg-gray-800 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-gray-700 active:bg-gray-900 focus:outline-none focus:border-gray-900 focus:ring focus:ring-gray-300 disabled:opacity-25 transition"
let copyButtonText = document.createTextNode("Copy Marked Text")
copyButton.appendChild(copyButtonText)
copyButton.addEventListener('click', this.getText)
let cpBtnType = document.createAttribute("type")
cpBtnType.value = "button"
copyButton.setAttributeNode(cpBtnType)
copyButtonDiv.appendChild(copyButton)
regionNode.appendChild(copyButtonDiv)
let newTranscriptDiv = document.createElement("div")
newTranscriptDiv.className = "pp_new_transcript flex flex-col"
let newTransLabel = document.createElement("label")
newTransLabel.innerText = "New Transcript:"
newTransLabel.style.fontWeight = "bold"
let selectTextArea = document.createElement("textarea")
selectTextArea.id = "selectedText"
selectTextArea.className = "selectedTextInput h-full"
let selectTextAreType = document.createAttribute("rows")
selectTextAreType.value = "5"
selectTextArea.setAttributeNode(selectTextAreType)
newTranscriptDiv.appendChild(newTransLabel)
newTranscriptDiv.appendChild(selectTextArea)
regionNode.appendChild(newTranscriptDiv)
let startStopDiv = document.createElement("div")
startStopDiv.className = "pp_start-stop"
let startLabel = document.createElement("label")
startLabel.innerText = "Start:"
startLabel.style.fontWeight = "bold"
let startLabelType = document.createAttribute("for")
startLabelType.value = "start"
startLabel.setAttributeNode(startLabelType)
let startInput = document.createElement("input")
startInput.id = "start"
let startInputType = document.createAttribute("disabled")
startInput.setAttributeNode(startInputType)
let stopLabel = document.createElement("label")
stopLabel.innerText = "Stop:"
stopLabel.style.fontWeight = "bold"
let stopLabelType = document.createAttribute("for")
stopLabelType.value = "stop"
stopLabel.setAttributeNode(stopLabelType)
let stopInput = document.createElement("input")
stopInput.id = "stop"
let stopInputType = document.createAttribute("disabled")
stopInput.setAttributeNode(stopInputType)
startStopDiv.appendChild(startLabel)
startStopDiv.appendChild(startInput)
startStopDiv.appendChild(stopLabel)
startStopDiv.appendChild(stopInput)
regionNode.appendChild(startStopDiv)
let submitBtnDiv = document.createElement("div")
submitBtnDiv.id = "submit_button"
let submitButton = document.createElement("Button")
let submitButtonText = document.createTextNode("Submit")
submitButton.className = "inline-flex items-center px-4 py-2 bg-gray-800 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-gray-700 active:bg-gray-900 focus:outline-none focus:border-gray-900 focus:ring focus:ring-gray-300 disabled:opacity-25 transition"
submitButton.appendChild(submitButtonText)
submitBtnDiv.appendChild(submitButton)
regionNode.appendChild(submitBtnDiv)
// this.$set()
document.querySelector('#region_block').appendChild(regionNode)
})
let playButton = document.querySelector('#play');
let pauseButton = document.querySelector('#pause');
wavesurfer.on('play', function() {
playButton.style.display = 'none';
pauseButton.style.display = '';
});
wavesurfer.on('pause', function() {
playButton.style.display = '';
pauseButton.style.display = 'none';
});
playButton.addEventListener('click', function () {
wavesurfer.play()
})
pauseButton.addEventListener('click', function () {
wavesurfer.pause()
})
},
data() {
return {
title: '',
path: '',
transcript: '',
selectedText: '',
newTranscript: '',
start: '',
stop: ''
}
}
}
</script>
<style>
.audio_edit {
padding: 10px;
}
.pp_title, .pp_audio, .copyButton, .pp_transcript, .pp_new_transcript, .pp_start-stop, #wave-timeline, #submit_button {
padding-bottom: 10px;
}
.region {
border-top: 3px solid black;
border-bottom: 3px solid black;
padding-top: 3px;
margin-bottom: 3px;
}
.pp_new_transcript {
display: flex;
width: 100%;
}
.selectedTextInput {
-webkit-box-flex:1;
-webkit-flex:1;
-ms-flex:1;
flex:1;
border: none;
}
</style>
Whenever you need some HTML to repeat in Vue, use v-for. In the example below I'm using it with component
Using createElement in Vue is rarely needed and should be avoided. If you think about my example below, you should see that whole wavesurfer.on('region-created') part can be just replaced by just pushing new Region object into an array which is used in v-for
const app = Vue.createApp({
data() {
return {
forms: [{
id: 1,
selectedText: 'Hello',
start: 0,
stop: 4
},
{
id: 2,
selectedText: 'world',
start: 6,
stop: 10
},
]
}
}
})
app.component('my-form', {
props: {
form: {
type: Object,
required: true
}
},
template: `
<div class="region">
<div id="copyTextButton" class="copyButton">
<button type="button">Copy Marked Text</button>
</div>
<div class="pp_new_transcript flex flex-col">
<label style="font-weight: bold">
New Transcript:
</label>
<textarea id="selectedText" rows="5" class="selectedTextInput h-full" v-model="form.selectedText" />
</div>
<div class="pp_start-stop">
<label for="start" style="font-weight: bold">
Start:
</label>
<input id="start" v-model="form.start" disabled/>
<label for="stop" style="font-weight: bold">
Stop:
</label>
<input id="stop" v-model="form.stop" disabled/>
</div>
<div id="submit_button">
<button>Submit</button>
</div>
</div>
`
})
app.mount('#app')
<script src="https://unpkg.com/vue#3.1.4/dist/vue.global.js"></script>
<div id='app'>
<my-form v-for="form in forms" :key="form.id" :form="form"></my-form>
</div>
I have Three Component with this structure
1.ParrentComponent(
-1.ChildComponent
-2.Child
)
1.Child component is loading image via file input and making preview in that controller. after i click submit button i need to send this blob image to second child and show that preview there.
My problem is when i sent image to second child url in image is the same but image not show just alt is showing
1.Child:
<template>
<div id="insert-component">
<div id="insert-new" >
<h2 class="text-md-left">Nová kategória</h2>
<div class="mt-2 text-left">
<a href="#" id="img-button" class=" d-flex flex-wrap" v-on:click.stop="loadImage()">
<img v-bind:src="category_img" alt="logo" id="preview">
<input type="file" class="d-none" id="load-category-image" v-on:input="handleFileSelected">
<button class="btn btn-dark btn-block" >Pridať obrázok</button>
</a>
<small class="text-danger d-none error" id="img-error">Súbor musí byť png, jpg alebo jpeg</small>
</div>
<div class="form-group mt-2 text-left">
<div>
<label for="category_name">Názov kategórie:</label>
<input type="text" required name="name" class="form-control" v-model="category_name" id="category_name">
<small class="text-danger d-none error" id="name-error">*Názov je povinný</small>
</div>
<label for="category_description" class="mt-2">Popis kategórie:</label>
<textarea name="description" class="form-control" rows="4" v-model="category_description" id="category_description">
</textarea>
</div>
</div>
<button class="btn btn-success btn-block my-2" v-on:click.prevent="submit()">Pridať kategóriu</button>
</div>
</template>
<script>
export default {
name: "InsertComponent",
props: [ 'updateTableData' ],
data: function () {
return {
category_name: "",
category_description: "",
category_img:"/storage/images/no_image.png",
file:null
}
},
methods: {
loadImage(){
document.getElementById('load-category-image').click();
},
submit(){
if(this.checkIfEmptyById('category_name')){
this.showErrors('name-error');
return
}
let item = this.createNewItem();
this.updateTableData(item);
this.clearInputs();
},
createNewItem(){
return {
category_img: this.category_img,
category_name: this.category_name,
category_description: this.category_description,
created_at: null,
updated_at: null,
id: null,
file:this.file
};
},
clearInputs(){
this.category_name="";
this.category_description="";
this.category_img="/storage/images/no_image.png";
},
handleFileSelected() {
let loadedFile = document.getElementById('load-category-image').files[0];
if(this.checkIfFileIsImage(loadedFile))
{
this.file = loadedFile;
//this.category_img="/storage/images/"+loadedFile.name;
this.changeImagePreview();
}
else{
//show image error
let imgError = document.getElementById('img-error');
imgError.classList.remove('d-none');
}
},
checkIfFileIsImage(file){
const acceptedImageTypes = ['image/jpg', 'image/jpeg', 'image/png'];
return acceptedImageTypes.includes(file['type']);
},
changeImagePreview(){
let loadedFile = document.getElementById('load-category-image').files;
this.category_img = URL.createObjectURL(loadedFile[0]);
},
},
}
</script>
Second child:
<template>
<div class="text-center">
<b-table striped hover :items="items" :fields="fields">
<template v-slot:cell(category_img)="data">
<img v-bind:src="'/storage/images/'+data.item.category_img" alt="img" width="50px" height="50px" class="rounded-circle">
</template>
<template v-slot:cell(category_name)="data">
{{data.item.category_name | capitalize}}
</template>
<template v-slot:cell(category_description)="data">
{{data.item.category_description | capitalize}}
</template>
<template v-slot:cell(actions)="data">
<div class="d-flex justify-content-center">
<i class="fas fa-edit"></i>
<i class="fas fa-times-circle"></i>
</div>
</template>
</b-table>
</div>
</template>
<script>
export default {
name:"TableComponent",
props: ['tableData'],
data() {
return {
fields: [
{
key: 'category_img',
label:'Img',
sortable: false
},
{
key: 'category_name',
label:'Name',
tdClass: 'capitalize-first-letter',
sortable: true,
variant: 'dark'
},
{
key: 'category_description',
thClass: 'd-none d-md-block',
tdClass: 'd-none d-md-block text-left',
label:'Description',
sortable: false,
},
{
key: 'actions',
sortable: false,
}
],
items: this.tableData
}
},
Parrent component just passing data between these component it is not important data are passing good. Problem is just that image. Thx for help
This is looks like: (right side child 1, left side child 2)
Correct if Im wrong but in your Child you are passing a function as a prop which in vue is an anti pattern. You should always follow the props down event up design pattern.
Anyway, continue to your problem. On your Child 2 you have the following line
items: this.tableData
This line will assign the value of this.tableData to items. This is only assigned on the created hook part of the component. If this table data changes (which I'm fairly sure it does) the item variable won't be updated. You should have a watch watching the prop and re-assign to item
watch: {
tableData: (value) => {
this.item = value;
}
}
I have some data and it must be updated each time the new data retrieved from pusher. I'm using Vue.js for the reactivity and prevent reloading all stuff from the server every time an event got fired.
I need to update my array regarding to this condition.
/**
*
* 1. prevent each array chunk to store more than 4 items
*
* 2. if all chunk is already contains 4 items, create a new chunk,
* unshift new data and move oldest item to the new chunk.
*
* ------------ example ------------
*
* * const beforeUnshift = [
* * 0 => [7, 6, 5, 4],
* * 1 => [3, 2, 1, 0]
* * ];
*
* * const afterUnshift = [
* * 0 => [8, 7, 6, 5],
* * 1 => [4, 3, 2, 1],
* * 2 => [0]
* * ];
*
* * const afterSecondaryUnshift = [
* * 0 => [9, 8, 7, 6],
* * 1 => [5, 4, 3, 2],
* * 2 => [1, 0]
* * ];
*
* * and so on...
*/
Please take a look at my code, each page must show maximum 4 data. It does updated once an event gets fired, but it updates all the way to the bottom until the page is refreshed.
<template>
<div class="table-swipable" style="overflow-x: hidden;">
<div class="swiper-wrapper">
<div class="swiper-slide" v-for="flightChunk in chunkedFlights">
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th class="pl-0 pr-1 border-0 text-center fit-content">
<div class="bg-white shadow rounded cardy-th">Flight No.</div>
</th>
<th class="px-1 border-0 text-center">
<div class="bg-white shadow rounded cardy-th">Airlines</div>
</th>
<th class="px-1 border-0 text-center" colspan="2">
<div class="bg-white shadow rounded cardy-th">Destination</div>
</th>
<th class="px-1 border-0 text-center" colspan="2">
<div class="bg-white shadow rounded cardy-th">Departure / Arrival</div>
</th>
<th class="pl-1 pr-0 border-0 text-center">
<div class="bg-white shadow rounded cardy-th">Status</div>
</th>
</tr>
</thead>
<tbody class="font-weight-bold">
<template v-for="flight in flightChunk">
<tr class="bg-white shadow fit-content">
<td class="border-0 py-4 pl-4 rounded-left">
<i class="fas fa-plane text-primary mr-3"></i>
#{{ flight.id }}
</td>
<td class="border-0 py-4 pl-4">{{ flight.airline.name }}</td>
<td class="border-0 py-4 pl-4 text-center">{{ flight.origin.city }}</td>
<td class="border-0 py-4 pl-4 text-center">{{ flight.destination.city }}</td>
<td class="border-0 py-4 pl-4 text-center">{{ flight.departure | removeSecond }}</td>
<td class="border-0 py-4 pl-4 text-center">{{ flight.arrival | removeSecond }}</td>
<td
class="border-0 py-4 pl-4 text-primary rounded-right text-center"
>{{ flight.status ? flight.status : 'Awaiting confirmation' }}</td>
</tr>
<tr class="tr-spacer"></tr>
</template>
</tbody>
</table>
</div>
</div>
</div>
</div>
</template>
<script>
import Swiper from "swiper";
import moment from "moment";
export default {
data() {
return {
flights: {}
};
},
computed: {
chunkedFlights() {
return _.chunk(this.flights.data, 4);
}
},
created() {
Echo.channel("flight-created").listen("FlightCreated", ({ flights }) => {
this.chunkedFlights[0].unshift(flights[0]);
this.$forceUpdate();
});
},
filters: {
removeSecond(time) {
if (!time) return "";
return moment(time).format("hh:mm");
}
},
updated() {
var tableSwipable = new Swiper(".table-swipable", {
centeredSlides: true,
slidesPerView: 1,
spaceBetween: 60,
autoplay: {
delay: 30000
}
});
},
mounted() {
axios.get("/flights/public").then(response => {
this.flights = response.data;
});
}
};
</script>
So this is a case where it makes sense to separate your data from your view layer. It makes it easier to deal with updates if you have a single source of truth that you update, instead of trying to update your chunks directly.
So, when you get an update, unshift it into this.flights.data instead of a chunk, then make vue recalculate your chunks. This keeps your original data array as your single source of truth, and you update the chunks from it each time for your view.
Oh, I see that frodo2975 caught that you were unshifting into a computed value rather than into your data. I didn't notice that. This works as you intend:
new Vue({
el: '#app',
data: {
flights: [
7, 6, 5, 4, 3, 2, 1, 0
]
},
computed: {
chunkedFlights() {
return _.chunk(this.flights, 4);
}
},
mounted() {
setTimeout(() => {
this.flights.unshift(8);
}, 2000);
}
});
#app {
display: grid;
grid-gap: 2rem;
background-color: black;
padding: 0.6rem;
}
.page {
background-color: #ffe;
padding: 0.6rem;
grid-gap: 0.2rem;
display: grid;
}
.row {
background-color: blue;
color: white;
padding: 0.6rem;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div v-for="chunk in chunkedFlights" class="page">
<div v-for="flight in chunk" class="row">
{{flight}}
</div>
</div>
</div>