Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save bran921007/ab8ff9d28be4ea9aecbc084af904f56d to your computer and use it in GitHub Desktop.
Save bran921007/ab8ff9d28be4ea9aecbc084af904f56d to your computer and use it in GitHub Desktop.
Alpinejs 3 - Bootstrap repeater
<div class="container" x-data x-cloak>
<h1 class="h2 d-print-none mt-4 mb-2">Alpinejs 3 - Bootstrap 5 repeater</h1>
<p class="d-print-none mb-4">Lorem ipsum dolor sit, amet consectetur adipisicing elit. Sequi corrupti accusantium numquam similique maxime earum quos alias voluptatibus eius porro. Voluptates culpa nam illum reprehenderit maiores modi eaque est in.</p>
<h2 class="d-none d-print-block text-center mb-4">My awesome table</h2>
<div x-data="repeater">
<template x-for="(row, index) in rows" :key="index">
<div class="position-relative d-print-none">
<div class="row g-1 mb-3">
<div class="col-12 col-md-5 col-lg-6 col-xl-7">
<div class="form-floating">
<input id="lavoroInput" x-model="row.description" type="text" class="form-control" placeholder="Description" />
<label for="lavoroInput">DESCRIPTION</label>
</div>
</div>
<div class="col-5 col-md-3 col-lg-2 col-xl-2">
<div class="form-floating">
<input x-model="row.unit_price" type="number" class="form-control" placeholder="Unit price" />
<label for="totaleInput">UNIT PRICE</label>
</div>
</div>
<div class="col-3 col-md-2 col-xl-1">
<div class="form-floating">
<input x-model="row.qty" type="number" min="1" class="form-control" />
<label for="totaleInput">QTY</label>
</div>
</div>
<div class="col-4 col-md-2 col-xl-2">
<div class="form-floating">
<input readonly id="amountInput" x-model="row.amount" :value="row.amount = (row.qty * row.unit_price) > 0 ? (row.qty * row.unit_price).toFixed(2) : null" type="text" class="form-control" placeholder="Amount" />
<label for="amountInput">AMOUNT <span x-text="currency_symbol"></span></label>
</div>
</div>
</div>
<div class="position-absolute top-0 start-100 translate-middle me-1 mt-1" x-show="rows.length > 1" x-data="{hover: false}">
<div class="p-1 text-secondary border rounded-circle" :class="hover ? 'border-danger bg-danger text-white' : 'bg-white'" style="cursor:pointer" @click="del(index)" @mouseenter="hover = true" @mouseleave="hover = false">
<div style="line-height:6px" class="text-center">
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="currentColor" class="bi bi-x-lg" viewBox="0 0 16 16">
<path d="M1.293 1.293a1 1 0 0 1 1.414 0L8 6.586l5.293-5.293a1 1 0 1 1 1.414 1.414L9.414 8l5.293 5.293a1 1 0 0 1-1.414 1.414L8 9.414l-5.293 5.293a1 1 0 0 1-1.414-1.414L6.586 8 1.293 2.707a1 1 0 0 1 0-1.414z" />
</svg>
</div>
</div>
</div>
</div>
</template>
<div class="d-print-none">
<div class="d-flex justify-content-between d-print-none">
<button class="btn btn-sm btn-outline-success lh-1" type="button" @click="add()">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-plus-square-fill" viewBox="0 0 16 16">
<path d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z" />
</svg>
ADD ITEM
</button>
<button type="button" class="btn btn-sm btn-outline-danger lh-1" @click="reset()">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash-fill" viewBox="0 0 16 16">
<path d="M2.5 1a1 1 0 0 0-1 1v1a1 1 0 0 0 1 1H3v9a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V4h.5a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1H10a1 1 0 0 0-1-1H7a1 1 0 0 0-1 1H2.5zm3 4a.5.5 0 0 1 .5.5v7a.5.5 0 0 1-1 0v-7a.5.5 0 0 1 .5-.5zM8 5a.5.5 0 0 1 .5.5v7a.5.5 0 0 1-1 0v-7A.5.5 0 0 1 8 5zm3 .5v7a.5.5 0 0 1-1 0v-7a.5.5 0 0 1 1 0z" />
</svg>
RESET
</button>
</div>
<hr>
<div class="text-end">
<div class="row g-1 lh-lg">
<div class="col-6 col-sm-8 col-md-10">SUBTOTAL:</div>
<div class="col-6 col-sm-4 col-md-2 text-start">
<span x-text="subtotal()"></span><span x-text="currency_symbol"></span>
</div>
</div>
<div class="row g-1 lh-lg">
<div class="col-6 col-sm-8 col-md-10">TAX:</div>
<div class="col-6 col-sm-4 col-md-2">
<div class="input-group input-group-sm">
<input type="text" x-model="tax" class="form-control">
<span class="input-group-text">%</span>
</div>
</div>
</div>
<div class="row g-1 lh-lg">
<div class="col-6 col-sm-8 col-md-10">TOTAL:</div>
<div class="col-6 col-sm-4 col-md-2 text-start">
<strong x-text="total()"></strong><span x-text="currency_symbol"></span>
<p class="small">Tax amount: <span x-text="taxAmount()"></span><span x-text="currency_symbol"></span></p>
</div>
</div>
</div>
<div class="text-center">
<button class="btn btn-outline-info btn-sm my-2" @click="print()"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-printer-fill" viewBox="0 0 16 16">
<path d="M5 1a2 2 0 0 0-2 2v1h10V3a2 2 0 0 0-2-2H5zm6 8H5a1 1 0 0 0-1 1v3a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1v-3a1 1 0 0 0-1-1z"></path>
<path d="M0 7a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2h-1v-2a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v2H2a2 2 0 0 1-2-2V7zm2.5 1a.5.5 0 1 0 0-1 .5.5 0 0 0 0 1z"></path>
</svg> PRINT TABLE</button>
</div>
<hr class="mb-4">
</div>
<div class="d-print-block d-none">
<table class="table table-striped">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Description</th>
<th scope="col">Unit price</th>
<th scope="col">Qty</th>
<th scope="col">Amount</th>
</tr>
</thead>
<tbody>
<template x-for="(row, index) in rows" :key="index">
<tr>
<th scope="row" x-text="index+1">1</th>
<td x-text="row.description"></td>
<td>
<span x-text="row.unit_price"></span>
<span x-text="currency_symbol"></span>
</td>
<td x-text="row.qty"></td>
<td>
<span x-text="row.amount"></span>
<span x-text="currency_symbol"></span>
</td>
</tr>
</template>
</tbody>
</table>
<table class="table table-borderless table-sm">
<tbody>
<tr class="">
<td style="width:70%" class="text-end">SUBTOTAL:</td>
<td colspan="2"><span x-text="subtotal()"></span><span x-text="currency_symbol"></span></td>
</tr>
<tr class="">
<td class="text-end">TAX:</td>
<td >
<span x-text="tax"></span>%
<p class="small mb-0">Tax amount: <span x-text="taxAmount()"></span><span x-text="currency_symbol"></span></p>
</td>
</tr>
<tr class="">
<td class="text-end">TOTAL:</td>
<td>
<span x-text="total()"></span><span x-text="currency_symbol"></span>
</td>
</tr>
</tbody>
</table>
</div>
<div class="d-print-none">
<button class="btn btn-outline-warning btn-sm my-2" @click="rows = [...window.demo_data]">LOAD DEMO DATA</button>
<div x-data="{open:false}">
<div class="card mt-6 small text-secondary" style="border: 1px dotted #aaa">
<div class="card-body p-1">
<div class="text-center cursor-pointer lh-1" style="cursor: pointer;" @click="open = ! open">
SHOW ROWS JSON
<template x-if="open">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-caret-down-fill" viewBox="0 0 16 16">
<path d="M7.247 11.14 2.451 5.658C1.885 5.013 2.345 4 3.204 4h9.592a1 1 0 0 1 .753 1.659l-4.796 5.48a1 1 0 0 1-1.506 0z" />
</svg>
</template>
<template x-if="!open">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-caret-up-fill" viewBox="0 0 16 16">
<path d="m7.247 4.86-4.796 5.481c-.566.647-.106 1.659.753 1.659h9.592a1 1 0 0 0 .753-1.659l-4.796-5.48a1 1 0 0 0-1.506 0z" />
</svg>
</template>
</div>
<div x-show="open">
<pre x-text="JSON.stringify(rows, null, 2)"></pre>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script defer src="https://unpkg.com/@alpinejs/[email protected]/dist/cdn.min.js"></script>
<script defer src="https://unpkg.com/[email protected]/dist/cdn.min.js"></script>
var demo_data = [
{
"description": "Logo design",
"qty": 1,
"unit_price": "1200",
"amount": 0,
},
{
"description": "Landing page",
"qty": 1,
"unit_price": "400",
"amount": 0,
},
{
"description": "Social media manager",
"qty": "12",
"unit_price": "180",
"amount": 0,
}
];
document.addEventListener('alpine:init', () => {
Alpine.data('repeater', () => ({
// Data
rowBase: {
description: null,
qty: 1,
unit_price: null,
amount: 0,
},
currency_symbol: '€',
tax: 22,
rows: [],
// Calculations
subtotal() {
const subt = this.rows.reduce((prev, cur) => {
return Number(prev) + Number(cur.amount);
}, 0);
return subt.toFixed(2);
},
taxAmount() {
const subt = this.subtotal();
return ((subt * this.tax) / 100).toFixed(2);
},
total() {
const subt = Number(this.subtotal());
return (subt + ( (subt * this.tax) / 100)).toFixed(2);
},
// Actions
add() {
// const row = JSON.stringify(this.rowBase);
// this.rows.push(JSON.parse(row));
this.rows.push({...this.rowBase});
},
del(index) {
this.rows.splice(index, 1);
},
reset() {
this.rows = [];
this.add();
},
print() {
window.print();
},
init() {
this.add();
}
}))
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.1.0/js/bootstrap.min.js"></script>
[x-cloak] {
display: none !important;
}
body {
font-family: "IBM Plex Sans", sans-serif;
}
@media print {
@page {
margin-top: 0;
margin-bottom: 0;
}
body {
margin-top: 50px;
margin-bottom: 50px;
font-size-adjust: 0.46;
}
.container {
max-width: 100% !important;
}
}
<link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/5.0.2/css/bootstrap.min.css" rel="stylesheet" />
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment