I have a table that shows a list of countries like so (also, full codesandbox link below):
When the "Review" button is pressed, a modal appears and the user can review some data about the country and then choose from the following options:
[mark as ready] [mark as reject] [mark as pending]
Based on his pick, I need to emit the country's id back to the parent component so we can now display the status for that specific country as either "Rejected", "Ready" , or "Pending".
What I need help with is how I can logically make this as simple as possible without getting complicated (something I am terrible at).
I can successfully emit data back to the parent, but where I fail is how to handle the logic on the parent side.
Let me explain:
Here is my table displaying country data and buttons for review:
App.vue:
<template>
<div id="app" class="container mt-5">
<table class="table">
<thead>
<tr>
<th scope="col">Country</th>
<th scope="col">Status</th>
</tr>
</thead>
<tbody>
<tr v-for="country in countryChanges" :key="country.id">
<td>{{ country.name }}</td>
<td>
<template v-if="ready">
<button :href="`#modal_${country.id}`" data-bs-toggle="modal">
Ready
</button>
</template>
<template v-else-if="rejected">
<button :href="`#modal_${country.id}`" data-bs-toggle="modal">
Rejected
</button>
</template>
<template v-else>
<button :href="`#modal_${country.id}`" data-bs-toggle="modal">
Review
</button>
</template>
</td>
<AppModal :country="country" #set-as-ready="setAsReady"> </AppModal>
</tr>
</tbody>
</table>
</div>
</template>
<script>
import AppModal from "#/components/AppModal.vue";
import changedCountryData from "#/assets/data.json";
export default {
name: "AdminDashboard",
components: {
AppModal,
},
data() {
return {
countryChanges: [],
ready: false,
rejected: false,
};
},
async created() {
this.countryChanges = changedCountryData;
},
methods: {
setAsReady(id) {
},
},
};
</script>
Here is the modal screen for each country detail view:
AppModal.vue:
<template>
<section
class="modal fade"
:id="`modal_${country.id}`"
tabindex="-1"
aria-labelledby="appModal"
aria-hidden="true"
>
<div class="modal-dialog modal-dialog-scrollable modal-fullscreen">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title fw-bold fs-2">
<slot name="heading"></slot>
</h5>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
></button>
</div>
<div class="modal-body">
<div class="container-fluid">
<ul class="list-inline fw-bold mb-4">
<li class="list-inline-item me-4">
<span class="badge rounded-pill bg-light text-secondary"
>Baseline Type</span
>
<slot name="baselineType"></slot>
</li>
<li class="list-inline-item me-4">
<span class="badge rounded-pill bg-light text-secondary">
Submitter</span
>
Doe, John
</li>
<li class="list-inline-item me-4">
<span class="badge rounded-pill bg-light text-secondary">
Submitted</span
>
March 2, 2021
</li>
<li class="list-inline-item">
<span class="badge rounded-pill bg-light text-secondary me-2"
>Status</span
>
<span class="badge rounded-pill bg-warning text-dark"
>Review Changes</span
>
</li>
</ul>
<table class="table table-bordered table-striped">
<thead class="table-dark fs-5">
<tr>
<th scope="col" class="text-muted">Field/Entity</th>
<th scope="col">{{ country.name }}</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Full Name</th>
<td>
Original Value
<div class="ps-2 fw-bold text-success">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
class="bi bi-arrow-return-right"
viewBox="0 0 16 16"
>
<path
fill-rule="evenodd"
d="M1.5 1.5A.5.5 0 0 0 1 2v4.8a2.5 2.5 0 0 0 2.5 2.5h9.793l-3.347 3.346a.5.5 0 0 0 .708.708l4.2-4.2a.5.5 0 0 0 0-.708l-4-4a.5.5 0 0 0-.708.708L13.293 8.3H3.5A1.5 1.5 0 0 1 2 6.8V2a.5.5 0 0 0-.5-.5z"
/>
</svg>
Updated Value
</div>
</td>
</tr>
<tr>
<th scope="row">Local Short Name</th>
<td>
Original Value
<div class="ps-2 text-success fw-bold">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
class="bi bi-arrow-return-right"
viewBox="0 0 16 16"
>
<path
fill-rule="evenodd"
d="M1.5 1.5A.5.5 0 0 0 1 2v4.8a2.5 2.5 0 0 0 2.5 2.5h9.793l-3.347 3.346a.5.5 0 0 0 .708.708l4.2-4.2a.5.5 0 0 0 0-.708l-4-4a.5.5 0 0 0-.708.708L13.293 8.3H3.5A1.5 1.5 0 0 1 2 6.8V2a.5.5 0 0 0-.5-.5z"
/>
</svg>
Updated Value
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="modal-footer border-top-0">
<button type="button" class="btn btn-link" data-bs-dismiss="modal">
Close
</button>
<button
type="button"
class="btn btn-success"
data-bs-dismiss="modal"
#click="setAsReady(country)"
>
Mark as Ready
</button>
<button type="button" class="btn btn-danger">Mark as Reject</button>
<button type="button" class="btn btn-warning">Mark as Pending</button>
</div>
</div>
</div>
</section>
</template>
<script>
export default {
props: {
country: {
type: Object,
required: true,
},
},
methods: {
setAsReady(country) {
// console.log(country.id);
this.$emit("set-as-ready", country.id);
},
},
};
</script>
If the "Mark as Ready" button is pressed, for example, I want to then show an updated button with status of "Ready" for that country (in this example, Mexico) like so:
Anyone have advice on how I can do this logically and efficiently? I have a codesandbox demo available here: https://codesandbox.io/s/tender-black-zn9nt?file=/src/App.vue
The simplest approach is to manipulate the central state of data. Set a new property "status" on table items. Then toggle that value depending on the event emitted from table.
<template v-if="country.status === 1">
<button :href="`#modal_${country.id}`" data-bs-toggle="modal">
Ready
</button>
</template>
methods: {
setAsReady(id) {
let country = this.countryChanges.find((e) => e.id === id);
country.status = 1;
},
},
See the working codesandbox: https://codesandbox.io/s/dry-water-gl2q9?file=/src/App.vue
Related
I'm currently developing an appointment application in which I'm working on a page where there would be a list of appointments. I've decided to paginate the data given that appointments would be coming from multiple patients and give the user the option to search. But my search is inactive and my pagination buttons aren't responding. I've push the data from the controller with the paginate function but still no luck.
This is the Vue where the list of components are rendered
<template>
<dashboard-layout :header-caption="Appointments" header="Appointments">
<div class="flex justify-between mb-6">
<div class="max-w-xs">
<input
type="search"
v-model="params.search"
aria-label="Search"
placeholder="Search..."
class="block w-full rounded-md border-gray-700 shadow-sm focus:ring-blue-700 focus:border-blue-700 sm:text-sm"
/>
</div>
</div>
<div class="overflow-hidden bg-white shadow-md sm:rounded-lg">
<div class="flex flex-col">
<div class="overflow-x-auto -my-2 sm:-mx-6 lg:-mx-8">
<div
class="inline-block py-2 min-w-full align-middle sm:px-6 lg:px-8"
>
<div
class="overflow-hidden border-b border-gray-700 shadow sm:rounded-lg"
>
<table
class="min-w-full divide-y divide-gray-700 table-fixed"
>
<thead class="bg-blue-800">
<tr>
<th
scope="col"
class="w-3/12 text-xs font-semibold tracking-wider text-left text-white uppercase"
>
<span
class="inline-flex py-3 px-6 w-full justify-between"
>Reason
</span>
</th>
<th
scope="col"
class="w-3/12 text-xs font-semibold tracking-wider text-left text-white uppercase"
>
<span
class="inline-flex py-3 px-6 w-full justify-between"
>Date
</span>
</th>
<th
scope="col"
class="w-3/12 text-xs font-semibold tracking-wider text-left text-white uppercase"
>
<span
class="inline-flex py-3 px-6 w-full justify-between"
>Time
</span>
</th>
<th
scope="col"
class="w-3/12 text-xs font-semibold tracking-wider text-left text-white uppercase"
>
<span
class="inline-flex py-3 px-6 w-full justify-between"
>Actions
</span>
</th>
</tr>
</thead>
<tbody
class="bg-white divide-y divide-gray-700"
>
<tr
v-for="appointment in appointments.data"
:key="appointment.id"
>
<td
class="py-4 px-6 text-sm text-gray-800 whitespace-nowrap"
v-text="appointment.reason"
/>
<td
class="py-4 px-6 text-sm text-gray-800 whitespace-nowrap"
v-text="
formatDate(
appointment.appointment_date
)
"
/>
<td
class="py-4 px-6 text-sm text-gray-800 whitespace-nowrap"
v-text="
tConvert(
appointment.appointment_time
)
"
/>
<td>
<div
v-if="
appointment.status ==
'Confirmed'
"
class="flex"
>
<status-tag
:status="appointment.status"
/>
<Link
method="delete"
:href="
route(
'appointments.destroy',
appointment
)
"
>
<small-button
class="ml-3"
color="red"
icon="fas fa-trash-alt fa-lg"
label="Remove"
type="button"
/>
</Link>
</div>
<div
v-else-if="
appointment.status ==
'Declined'
"
class="flex"
>
<status-tag
:status="appointment.status"
/>
<Link
method="delete"
:href="
route(
'appointments.destroy',
appointment
)
"
>
<small-button
class="ml-3"
color="red"
icon="fas fa-trash-alt fa-lg"
label="Remove"
type="button"
/>
</Link>
</div>
<div
v-else
class="flex justify-end mr-9"
>
<small-button
color="blue"
icon="fas fa-check-circle fa-lg"
label="Confirm"
type="button"
#click="
confirmAppointment({
appointment,
})
"
/>
<!-- <small-button
class="ml-5"
color="purple"
icon="fas fa-clock fa-lg"
type="button"
label="Reschedule"
/> -->
<small-button
class="ml-5"
color="red"
icon="fas fa-times-circle fa-lg"
type="button"
label="Decline"
#click="
declineAppointment({
appointment,
})
"
/>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<pagination class="mt-10" :links="appointments.links" />
<appointment-modal
v-if="showAppointmentModal"
#toggle="toggleAppointmentModal"
/>
</dashboard-layout>
</template>
<script>
import DashboardLayout from "#/Layouts/DashboardLayout";
import Pagination from "#/Components/Common/Pagination";
import useFormatter from "#/Components/composables/useFormatter";
import BaseButton from "#/Components/Common/BaseButton";
import SmallButton from "#/Components/Common/SmallButton";
import StatusTag from "#/Components/Common/StatusTag";
import { Link } from "#inertiajs/inertia-vue3";
import { Inertia } from "#inertiajs/inertia";
export default {
components: {
Link,
DashboardLayout,
Pagination,
StatusTag,
BaseButton,
SmallButton,
},
props: {
appointments: Object,
},
data() {
const { formatDate } = useFormatter();
function tConvert(time) {
// Check correct time format and split into components
time = time
.toString()
.match(/^([01]\d|2[0-3])(:)([0-5]\d)(:[0-5]\d)?$/) || [time];
if (time.length > 1) {
// If time format correct
time = time.slice(1); // Remove full string match value
time[5] = +time[0] < 12 ? "AM" : "PM"; // Set AM/PM
time[0] = +time[0] % 12 || 12; // Adjust hours
}
return time.join(""); // return adjusted time or original string
}
function confirmAppointment(appointment) {
Inertia.visit("appointment/confirm", {
method: "put",
data: { appointment: appointment },
});
}
function reschedule(appointment) {}
function declineAppointment(appointment) {
Inertia.visit("appointment/declined", {
method: "put",
data: { appointment: appointment },
});
}
return {
declineAppointment,
confirmAppointment,
formatDate,
tConvert,
params: { search: null },
};
},
watch: {
params: {
handler() {
this.inertia.get(this.route("appointments"), this.params, {
replace: true,
preserveState: true,
});
},
deep: true,
},
},
};
</script>
<style></style>
and this is what is being pushed from the Appointment Controller
public function index()
{
return Inertia::render("Doctor/Appointments", [
'appointments' => Appointment::where('status','!=','')->paginate(10)
]);
}
I have 2 pages, one has a list of products and the other one has a bunch of data for that product. There is also a drop-down in the second page.
The problem I'm having is that I'm struggling to get the drop-down to have the selected product selected.
So for example, if I select product 1 from the first page then on the second page the drop-down will show product 1 and if I select product 2 then the drop-down will show product 2.
Here is my code for productIndex.vue
<template>
<div>
<div class="content-header">
<div class="container">
<div class="row">
<div class="col-md-12">
<div class="card">
<div class="card-header">
<h3 class="card-title">Product Index</h3>
</div>
<div class="card-body p-0">
<table class="table table-sm table-hover">
<thead>
<tr>
<th>ID</th>
<th>Product Name</th>
<th style="width: 180px;"></th>
</tr>
</thead>
<tbody>
<tr v-for="product in products">
<td>{{ product.id }}</td>
<td>{{product.name}}</td>
<td class="text-right">
<a #click="goToProductData(product.id)" class="btn btn-xs btn-primary">
Data
</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
props: ['products'],
data() {
return {
}
},
methods: {
goToProductData(product_id){
localStorage.setItem('product', product_id);
window.location = `/product-data`;
}
},
}
</script>
and here is my productData.vue
<template>
<div>
<div class="content-header">
<div class="container">
<div class="card">
<div class="card-body">
<div class="row mb-2">
<div class="col-sm-6">
<label for="product_id_select">Product</label>
<select id="product_id_select" class="form-control select2" style="width: 100%;">
<optgroup v-for="product in selectProducts" :label="customer.text">
<option v-for="p in product.children" :value="p.id">{{ p.text }}</option>
</optgroup>
</select>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
props: ['products'],
data() {
return {
}
},
computed: {
selectProducts() {
let select2Options = [];
Object.keys(this.products).forEach(product => {
let productOptions = [];
this.products[product].forEach(p => {
productOptions.push({ id: p.id, text: p.name });
});
select2Options.push({
text: product,
children: productOptions
});
});
return select2Options;
}
}
}
</script>
You forgot to add the v-model to the select in the second page. You can find some example here.
Your code should be something like:
<select id="product_id_select" v-model="selectedProductId" class="form-control select2" style="width: 100%;">
where selectedProductId is a value in your data (or a computed property) that contains the value inserted into the local storage in the first page
I'm trying to calculate the quantity of items in my vue. The problem I'm having is that my computed property isn't picking up my object, because my thinking was as you can see with the commented out section is that I was going to loop through it and calculate the quantity, but since I'm not able to grab productItems I'm not able to loop through it.
Here is my code
<template>
<div class="content-header">
<div class="container">
<div class="row">
<div class="col-sm-12">
<div class="card">
<div class="card-body">
<div class="row">
<div class="col-lg-12">
<table class="table">
<thead>
<tr>
<th></th>
<th>Name</th>
<th>Qty</th>
</tr>
</thead>
<tbody>
<tr v-for="item in products">
<td>
{{ item['name'] }}
</td>
<td>
<input style="width: 100px; margin-left: 0; display: inline"
type="number" class="form-control"
v-model="productItems[item['name']]['unit']"
>
</td>
</tr>
<tr>
<td></td>
<td>
Consumption total: {{ consumptionTotal }}
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default{
props: [],
data(){
return {
productItems: {}
},
computed:{
consumptionTotal(){
console.log(this.productItems);
// return Object.keys(this.productItems).reduce((carry, item) => {
// carry += Number(this.productItems[item]['unit'])
// return carry;
// }, Number(0));
},
},
watch: {
},
methods: {
},
mounted() {
}
}
</script>
Try below steps. Hopefully it will helps you.
Step 1: productItems should be an array
Step 2: Calculate functions be like
computed: {
consumptionTotal () {
return this.productItems.reduce((total, item) => {
total += item.unit
return total
}, Number(0))
}
}
Step 3: HTML template will be like
<div class="content-header">
<div class="container">
<div class="row">
<div class="col-sm-12">
<div class="card">
<div class="card-body">
<div class="row">
<div class="col-lg-12">
<table class="table">
<thead>
<tr>
<th>Id</th>
<th>Name</th>
<th>Qty</th>
</tr>
</thead>
<tbody>
<tr v-for="item in productItems" :key="item.id">
<td>{{item.id}}</td>
<td>
{{item.name}}
</td>
<td>
<input style="width: 100px; margin-left: 0; display: inline" type="number" class="form-control" :value="item.unit">
</td>
</tr>
<tr>
<td></td>
<td>
Consumption total: {{ consumptionTotal }}
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
DEMO
I'm trying to compare prices from last year and this year and the problem I'm getting is this:
Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value
I understand what it's saying but I'm not sure how to solve it.
Here is my code:
<template>
<div class="content-header">
<div class="container">
<div class="row">
<div class="col-sm-6">
<div class="card">
<div class="card-body">
<div class="row mb-2">
<div class="col-sm-12">
<label for="year_select">Select Year</label>
<select id="year_select" #change="getYearComparison()" class="form-control" style="width: 100%;" v-model="yearSelect">
<option v-for="year in years" :value="year.id">
{{ year.year }}
</option>
</select>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<div class="card">
<div class="card-body">
<div class="row">
<div class="col-lg-12">
<div class="row">
<div class="col-lg-12">
<div class="row">
<div class="col-lg-12">
<table class="table">
<thead>
<tr class="text-center">
<th colspan="4" style="border-right: #dee2e6 solid 1px">
Previous Year
</th>
<th colspan="3" style="border-right: #dee2e6 solid 1px">
This year
</th>
<th colspan="2">
Variance
</th>
</tr>
</thead>
<tr v-for="c in compare">
<td>
{{ c.prev_price }}
</td>
<td>
{{ c.curr_price }}
</td>
<td style="border-right: #dee2e6 solid 1px">
{{ c.v_price }}
</td>
</tr>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
props: ['compare'],
data(){
return {
years: [],
tariffSelect: null
}
},
computed: {
},
methods: {
getYears(){
axios.get('/api/years/getYears').then(response => {
this.years = response.data.years;
});
},
getYearComparison(){
axios.get(`/api/product/${this.tariffSelect}/comparison`).then(response => {
this.compare = response.data.compare;
})
}
},
mounted(){
this.getYears();
}
}
</script>
compare is a prop, meaning that it's to be transferred to your component, the "child" by another component (or the Vue app), the "parent". On the other end, your child component should communicate a value change to its parent using an event.
In your parent component, you can do this:
<template>
<child-component :compare="compare" #updateCompare="compare = $event"/>
</template>
<script>
export default {
data() {
return { compare: '' }; // Put a default value here that makes sense
}
};
</script>
And in your child component, you need to emit the event, so you can replace this line:
this.compare = response.data.compare;
By this one:
this.$emit('updateCompare', response.data.compare);
You can take a look at the documentation for events.
I have a table where each row has a button 'show details'. I have another div stacked within the table, so on clicking the button I want the table to be displayed in the particular column. But I am not able to point which button is clicked from which row to show and hide the div on the clicked row. Here is my code:
<table class="table nowrap scroll-horizontal-vertical">
<thead>
<tr>
<th>SN</th>
<th>Ward no</th>
<th>Personal details</th>
<th>Social security</th>
</tr>
</thead>
<tbody>
<tr v-for="(pop,index) in population" :key="pop.ward_no">
<td>{{index+1}}</td>
<td>{{ pop.ward_no }}</td>
<td>{{ pop.birth }}</td>
<td>
<a href="#" #click="show(index)" class="btn btn-primary">
Show Details
</a>
//show or hide div below on clicking show details button
<div v-show="showDetails">
<h4>Citizen </h4>
<div class="row">
<div class="col-3">{{pop.female}}</div>
<div class="col-2"> {{ pop.male }}</div>
<div class="col-2"> {{ pop.other }}</div>
</div> <br>
</div>
</td>
<td>
<a href="#" class="btn btn-primary" #click="editModal(pop)">
<i class="fa fa-edit"></i>
</a>
<a href="#" class="btn btn-primary" #click="deletePopulation(pop.ward_no)">
<i class="fa fa-trash"></i>
</a>
</td>
</tr>
</tbody>
</table>
This is my vue code:
export default {
data(){
return{
editMode: false,
showDetails: false,
population:{}
})
}
},
methods: {
show(index){
this.showDetails = true;
},
Your showDetails is global and if you click one, you click, vue will show all elements. Your code must be like this code.
new Vue({
el: '.table',
data: {
populations: [
{ showDetails: false, job: 'A' },
{ showDetails: false, job: 'B' },
{ showDetails: false, job: 'C' }
]
}
});
<script src="https://unpkg.com/vue#2.5.2/dist/vue.min.js"></script>
<table class="table">
<tbody>
<tr v-for="(pop, index) in populations">
<td><p v-show='pop.showDetails' class="Detail">My Details</p></td>
<td><a #click="pop.showDetails = false">hide Details</a></td>
<td><a #click="pop.showDetails = true">show Details</a></td>
</tr>
</tbody>
</table>