web-dev-qa-db-fra.com

Spring 3 MVC: un à plusieurs dans un formulaire dynamique (ajout / suppression lors de la création / mise à jour)

Je cherche une solution pour gérer une relation un-à-plusieurs dans un formulaire HTML en utilisant jQuery. Je développe avec Spring, Spring MVC et Hibernate. J'ai trouvé de nombreuses pistes sur le Web, mais aucun exemple complet ne fonctionnait.

L'arrière-plan

J'ai trois entités JPA:

Consult.Java (1)

@Entity
@Table(name = "consult")
public class Consult

    private Integer id;
    private String label;
    private Set<ConsultTechno> consultTechnos;

    /* getters & setters */

}

ConsultTechno.Java (2)

@Entity
@Table(name = "consult_techno")
public class ConsultTechno {

    private Integer id;
    private Techno techno;
    private Consult consult;
    private String level;

    /* getters & setters */

}

Techno.Java (3)

@Entity
@Table(name="techno")
public class Techno {

    private Integer id;
    private String label;
    private Set<ConsultTechno> consultTechnos;

    /* getters & setters */

}

Comme indiqué, un Consult (1) contient n ConsultTechnos (2), qui sont caractérisés par un niveau et une Techno (3).

Les besoins

En utilisant un formulaire HTML, j'aimerais avoir un Add a techno bouton qui ajoute dynamiquement deux champs dans le DOM:

<input type="text" name="consult.consultTechnos[].techno.id" />
<input type="text" name="consult.consultTechnos[].level" />

Bien sûr, chaque fois que l'utilisateur clique sur le bouton, ces deux champs doivent être rajoutés, etc. J'ai choisi input type="text" pour l'exemple, mais à la fin, les champs seront deux select.

Quatre types d'opérations devraient être couverts:

  1. Ajouter une entité enfant lorsque création une nouvelle entité maître
  2. Supprimer une entité enfant lorsque création une nouvelle entité maître
  3. Ajouter une entité enfant lorsque mise à jour une nouvelle entité maître
  4. Supprimer une entité enfant lorsque mise à jour une nouvelle entité maître

Le problème

Cette partie de mise en page fonctionne déjà, mais lors de la publication du formulaire, je n'arrive pas à lier les champs ajoutés dynamiquement à mon @ModelAttribute consult.

Avez-vous une idée de comment faire ce genre de travail? J'espère avoir été assez clair ...

Merci d'avance :)

37
sp00m

Ce point est encore assez confus et peu clair sur le Web, voici donc la façon dont j'ai résolu mon problème. Cette solution n'est probablement pas la plus optimisée, mais elle fonctionne lorsque crée et met à jour une entité maître.

Théorie

  1. Utilisez un List au lieu d'un Set pour vos relations un-à-plusieurs qui doivent être gérées dynamiquement.

  2. Initialisez votre List sous la forme d'un AutoPopulatingList . C'est une liste paresseuse qui permet d'ajouter dynamiquement des éléments.

  3. Ajoutez un attribut remove de int à votre entité enfant. Cela jouera le rôle d'un drapeau booléen et sera utile lorsque supprimera dynamiquement un élément.

  4. Lors de la publication du formulaire, ne conservez que les éléments qui ont le drapeau remove sur 0 (c'est-à-dire false).

Entraine toi

Un exemple complet: un employeur a de nombreux employés, un employé a un employeur.

Entités:

Employer.Java

@Entity
@Table(name = "employer")
public class Employer

    private Integer id;

    private String firstname;
    private String lastname;
    private String company;

    private List<Employee> employees; // one-to-many

    /* getters & setters */

}

Employee.Java

@Entity
@Table(name = "employee")
public class Employee {

    private Integer id;

    @Transient // means "not a DB field"
    private Integer remove; // boolean flag

    private String firstname;
    private String lastname;

    private Employer employer; // many-to-one

    /* getters & setters */

}

Manette:

EmployerController.Java

@Controller
@RequestMapping("employer")
public class EmployerController {

    // Manage dynamically added or removed employees
    private List<Employee> manageEmployees(Employer employer) {
        // Store the employees which shouldn't be persisted
        List<Employee> employees2remove = new ArrayList<Employee>();
        if (employer.getEmployees() != null) {
            for (Iterator<Employee> i = employer.getEmployees().iterator(); i.hasNext();) {
                Employee employee = i.next();
                // If the remove flag is true, remove the employee from the list
                if (employee.getRemove() == 1) {
                    employees2remove.add(employee);
                    i.remove();
                // Otherwise, perform the links
                } else {
                    employee.setEmployer(employer);
                }
            }
        }
        return employees2remove;
    }

    // -- Creating a new employer ----------

    @RequestMapping(value = "create", method = RequestMethod.GET)
    public String create(@ModelAttribute Employer employer, Model model) {
        // Should init the AutoPopulatingList
        return create(employer, model, true);
    }

    private String create(Employer employer, Model model, boolean init) {
        if (init) {
            // Init the AutoPopulatingList
            employer.setEmployees(new AutoPopulatingList<Employee>(Employee.class));
        }
        model.addAttribute("type", "create");
        return "employer/edit";
    }

    @RequestMapping(value = "create", method = RequestMethod.POST)
    public String create(@Valid @ModelAttribute Employer employer, BindingResult bindingResult, Model model) {
        if (bindingResult.hasErrors()) {
            // Should not re-init the AutoPopulatingList
            return create(employer, model, false);
        }
        // Call the private method
        manageEmployees(employer);
        // Persist the employer
        employerService.save(employer);
        return "redirect:employer/show/" + employer.getId();
    }

    // -- Updating an existing employer ----------

    @RequestMapping(value = "update/{pk}", method = RequestMethod.GET)
    public String update(@PathVariable Integer pk, @ModelAttribute Employer employer, Model model) {
        // Add your own getEmployerById(pk)
        model.addAttribute("type", "update");
        return "employer/edit";
    }

    @RequestMapping(value = "update/{pk}", method = RequestMethod.POST)
    public String update(@PathVariable Integer pk, @Valid @ModelAttribute Employer employer, BindingResult bindingResult, Model model) {
        // Add your own getEmployerById(pk)
        if (bindingResult.hasErrors()) {
            return update(pk, employer, model);
        }
        List<Employee> employees2remove = manageEmployees(employer);
        // First, save the employer
        employerService.update(employer);
        // Then, delete the previously linked employees which should be now removed
        for (Employee employee : employees2remove) {
            if (employee.getId() != null) {
                employeeService.delete(employee);
            }
        }
        return "redirect:employer/show/" + employer.getId();
    }

    // -- Show an existing employer ----------

    @RequestMapping(value = "show/{pk}", method = RequestMethod.GET)
    public String show(@PathVariable Integer pk, @ModelAttribute Employer employer) {
        // Add your own getEmployerById(pk)
        return "employer/show";
    }

}

Vue:

employer/edit.jsp

<%@ taglib prefix="c" uri="http://Java.Sun.com/jsp/jstl/core"
%><%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"
%><%@ taglib prefix="fn" uri="http://Java.Sun.com/jsp/jstl/functions"
%><%@ page language="Java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"
%><!DOCTYPE HTML>
<html>
<head>

    <title>Edit</title>
    <style type="text/css">.hidden {display: none;}</style>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.2/jquery.min.js"></script>
    <script type="text/javascript">
    $(function() {

        // Start indexing at the size of the current list
        var index = ${fn:length(employer.employees)};

        // Add a new Employee
        $("#add").off("click").on("click", function() {
            $(this).before(function() {
                var html = '<div id="employees' + index + '.wrapper" class="hidden">';                    
                html += '<input type="text" id="employees' + index + '.firstname" name="employees[' + index + '].firstname" />';
                html += '<input type="text" id="employees' + index + '.lastname" name="employees[' + index + '].lastname" />';
                html += '<input type="hidden" id="employees' + index + '.remove" name="employees[' + index + '].remove" value="0" />';
                html += '<a href="#" class="employees.remove" data-index="' + index + '">remove</a>';                    
                html += "</div>";
                return html;
            });
            $("#employees" + index + "\\.wrapper").show();
            index++;
            return false;
        });

        // Remove an Employee
        $("a.employees\\.remove").off("click").on("click", function() {
            var index2remove = $(this).data("index");
            $("#employees" + index2remove + "\\.wrapper").hide();
            $("#employees" + index2remove + "\\.remove").val("1");
            return false;
        });

    });
    </script>

</head>
<body>

    <c:choose>
        <c:when test="${type eq 'create'}"><c:set var="actionUrl" value="employer/create" /></c:when>
        <c:otherwise><c:set var="actionUrl" value="employer/update/${employer.id}" /></c:otherwise>
    </c:choose>

    <form:form action="${actionUrl}" modelAttribute="employer" method="POST" name="employer">
        <form:hidden path="id" />
        <table>
            <tr>
                <td><form:label path="firstname">Firstname</form:label></td>
                <td><form:input path="firstname" /><form:errors path="firstname" /></td>
            </tr>
            <tr>
                <td><form:label path="lastname">Lastname</form:label></td>
                <td><form:input path="lastname" /><form:errors path="lastname" /></td>
            </tr>
            <tr>
                <td><form:label path="company">company</form:label></td>
                <td><form:input path="company" /><form:errors path="company" /></td>
            </tr>
            <tr>
                <td>Employees</td>
                <td>
                    <c:forEach items="${employer.employees}" varStatus="loop">
                        <!-- Add a wrapping div -->
                        <c:choose>
                            <c:when test="${employer.employees[loop.index].remove eq 1}">
                                <div id="employees${loop.index}.wrapper" class="hidden">
                            </c:when>
                            <c:otherwise>
                                <div id="employees${loop.index}.wrapper">
                            </c:otherwise>
                        </c:choose>
                            <!-- Generate the fields -->
                            <form:input path="employees[${loop.index}].firstname" />
                            <form:input path="employees[${loop.index}].lastname" />
                            <!-- Add the remove flag -->
                            <c:choose>
                                <c:when test="${employees[loop.index].remove eq 1}"><c:set var="hiddenValue" value="1" /></c:when>
                                <c:otherwise><c:set var="hiddenValue" value="0" /></c:otherwise>
                            </c:choose>
                            <form:hidden path="employees[${loop.index}].remove" value="${hiddenValue}" />
                            <!-- Add a link to remove the Employee -->
                            <a href="#" class="employees.remove" data-index="${loop.index}">remove</a>
                        </div>
                    </c:forEach>
                    <button id="add" type="button">add</button>
                </td>
            </tr>
        </table>
        <button type="submit">OK</button>
    </form:form>

</body>
</html>

J'espère que cela pourrait aider :)

44
sp00m

pourquoi vous utilisez la balise d'entrée HTML au lieu des formulaires taglib de printemps

   <form:form action="yourURL.htm" command="employeeDto">
    <form:input type="text" name="consult.consultTechnos[].techno.id" />
    <form:input type="text" name="consult.consultTechnos[].level" /> 
   <form:form>

et créez une classe EmployeeDto comme, et ajoutez la modelMap.addAttribute("employeeDto", new Employee); dans votre contrôleur

1
pradeep

Eh bien, je viens de résoudre le problème, la source de vue html n'affichera pas le HTML dynamique ajouté. Si vous inspectez les éléments html, l'arborescence DOM vous montrera tous les éléments dynamiques ajoutés mais sur le formulaire Soumettre si vous voyez que tous les éléments seront soumis au serveur, y compris les éléments créés dynamiques.

Une façon de se reproduire est ob form submit, appelez la méthode javascript et placez un point de débogage dans la méthode javascript et vérifiez les valeurs de formulaire submit dans le document> éléments de formulaire en inspectant l'arborescence DOM

0
chakputth