Refactor forms.js and generate custom form elements in the template.

This commit is contained in:
Daru13 2020-09-23 15:13:39 +02:00
parent 731e69a80b
commit 290bb67293
4 changed files with 501 additions and 166 deletions

View file

@ -374,6 +374,9 @@ body {
column-gap: 10px;
margin: 0 0 10px 0;
}
#content-edit-profile form .form-sub-entry.hidden {
display: none;
}
#content-edit-profile form .form-sub-entry :nth-child(1) {
grid-area: type-input;
}
@ -393,6 +396,12 @@ body {
#content-edit-profile form .form-sub-entry .remove-button:active {
background-color: #973232;
}
#content-edit-profile form .form-sub-entry input[type=checkbox] {
display: none;
}
#content-edit-profile form .form-sub-entry-template {
display: none;
}
#content-edit-profile form .add-sub-entry-button {
margin: 0 auto 0 auto;
}

View file

@ -1,80 +1,90 @@
"use strict";
function countSubEntries(formEntryElement) {
return formEntryElement
.querySelectorAll(".form-sub-entry")
.length;
}
// function countSubEntries(formEntryElement) {
// return formEntryElement
// .querySelectorAll(".form-sub-entry")
// .length;
// }
function reindexSubEntries(formEntryElement) {
const subEntryElements = [...formEntryElement.querySelectorAll(".form-sub-entry")];
for (let [subEntryIndex, subEntryElement] of subEntryElements.entries()) {
subEntryElement.setAttribute("data-sub-entry-index", subEntryIndex);
}
}
// function reindexSubEntries(formEntryElement) {
// const subEntryElements = [...formEntryElement.querySelectorAll(".form-sub-entry")];
// for (let [subEntryIndex, subEntryElement] of subEntryElements.entries()) {
// subEntryElement.setAttribute("data-sub-entry-index", subEntryIndex);
// }
// }
function createAndAppendNewSubEntryButton(formEntryElement) {
const buttonElement = document.createElement("button");
buttonElement.setAttribute("type", "button");
buttonElement.classList.add("add-sub-entry-button");
// function checkSubEntryDeleteCheckbox(subEntryElement) {
// const checkboxElement = [...subEntryElement.querySelectorAll('input[type="checkbox"')]
// .filter(element => element.className.endsWith("DELETE"));
// if (checkboxElement.length >= 1) {
// checkboxElement.checked = true;
// }
// }
// TODO: handle the translation of this button
buttonElement.textContent = "New entry";
buttonElement.addEventListener("click", event => {
// Clone one of the sub-entries, make it look like it is new, and reindex the sub-entries
// This works because there is always at least one sub-entry to clone
const someSubEntryElement = formEntryElement.querySelector(".form-sub-entry");
const clonedSubEntryElement = someSubEntryElement.cloneNode(true);
// function createAndAppendNewSubEntryButton(formEntryElement) {
// const buttonElement = document.createElement("button");
// buttonElement.setAttribute("type", "button");
// buttonElement.classList.add("add-sub-entry-button");
for (let inputElement of clonedSubEntryElement.querySelectorAll("input")) {
inputElement.value = "";
}
// // TODO: handle the translation of this button
// buttonElement.textContent = "New entry";
// Since the cloned removal button has lost its click event listener,
// it should be created again from scratch
clonedSubEntryElement
.querySelector(".remove-button")
.remove();
createAndAppendSubEntryDeletionButton(clonedSubEntryElement);
// buttonElement.addEventListener("click", event => {
// // Clone one of the sub-entries, make it look like it is new, and reindex the sub-entries
// // This works because there is always at least one sub-entry to clone
// const someSubEntryElement = formEntryElement.querySelector(".form-sub-entry");
// const clonedSubEntryElement = someSubEntryElement.cloneNode(true);
formEntryElement.insertBefore(clonedSubEntryElement, buttonElement);
reindexSubEntries(formEntryElement);
});
// for (let inputElement of clonedSubEntryElement.querySelectorAll("input")) {
// inputElement.value = "";
// }
formEntryElement.append(buttonElement);
}
// // Since the cloned removal button has lost its click event listener,
// // it should be created again from scratch
// clonedSubEntryElement
// .querySelector(".remove-button")
// .remove();
// createAndAppendSubEntryDeletionButton(clonedSubEntryElement);
function createAndAppendSubEntryDeletionButton(subEntryElement) {
const buttonElement = document.createElement("button");
buttonElement.setAttribute("type", "button");
buttonElement.classList.add("remove-button");
// formEntryElement.insertBefore(clonedSubEntryElement, buttonElement);
// reindexSubEntries(formEntryElement);
// });
buttonElement.addEventListener("click", event => {
const parentFormEntryElement = subEntryElement.parentNode;
// formEntryElement.append(buttonElement);
// }
// If this sub-entry is the last one, only clear its input fields
// Otherwise, remove the node from the form entry
if (countSubEntries(parentFormEntryElement) === 1) {
subEntryElement
.querySelectorAll("input")
.value = "";
}
else {
subEntryElement.remove();
reindexSubEntries(parentFormEntryElement);
}
});
// function createAndAppendSubEntryDeletionButton(subEntryElement) {
// const buttonElement = document.createElement("button");
// buttonElement.setAttribute("type", "button");
// buttonElement.classList.add("remove-button");
subEntryElement.append(buttonElement);
}
// buttonElement.addEventListener("click", event => {
// const parentFormEntryElement = subEntryElement.parentNode;
function createSubEntryElement() {
const subEntryElement = document.createElement("div");
subEntryElement.classList.add("form-sub-entry");
// // If this sub-entry is the last one, only clear its input fields
// // Otherwise, remove the node from the form entry
// if (countSubEntries(parentFormEntryElement) === 1) {
// subEntryElement
// .querySelectorAll("input")
// .value = "";
// }
// else {
// subEntryElement.remove();
// reindexSubEntries(parentFormEntryElement);
// }
// });
return subEntryElement;
}
// subEntryElement.append(buttonElement);
// }
// function createSubEntryElement() {
// const subEntryElement = document.createElement("div");
// subEntryElement.classList.add("form-sub-entry");
// return subEntryElement;
// }
// ----------------------------------------------------------------------------
@ -82,117 +92,341 @@ function createSubEntryElement() {
// TODO: remove this hacky function by directly grouping all the elements of each sub-entry
// directly in Django (e.g. <div> elements with a "form-sub-entry" class)
function transformFormEntryWithSubEntries(formEntryElement) {
// Nb. successive children to group together
// (6 - 4 = 2, since the checkbox and all the label are ignored)
const nbSuccessiveElementsPerSubEntry = 2;
// function transformFormEntryWithSubEntries(formEntryElement) {
// // Nb. successive children to group together
// // (6 - 4 = 2, since the checkbox and all the label are ignored)
// const nbSuccessiveElementsPerSubEntry = 2;
const subEntryElements = [];
let currentSubEntryElement = createSubEntryElement();
let nbChildrenOfCurrentSubEntry = 0;
subEntryElements.push(currentSubEntryElement);
// const subEntryElements = [];
// let currentSubEntryElement = createSubEntryElement();
// let nbChildrenOfCurrentSubEntry = 0;
// subEntryElements.push(currentSubEntryElement);
const formEntryChildNodes = [...formEntryElement.childNodes];
let firstElementHasBeenSkipped = false;
// const formEntryChildNodes = [...formEntryElement.childNodes];
// let firstElementHasBeenSkipped = false;
for (let childNode of formEntryChildNodes) {
// Ignore non-element nodes
if (childNode.nodeType !== Node.ELEMENT_NODE) {
continue;
}
// for (let childNode of formEntryChildNodes) {
// // Ignore non-element nodes
// if (childNode.nodeType !== Node.ELEMENT_NODE) {
// continue;
// }
// Skip the first element (it should be the label of the whole form entry)
if (!firstElementHasBeenSkipped) {
firstElementHasBeenSkipped = true;
continue;
}
// // Skip the first element (it should be the label of the whole form entry)
// if (!firstElementHasBeenSkipped) {
// firstElementHasBeenSkipped = true;
// continue;
// }
// Ignore hidden form elements
if (childNode.hasAttribute("type")
&& childNode.getAttribute("type") === "hidden") {
continue;
}
// // Ignore hidden form elements
// if (childNode.hasAttribute("type")
// && childNode.getAttribute("type") === "hidden") {
// continue;
// }
// Remove the checkbox to delete an entry and its label
if (childNode.hasAttribute("id")
&& childNode.getAttribute("id").endsWith("DELETE")) {
childNode.remove();
continue;
}
// // Remove the checkbox to delete an entry and its label
// if (childNode.hasAttribute("id")
// && childNode.getAttribute("id").endsWith("DELETE")) {
// //childNode.remove();
// continue;
// }
if (childNode.hasAttribute("for")
&& childNode.getAttribute("for").endsWith("DELETE")) {
childNode.remove();
continue;
}
// if (childNode.hasAttribute("for")
// && childNode.getAttribute("for").endsWith("DELETE")) {
// //childNode.remove();
// continue;
// }
// Remove all the remaining labels
// (they should be replaced by placeholder text within the input)
if (childNode.nodeName === "LABEL") {
childNode.remove();
continue;
}
// // Remove all the remaining labels
// // (they should be replaced by placeholder text within the input)
// if (childNode.nodeName === "LABEL") {
// childNode.remove();
// continue;
// }
// If the current sub-entry is full,
// create a new sub-entry (for the remaining child nodes)
if (nbChildrenOfCurrentSubEntry === nbSuccessiveElementsPerSubEntry) {
currentSubEntryElement = createSubEntryElement();
nbChildrenOfCurrentSubEntry = 0;
subEntryElements.push(currentSubEntryElement);
}
// // If the current sub-entry is full,
// // create a new sub-entry (for the remaining child nodes)
// if (nbChildrenOfCurrentSubEntry === nbSuccessiveElementsPerSubEntry) {
// currentSubEntryElement = createSubEntryElement();
// nbChildrenOfCurrentSubEntry = 0;
// subEntryElements.push(currentSubEntryElement);
// }
// Fill the current sub-entry (until it becomes full)
// Note: the order MUST be preserved for the CSS to work (see _content.scss for details)
currentSubEntryElement.append(childNode);
nbChildrenOfCurrentSubEntry += 1;
}
// // Fill the current sub-entry (until it becomes full)
// // Note: the order MUST be preserved for the CSS to work (see _content.scss for details)
// currentSubEntryElement.append(childNode);
// nbChildrenOfCurrentSubEntry += 1;
// }
// For each sub-entry element:
// - add placeholder text in each input field
// - add a button to remove the sub-entry
// Note: this method is not robust since it assumes the two elements always exist
// and it identifies them by their relative order only!
for (let [subEntryIndex, subEntryElement] of subEntryElements.entries()) {
const subEntryInputElements = subEntryElement.querySelectorAll("input");
subEntryInputElements[0].setAttribute("placeholder", "Type"); // TODO: handle translation
subEntryInputElements[1].setAttribute("placeholder", "Value"); // TODO: handle translation
// // For each sub-entry element:
// // - add placeholder text in each input field
// // - add a button to remove the sub-entry
// // Note: this method is not robust since it assumes the two elements always exist
// // and it identifies them by their relative order only!
// for (let [subEntryIndex, subEntryElement] of subEntryElements.entries()) {
// const subEntryInputElements = subEntryElement.querySelectorAll("input");
// subEntryInputElements[0].setAttribute("placeholder", "Type"); // TODO: handle translation
// subEntryInputElements[1].setAttribute("placeholder", "Value"); // TODO: handle translation
createAndAppendSubEntryDeletionButton(subEntryElement);
}
// createAndAppendSubEntryDeletionButton(subEntryElement);
// }
// Finally append all the sub-entry elements to the form entry
formEntryElement.append(...subEntryElements);
reindexSubEntries(formEntryElement);
}
// // Finally append all the sub-entry elements to the form entry
// formEntryElement.append(...subEntryElements);
// reindexSubEntries(formEntryElement);
// }
// TODO: remove this hacky setup (cf. the TODO above)
document.addEventListener("DOMContentLoaded", event => {
// Create a list of all the form entry elements which contain sub entries
// by filtering the list of all the form entry elements
const targetForAttributeValues = [
"id_phone",
"id_social",
"id_mail",
"id_address"
];
// document.addEventListener("DOMContentLoaded", event => {
// // Create a list of all the form entry elements which contain sub entries
// // by filtering the list of all the form entry elements
// const targetForAttributeValues = [
// "id_phone",
// "id_social",
// "id_mail",
// "id_address"
// ];
const formEntryElementsWithSubEntries = [...document.querySelectorAll("#content-edit-profile .form-entry")]
.filter(formEntryElement => {
return [...formEntryElement.childNodes]
.some(formEntryChildElement => {
return formEntryChildElement.nodeType === Node.ELEMENT_NODE
&& formEntryChildElement.hasAttribute("for")
&& targetForAttributeValues.includes(formEntryChildElement.getAttribute("for"))
});
});
// const formEntryElementsWithSubEntries = [...document.querySelectorAll("#content-edit-profile .form-entry")]
// .filter(formEntryElement => {
// return [...formEntryElement.childNodes]
// .some(formEntryChildElement => {
// return formEntryChildElement.nodeType === Node.ELEMENT_NODE
// && formEntryChildElement.hasAttribute("for")
// && targetForAttributeValues.includes(formEntryChildElement.getAttribute("for"))
// });
// });
// Transform the remaining form entry elements to fix their structure
for (let formEntryElement of formEntryElementsWithSubEntries) {
transformFormEntryWithSubEntries(formEntryElement);
createAndAppendNewSubEntryButton(formEntryElement);
}
});
// // Transform the remaining form entry elements to fix their structure
// for (let formEntryElement of formEntryElementsWithSubEntries) {
// //transformFormEntryWithSubEntries(formEntryElement);
// //createAndAppendNewSubEntryButton(formEntryElement);
// }
// });
// ----------------------------------------------------------------------------
class SubEntry {
constructor(subEntryElement, index, parentFormEntry) {
this.parentFormEntry = parentFormEntry;
this.index = index;
this.subEntryElement = subEntryElement;
this.removeButtonElement = subEntryElement.querySelector(".remove-button");
this.deletionCheckboxElement = subEntryElement.querySelector("input[type=checkbox]");
this.addPlaceholderAttributes();
this.startHandlingSubEntryRemoveButtonClicks();
}
addPlaceholderAttributes() {
// Add the type placeholder to the first input element
// and the value placeholder to the second one
const inputElements = this.subEntryElement.querySelectorAll("input");
inputElements[0].setAttribute("placeholder", this.parentFormEntry.placeholders.type);
inputElements[1].setAttribute("placeholder", this.parentFormEntry.placeholders.value);
}
hide() {
this.subEntryElement.classList.add("hidden");
}
display() {
this.subEntryElement.classList.remove("hidden");
}
markForDeletionAndHide() {
// Check Django's deletion checkbox
this.deletionCheckboxElement.checked = true;
this.hide();
}
reset() {
// Clear all input fields
for (let inputElement of this.subEntryElement.querySelectorAll("input")) {
inputElement.value = "";
}
// Uncheck Django's deletion checkbox
this.deletionCheckboxElement.checked = false;
}
resetAndDisplay() {
this.reset();
this.display();
}
reindex(newIndex) {
// Replace -<old index>- by -<new index>- in the values of the
// id and name attributes of every child of this sub-entry's element
const attributesToUpdate = ["id", "name"];
for (let childElement of this.subEntryElement.childNodes) {
for (let attribute of attributesToUpdate) {
if (!childElement.hasAttribute(attribute)) {
continue;
}
const newValue = childElement
.getAttribute(attribute)
.replace(`-${this.index}-`, `-${newIndex}-`);
childElement.setAttribute(attribute, newValue);
}
}
this.index = newIndex;
}
startHandlingSubEntryRemoveButtonClicks() {
this.removeButtonElement.addEventListener("click", event => {
event.preventDefault();
this.parentFormEntry.removeSubEntry(this);
});
}
static fromTemplate(templateElement, index, parentFormEntry) {
// Clone the template, update its class,
// and replace __prefix__ by the index of the new sub-entry
const clonedTemplateElement = templateElement.cloneNode(true);
clonedTemplateElement.innerHTML = clonedTemplateElement.innerHTML
.replace(/__prefix__/g, index.toString());
clonedTemplateElement.className = "form-sub-entry";
return new SubEntry(clonedTemplateElement, index, parentFormEntry);
}
}
class MultiEntryFormEntry {
constructor(formEntryElement) {
this.formEntryElement = formEntryElement;
this.subEntryTemplateElement = formEntryElement.querySelector(".form-sub-entry-template");
this.newSubEntryButtonElement = formEntryElement.querySelector(".add-sub-entry-button");
this.totalFormsElement = [...formEntryElement.querySelectorAll("input")]
.find(element => element.name && element.name.endsWith("TOTAL_FORMS"));
this.maxNumFormsElement = [...formEntryElement.querySelectorAll("input")]
.find(element => element.name && element.name.endsWith("MAX_NUM_FORMS"));
this.placeholders = {
type: this.formEntryElement.getAttribute("data-type-placeholder"),
value: this.formEntryElement.getAttribute("data-value-placeholder")
};
this.maxNbSubEntries = parseInt(this.maxNumFormsElement.value);
this.initialNbSubEntries = 0;
this.subEntries = [];
this.createInitialSubEntries();
this.startHandlingNewSubEntryButtonClicks();
}
get nbSubEntries() {
return this.subEntries
.filter(subEntry =>
!subEntry.subEntryElement.classList.contains("hidden")
)
.length;
}
get hasHiddenSubEntries() {
return this.subEntries
.findIndex(subEntry =>
subEntry.subEntryElement.classList.contains("hidden")
) >= 0;
}
createInitialSubEntries() {
const subEntryElements = this.formEntryElement.querySelectorAll(".form-sub-entry");
this.initialNbSubEntries = subEntryElements.length;
// This loop assumes sub-entries elements are
// ordered with no gap according to indices which start at 0
for (let [index, element] of [...subEntryElements].entries()) {
this.subEntries.push(
new SubEntry(element, index, this)
);
}
}
createNewSubEntry() {
if (this.nbSubEntries === this.maxNbSubEntries) {
console.log(`The max. number of sub-entries has been reached (${this.maxNbSubEntries}).`);
return;
}
// If there are hidden sub-entries,
// it means one of the initial sub-entry elements should be reset and displayed
if (this.hasHiddenSubEntries) {
// Reset and display the first hidden sub-entry
const existingSubEntry = this.subEntries.find(
subEntry => subEntry.subEntryElement.classList.contains("hidden")
);
existingSubEntry.resetAndDisplay();
}
// Otherwise, it means a new sub-entry (element) must be created
// and appended to the form entry element
else {
const newSubEntryIndex = this.nbSubEntries;
const newSubEntry = SubEntry.fromTemplate(
this.subEntryTemplateElement,
newSubEntryIndex,
this
);
this.subEntries.push(newSubEntry);
this.formEntryElement.insertBefore(
newSubEntry.subEntryElement,
this.newSubEntryButtonElement
);
// Increment Django's TOTAL_FORMS hidden form input
this.totalFormsElement.value = (parseInt(this.totalFormsElement.value) + 1).toString();
}
}
removeSubEntry(subEntry) {
// If the index of the sub-entry to remove is below the initial number of sub-entries,
// it means one of the initial sub entry elements should be marked for deletion and hidden
const removedSubEntryIndex = subEntry.index;
if (removedSubEntryIndex < this.initialNbSubEntries) {
subEntry.markForDeletionAndHide();
}
// Otherwise, delete the sub-entry (and remove its element from the DOM)
// and reindex other user-created sub-entries if need be
else {
subEntry.subEntryElement.remove();
this.subEntries.splice(subEntry.index, 1);
for (let index = this.removeSubEntryIndex; index < this.subEntries.length; index++) {
this.subEntries[index].reindex(index);
}
// Decrement Django's TOTAL_FORMS hidden form input
this.totalFormsElement.value = (parseInt(this.totalFormsElement.value) - 1).toString();
}
}
startHandlingNewSubEntryButtonClicks() {
const buttonElement = this.formEntryElement.querySelector(".add-sub-entry-button");
buttonElement.addEventListener("click", event => {
this.createNewSubEntry();
});
}
}
// Setup the script by creating one instance of MultiEntryFormEntry
// for each form entry configured to contain several sub-entries
document.addEventListener("DOMContentLoaded", event => {
const multiEntryFormEntryElements = document.querySelectorAll(
"#content-edit-profile .form-entry.multi-entry"
);
for (let element of multiEntryFormEntryElements) {
const formEntry = new MultiEntryFormEntry(element);
console.log("New form entry:", formEntry);
}
});

View file

@ -199,6 +199,10 @@
column-gap: 10px;
margin: 0 0 10px 0;
&.hidden {
display: none;
}
// Since the different labels and inputs are not obvious to identifiate using CSS selectors,
// they are selected one after the other using their natural order in the DOM
// TODO: make this more robust by giving proper class names to each sub-entry element
@ -219,6 +223,14 @@
.remove-button:active {
background-color: #973232;
}
input[type="checkbox"] {
display: none;
}
}
.form-sub-entry-template {
display: none;
}
.add-sub-entry-button {

View file

@ -6,7 +6,7 @@
<div id="content-edit-profile" class="content">
<h2>{% trans "Modifier ma page d'annuaire" %}</h2>
{{ form.errors }}
{{ form.non_field_errors }}
<form method="post" action="" enctype="multipart/form-data">
{% csrf_token %}
<div class="form-entry">
@ -63,21 +63,101 @@
<label for="id_thurne">{% trans "Thurne :" %}</label>
{{ form.thurne }}
</div>
<div class="form-entry">
<div
class="form-entry multi-entry"
data-type-placeholder="{% trans "Personnel" %}"
data-value-placeholder="{% trans "0612345678" %}"
>
{{ phone_form.non_field_errors }}
{{ phone_form.management_form }}
<label for="id_phone">{% trans "Numéro(s) de téléphone :" %}</label>
{{ phone_form }}
{% for form in phone_form %}
<div class="form-sub-entry">
{{ form.name }}
{{ form.number }}
{{ form.DELETE }}
<button type=button" class="remove-button"></button>
</div>
{% endfor %}
<div class="form-sub-entry-template">
{{ phone_form.empty_form.name }}
{{ phone_form.empty_form.number }}
{{ phone_form.empty_form.DELETE }}
<button type="button" class="remove-button"></button>
</div>
<button type="button" class="add-sub-entry-button">{% trans "Ajouter un numéro" %}</button>
</div>
<div class="form-entry">
<label for="id_social">{% trans "Réseaux sociaux :" %}</label>
{{ social_form }}
<div
class="form-entry multi-entry"
data-type-placeholder="{% trans "InstaTok" %}"
data-value-placeholder="{% trans "mon_profil_instatok" %}"
>
{{ social_form.non_field_errors }}
{{ social_form.management_form }}
<label for="id_social">{% trans "Réseaux sociaux :" %}</label>
{% for form in social_form %}
<div class="form-sub-entry">
{{ form.name }}
{{ form.content }}
{{ form.DELETE }}
<button type=button" class="remove-button"></button>
</div>
{% endfor %}
<div class="form-sub-entry-template">
{{ social_form.empty_form.name }}
{{ social_form.empty_form.content }}
{{ social_form.empty_form.DELETE }}
<button type="button" class="remove-button"></button>
</div>
<button type="button" class="add-sub-entry-button">{% trans "Ajouter un réseau social" %}</button>
</div>
<div class="form-entry">
<label for="id_mail">{% trans "Mail(s):" %}</label>
{{ mail_form }}
<div
class="form-entry multi-entry"
data-type-placeholder="{% trans "Professionelle" %}"
data-value-placeholder="{% trans "moi@ens.fr" %}"
>
{{ mail_form.non_field_errors }}
{{ mail_form.management_form }}
<label for="id_mail">{% trans "Mail(s):" %}</label>
{% for form in mail_form %}
<div class="form-sub-entry">
{{ form.name }}
{{ form.mail }}
{{ form.DELETE }}
<button type=button" class="remove-button"></button>
</div>
{% endfor %}
<div class="form-sub-entry-template">
{{ mail_form.empty_form.name }}
{{ mail_form.empty_form.mail }}
{{ mail_form.empty_form.DELETE }}
<button type="button" class="remove-button"></button>
</div>
<button type="button" class="add-sub-entry-button">{% trans "Ajouter un email" %}</button>
</div>
<div class="form-entry">
<label for="id_address">{% trans "Adresse(s):" %}</label>
{{ address_form }}
<div
class="form-entry multi-entry"
data-type-placeholder="{% trans "Bureau" %}"
data-value-placeholder="{% trans "45 rue d'Ulm" %}"
>
{{ address_form.non_field_errors }}
{{ address_form.management_form }}
<label for="id_address">{% trans "Adresse(s):" %}</label>
{% for form in address_form %}
<div class="form-sub-entry">
{{ form.name }}
{{ form.content }}
{{ form.DELETE }}
<button type=button" class="remove-button"></button>
</div>
{% endfor %}
<div class="form-sub-entry-template">
{{ address_form.empty_form.name }}
{{ address_form.empty_form.content }}
{{ address_form.empty_form.DELETE }}
<button type="button" class="remove-button"></button>
</div>
<button type="button" class="add-sub-entry-button">{% trans "Ajouter une adresse" %}</button>
</div>
<div class="form-entry">
<label for="id_text_field">{% trans "Champ libre :" %}</label>