feat(RepeatLine.js): Ajoute la classe RepeatLine pour répéter des lignes de formulaire

Ajoute la classe RepeatLine pour gérer la répétition dynamique de lignes de formulaire avec suppression et tri.
This commit is contained in:
Serreau Jovann
2025-07-24 09:17:20 +02:00
parent a18b89b790
commit 6476186275
4 changed files with 215 additions and 108 deletions

View File

@@ -3,12 +3,14 @@ import * as Turbo from "@hotwired/turbo"
import {AutoSubmit} from './class/AutoSubmit'
import {ServerCard} from './class/ServerCard'
import {AutoCustomer} from './class/AutoCustomer'
import {RepeatLine} from './class/RepeatLine'
function script() {
customElements.define('auto-submit',AutoSubmit,{extends:'form'})
customElements.define('server-card',ServerCard,{extends:'div'})
customElements.define('auto-customer',AutoCustomer,{extends:'button'})
customElements.define('repeat-line',RepeatLine,{extends:'div'})
}

125
assets/class/RepeatLine.js Normal file
View File

@@ -0,0 +1,125 @@
import Sortable from 'sortablejs';
export class RepeatLine extends HTMLDivElement{
connectedCallback(){
this.$props = this.getProps(this, { maxRows: 5 });
this.$refs = this.getRefs(this);
this.rowHTML = this.$refs.rows.children[0].outerHTML;
this.init();
}
// Hook up events for the row.
setUpRow(row) {
const rowRefs = this.getRefs(row);
rowRefs.removeButton.onclick = (e) => {
e.preventDefault();
this.removeRow(row);
};
}
// Enable or disable addButton as necessary.
updateAddButton() {
if (this.$refs.rows.children.length >= this.$props.maxRows) {
this.$refs.addButton.setAttribute('disabled', '');
return;
}
this.$refs.addButton.removeAttribute('disabled');
}
// Update array key values to the row number
updateFieldNames() {
[...this.$refs.rows.children]
.forEach((el, index) => {
el.querySelectorAll('[name]')
.forEach(el => {
const newName = el.getAttribute('name').replace(/\[\d\]/gm, `[${index}]`);
el.setAttribute('name', newName);
});
});
}
addRow() {
if (
!this.rowHTML ||
this.$refs.rows.children.length >= this.$props.maxRows
) return;
let newRow = this.createFromHTML(this.rowHTML);
newRow.removeAttribute('id');
this.setUpRow(newRow);
this.$refs.rows.appendChild(newRow);
newRow.querySelector('input,textarea,select').focus();
this.updateFieldNames();
this.updateAddButton();
}
removeRow(row) {
if (this.$refs.rows.children.length <= 1) return;
row.remove();
this.$refs.rows.focus();
this.updateFieldNames();
this.updateFieldNames();
}
init() {
this.setUpRow(this.$refs.rows.children[0]);
this.$refs.addButton.onclick = (e) => {
e.preventDefault();
this.addRow();
}
this.updateFieldNames();
let repeater__rows = this.querySelector('.form-repeater__rows');
new Sortable(repeater__rows,{
});
}
// Return an object that contains references to DOM objects.
getRefs(el) {
let result = {};
[...el.querySelectorAll('[data-ref]')]
.forEach(ref => {
result[ref.dataset.ref] = ref;
});
return result;
}
setDefaults(obj, defaults) {
let results = obj;
for (const prop in defaults) {
if (!obj.hasOwnProperty(prop)) {
results[prop] = defaults[prop];
}
}
return results;
}
getProps(el, defaults={}) {
return this.setDefaults(
JSON.parse(el.dataset.props ?? '{}'),
defaults
);
}
createFromHTML(html='') {
let element = document.createElement(null);
element.innerHTML = html;
return element.firstElementChild;
}
}

View File

@@ -18,116 +18,8 @@ export function repeaterComponent($el) {
let rowHTML = $refs.rows.children[0].outerHTML;
// Hook up events for the row.
function setUpRow(row) {
const rowRefs = getRefs(row);
rowRefs.removeButton.onclick = (e) => {
e.preventDefault();
removeRow(row);
};
}
// Enable or disable addButton as necessary.
function updateAddButton() {
if ($refs.rows.children.length >= $props.maxRows) {
$refs.addButton.setAttribute('disabled', '');
return;
}
$refs.addButton.removeAttribute('disabled');
}
// Update array key values to the row number
function updateFieldNames() {
[...$refs.rows.children]
.forEach((el, index) => {
el.querySelectorAll('[name]')
.forEach(el => {
const newName = el.getAttribute('name').replace(/\[\d\]/gm, `[${index}]`);
el.setAttribute('name', newName);
});
});
}
function addRow() {
if (
!rowHTML ||
$refs.rows.children.length >= $props.maxRows
) return;
let newRow = createFromHTML(rowHTML);
newRow.removeAttribute('id');
setUpRow(newRow);
$refs.rows.appendChild(newRow);
newRow.querySelector('input,textarea,select').focus();
updateFieldNames();
updateAddButton();
}
function removeRow(row) {
if ($refs.rows.children.length <= 1) return;
row.remove();
$refs.rows.focus();
updateFieldNames();
updateAddButton();
}
function init() {
setUpRow($refs.rows.children[0]);
$refs.addButton.onclick = (e) => {
e.preventDefault();
addRow();
}
updateFieldNames();
}
init();
}
// Return an object that contains references to DOM objects.
function getRefs(el) {
let result = {};
[...el.querySelectorAll('[data-ref]')]
.forEach(ref => {
result[ref.dataset.ref] = ref;
});
return result;
}
function setDefaults(obj, defaults) {
let results = obj;
for (const prop in defaults) {
if (!obj.hasOwnProperty(prop)) {
results[prop] = defaults[prop];
}
}
return results;
}
// Get initial component data from the `data-props` attribute in JSON format.
function getProps(el, defaults={}) {
return setDefaults(
JSON.parse(el.dataset.props ?? '{}'),
defaults
);
}
// Create a new element from an HTML string.
function createFromHTML(html='') {
let element = document.createElement(null);
element.innerHTML = html;
return element.firstElementChild;
}

View File

@@ -5,5 +5,93 @@
<h2 class="text-3xl font-semibold text-gray-800 dark:text-gray-200">Client - {{ customer.raisonSocial }} - Création devis / avis de paiement / facture</h2>
</div>
<form method="post" class="mt-5 bg-gray-800 rounded-lg shadow-lg p-6 space-y-4">
<div class="flex space-x-4">
<div class="flex-1">
<div class="mb-5">
<label for="type" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Type</label>
<select id="type" name="type" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
<option value="avis">Avis de paiement</option>
<option value="devis">Devis</option>
<option value="facture">Facture</option>
</select>
</div>
</div>
<div class="flex-1">
<div class="mb-5">
<label for="num" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Numéro</label>
<input type="text" name="num" id="num_devis" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" required value="AVIS-" />
</div>
</div>
<div class="flex-1">
<div class="mb-5">
<label for="date" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Date</label>
<input type="datetime-local" name="date" id="date" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" required />
</div>
</div>
</div>
<fieldset class="form-section">
<legend>
<h2>Guests</h2>
</legend>
<div class="form-repeater" data-component="repeater" is="repeat-line">
<ol class="form-repeater__rows" data-ref="rows" tabindex="0">
<!-- This element will be repeated: -->
<li class="form-repeater__row">
<fieldset class="form-group form-group--horizontal">
<div class="flex space-x-4">
<div class="flex-1">
<div class="form-field">
<div class="mb-1">
<label for="titre" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Titre</label>
<input type="text" name="titre" id="lines[0]['title]" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" required />
</div>
<div class="mb-1">
<label for="price" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Prix TTC</label>
<input type="number" step="0.1" name="titre" id="lines[0]['price]" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" required />
</div>
<button
class="w-full form-repeater__remove-button bg-red-600 hover:bg-red-700 text-white px-3 py-1 rounded"
data-ref="removeButton"
type="button"
>
Supprimer la ligne
</button>
</div>
</div>
<div class="flex-1">
<div class="form-field">
<div class="mb-5">
<label for="description" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Description</label>
<textarea rows="7" type="text" name="description" id="lines[0]['description]" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" required></textarea>
</div>
</div>
</div>
</div>
</fieldset>
</li>
</ol>
<button
class="form-repeater__add-button w-full bg-purple-600 hover:bg-purple-700 text-white px-3 py-1 rounded"
data-ref="addButton"
type="button"
>
+ Ajouter une ligne
</button>
</div>
</fieldset>
<button
class="w-full bg-purple-600 hover:bg-purple-700 text-white px-3 py-1 rounded"
type="button"
>
Enregistrer
</button>
</form>
{% endblock %}