Rendering multiple nested fieldset in laminas form

Hi everyone.
I’m coming back with my fieldset in laminas-form.
I have 5 nested fieldsets like this :
Impact → Effet → Composante → Produit → Activite
I use a loop function to render each element of a fieldset and it works.
I would like to use the Javascript function to add more elements for each fieldset but the data-template does not render my elements as I would like to have.

Here is how I render my elements and this is how I want to render it :

<div class="row clearfix">
	<div class="mb-0 col-lg-4 col-md-4 col-sm-4 form-control-label">                                    
		<label class="" for="ImpactForm[effets][0][composantes][0][produits][0][activites][0][libelle]">Intitulé de l'activité</label>
	</div>
	<div class="mb-0 col-lg-8 col-md-8 col-sm-8">
		<div class="form-group ">
			<input type="text" name="ImpactForm[effets][0][composantes][0][produits][0][activites][0][libelle]" class="form-control champ-requis" value="">            
		</div>
	</div>
	<div class="col-lg-12">
		<div class="form-group error-message mt-0"></div>
	</div>
</div>

How can I render elements in each fieldset like the code above ?

I post all my code below :

  1. The main form

<?php

/**
    * @module     Commun
    * @subpackage Form\Admin
    * @author     Samuel NANGUI <nanguisamuel@gmail.com>
    * @copyright  Copyright (c) 2021 Nslabs
    */

namespace Commun\Form\Modules\Application;

use Commun\Form\Modules\Application\Fieldset\ImpactFieldset;


use Commun\Form\CommunForm;

class CadreLogiqueForm extends CommunForm 
{
            
    public function init() {        
        $this->setName('CadreLogiqueForm');
        $this->addFieldset(ImpactFieldset::class,['use_as_base_fieldset' => true]);
        $this->addSubmitButton('next', 'Poursuivre', 'next', 'btn btn-vert w-100');        
        $this->addSubmitButton('previous', 'Précédent', 'previous', 'btn btn-rouge w-100');
        $this->addSubmitButton('home', 'Accueil', 'home', 'btn btn-noir w-100');
    }  
  
}
  1. Impact Fieldset :
<?php

/**
    * @module     Commun
    * @subpackage Form\Admin
    * @author     Samuel NANGUI <nanguisamuel@gmail.com>
    * @copyright  Copyright (c) 2020 Nslabs
    */

namespace Commun\Form\Modules\Application\Fieldset;

use Commun\Model\Entity\Projet;
use Laminas\InputFilter\InputFilterProviderInterface;
use Laminas\Hydrator\ReflectionHydrator;

use Commun\Form\CommunFormFieldset;


class ImpactFieldset extends CommunFormFieldset implements InputFilterProviderInterface
{
        
    
    private $mapper;
       
    public function __construct($mappers=[])
    {        
        $this->mapper = $mappers;
        parent::__construct($this->mapper,'ImpactForm');           
        $this->setHydrator(new ReflectionHydrator());
        $this->setObject(new Projet());
        $this->setLabel('impactFieldset');          
        
    }
    
    
    public function init() {
        parent::init();                      
        $this->addTextarea('impactProjet', 'Impact', 'impactProjet', ['class' => 'form-control','rows' => 3],'champ-requis');   
        $this->addCollection('effets','Nouvel effet', EffetFieldset::class,1);
        
    }
    
        
    /**
     * @return array
     */
    public function getInputFilterSpecification()
    {
        return [
            
        ];
    }
}
  1. Effet fieldset :
<?php

/**
    * @module     Commun
    * @subpackage Form\Admin
    * @author     Samuel NANGUI <nanguisamuel@gmail.com>
    * @copyright  Copyright (c) 2020 Nslabs
    */

namespace Commun\Form\Modules\Application\Fieldset;

use Commun\Model\Entity\EffetProjet;
use Laminas\InputFilter\InputFilterProviderInterface;
use Laminas\Hydrator\ReflectionHydrator;

use Laminas\Filter\StripTags;
use Laminas\Filter\StringTrim;

use Commun\Form\CommunFormFieldset;


class EffetFieldset extends CommunFormFieldset implements InputFilterProviderInterface
{
        
    
    private $mapper;
    private $inputFilter;    
       
    public function __construct($mappers=[])
    {        
        $this->mapper = $mappers;
        parent::__construct($this->mapper,'EffetForm');           
        $this->setHydrator(new ReflectionHydrator());
        $this->setObject(new EffetProjet());
        $this->setLabel('EffetFieldset');          
        
    }
    
    
    public function init() {
        parent::init();                      
        $this->addText('libelleEffet','Intitulé de l\'effet','libelleEffet',['class' => 'form-control']);
        $options = $this->mapper['indicateur']->getOptions('idRefIndicateur','libelle','Aucune valeur choisie',null,['libelle']);           
        //$options = [];          
        $this->addSelect('idRefIndicateur','Indicateur de performance',$options,['class' => 'form-control']);
        $this->addText('valeurReference','Valeur de référence','valeurReference',['class' => 'form-control champ_decimal']);
        $this->addText('valeurCible','Valeur cible','valeurCible',['class' => 'form-control champ_decimal']);
        $this->addTextarea('moyenVerification', 'Moyen de vérification', 'moyenVerification', ['class' => 'form-control','rows' => 3]); 
        $this->addTextarea('risqueMesureAttenuation', 'Risques et mesures d\'atténuation', 'risqueMesureAttenuation', ['class' => 'form-control','rows' => 3]);   
        $this->addCollection('composantes','Nouvelle composante', ComposanteFieldset::class,1);
    }
    
        
    /**
     * @return array
     */
    public function getInputFilterSpecification()
    {
        return [
            
        ];       
    }
}
  1. Composante Fieldset :

<?php

/**
    * @module     Commun
    * @subpackage Form\Admin
    * @author     Samuel NANGUI <nanguisamuel@gmail.com>
    * @copyright  Copyright (c) 2020 Nslabs
    */

namespace Commun\Form\Modules\Application\Fieldset;

use Commun\Model\Entity\ComposanteEffet;
use Laminas\InputFilter\InputFilterProviderInterface;
use Laminas\Hydrator\ReflectionHydrator;

use Commun\Form\CommunFormFieldset;


class ComposanteFieldset extends CommunFormFieldset implements InputFilterProviderInterface
{
        
    
    private $mapper;
       
    public function __construct($mappers=[])
    {        
        $this->mapper = $mappers;
        parent::__construct($this->mapper,'ComposanteEffetForm');           
        $this->setHydrator(new ReflectionHydrator());
        $this->setObject(new ComposanteEffet());
        $this->setLabel('composanteEffetFieldset');          
        
    }
    
    
    public function init() {
        parent::init();                      
        $this->addText('libelle','Intitulé de la composante','libelle',['class' => 'form-control champ-requis']);
        /*$options = $this->mapper['indicateur']->getOptions('idRefIndicateur','libelle','Aucune valeur choisie',null,['libelle']);           
        $this->addSelect('idRefIndicateur','Indicateur de performance',$options,['class' => 'form-control']);
        $this->addText('valeurReference','Valeur de référence','valeurReference',['class' => 'form-control champ_decimal']);
        $this->addText('valeurCible','Valeur cible','valeurCible',['class' => 'form-control champ_decimal']);
        $this->addTextarea('moyenVerification', 'Moyen de vérification', 'moyenVerification', ['class' => 'form-control','rows' => 3]); 
        $this->addTextarea('risqueMesureAttenuation', 'Risques et mesures d\'atténuation', 'risqueMesureAttenuation', ['class' => 'form-control','rows' => 3]);*/
        $this->addCollection('produits','Nouveau produit', ProduitFieldset::class,1);
    }
    
        
    /**
     * @return array
     */
    public function getInputFilterSpecification()
    {
        return [
            
        ];
    }
}
  1. Produit fieldset :
<?php

/**
    * @module     Commun
    * @subpackage Form\Admin
    * @author     Samuel NANGUI <nanguisamuel@gmail.com>
    * @copyright  Copyright (c) 2020 Nslabs
    */

namespace Commun\Form\Modules\Application\Fieldset;

use Commun\Model\Entity\ProduitEffet;
use Laminas\InputFilter\InputFilterProviderInterface;
use Laminas\Hydrator\ReflectionHydrator;

use Commun\Form\CommunFormFieldset;


class ProduitFieldset extends CommunFormFieldset implements InputFilterProviderInterface
{
        
    
    private $mapper;
       
    public function __construct($mappers=[])
    {        
        $this->mapper = $mappers;
        parent::__construct($this->mapper,'produitForm');           
        $this->setHydrator(new ReflectionHydrator());
        $this->setObject(new ProduitEffet());
        $this->setLabel('produitFieldset');          
        
    }
    
    
    public function init() {
        parent::init();                      
        $this->addText('libelle','Intitulé du produit','libelle',['class' => 'form-control champ-requis']);
        $options = $this->mapper['indicateur']->getOptions('idRefIndicateur','libelle','Aucune valeur choisie',null,['libelle']);           
        $this->addSelect('idRefIndicateur','Indicateur de performance',$options,['class' => 'form-control']);
        $this->addText('valeurReference','Valeur de référence','valeurReference',['class' => 'form-control champ_decimal']);
        $this->addText('valeurCible','Valeur cible','valeurCible',['class' => 'form-control champ_decimal']);
        $this->addTextarea('moyenVerification', 'Moyen de vérification', 'moyenVerification', ['class' => 'form-control','rows' => 3]); 
        $this->addTextarea('risqueMesureAttenuation', 'Moyen de vérification', 'risqueMesureAttenuation', ['class' => 'form-control','rows' => 3]);  
        $this->addCollection('activites','Nouvelle activité', ActiviteFieldset::class,1);
    }
    
        
    /**
     * @return array
     */
    public function getInputFilterSpecification()
    {
        return [
            
        ];
    }
}
  1. Activite Fieldset :
<?php

/**
    * @module     Commun
    * @subpackage Form\Admin
    * @author     Samuel NANGUI <nanguisamuel@gmail.com>
    * @copyright  Copyright (c) 2020 Nslabs
    */

namespace Commun\Form\Modules\Application\Fieldset;

use Commun\Model\Entity\ActiviteProduit;
use Laminas\InputFilter\InputFilterProviderInterface;
use Laminas\Hydrator\ReflectionHydrator;

use Commun\Form\CommunFormFieldset;


class ActiviteFieldset extends CommunFormFieldset implements InputFilterProviderInterface
{
        
    
    private $mapper;
       
    public function __construct($mappers=[])
    {        
        $this->mapper = $mappers;
        parent::__construct($this->mapper,'ActiviteForm');           
        $this->setHydrator(new ReflectionHydrator());
        $this->setObject(new ActiviteProduit());
        $this->setLabel('activiteFieldset');          
        
    }
    
    
    public function init() {
        parent::init();                      
        $this->addText('libelle','Intitulé de l\'activité','libelle',['class' => 'form-control champ-requis']);
        $options = $this->mapper['indicateur']->getOptions('idRefIndicateur','libelle','Aucune valeur choisie',null,['libelle']);           
        $this->addSelect('idRefIndicateur','Indicateur de performance',$options,['class' => 'form-control']);
        $this->addText('valeurReference','Valeur de référence','valeurReference',['class' => 'form-control champ_decimal']);
        $this->addText('valeurCible','Valeur cible','valeurCible',['class' => 'form-control champ_decimal']);
        $this->addTextarea('moyenVerification', 'Moyen de vérification', 'moyenVerification', ['class' => 'form-control','rows' => 3]); 
        $this->addTextarea('risqueMesureAttenuation', 'Moyen de vérification', 'risqueMesureAttenuation', ['class' => 'form-control','rows' => 3]);         
    }
    
        
    /**
     * @return array
     */
    public function getInputFilterSpecification()
    {
        return [
            
        ];
    }
}

My partial view forn rendering elements of nested fieldset :


<?php 
foreach ($form as $element){  
    $classCheckBox='';
    if(!($element instanceof Laminas\Form\Element\Collection) && ($element->getAttributes()['type']=='multi_checkbox')){
        $classCheckBox = 'icheck-default';
    }    
    
?>
<!--We render elements wich are not collections-->
<?php if(!($element instanceof Laminas\Form\Element\Collection)): ?> 
    <div class="row clearfix">
        <?php if($setLabel): ?>
        <div class="mb-0 col-lg-<?=$colLabel?> col-md-<?=$colLabel?> col-sm-4 form-control-label">                                    
            <?= $this->formLabel($element) ?>
        </div>
        <?php endif; ?>
        <div class="mb-0 col-lg-<?=$colInput?> col-md-<?=$colInput?> col-sm-8">
            <div class="form-group <?=$classCheckBox?>">
                <?= $this->formElement($element) ?>            
            </div>
        </div>
        <div class="col-lg-12">
            <div class="form-group error-message mt-0">
                <?= $this->formElementErrors()->render($element) ?>
            </div>
        </div>
    </div>
<?php else : ?>
<?php 
  $effetFieldsets = $element->getFieldsets();  
  $effetsFieldset = $form->get('effets');
?>
    <div class="div-scrollable-effet">
        <?php 
            $i=1; 
            
            
            foreach ($effetFieldsets as $effetFieldset){ 
                $classEffetHide = ' div-affiche';
                if($hideEffetFrom && ($i>=$hideEffetFrom)){
                    $classEffetHide = ' div-cache';
                }                
                echo('<div class="div-effet'.$classEffetHide.'">'); 
                echo('<div class="title-element-cadre-logique">Effet '.$i.' </div>');
                    foreach ($effetFieldset->getElements() as $elementsEffetFieldset){ 
                        ?>
                        <?=$this->partial('partial/cadre-logique/view-rendering-element',['v_elements' => $elementsEffetFieldset,
                                                                                          'v_setLabel' => $setLabel,
                                                                                          'v_colInput' => $colInput,
                                                                                          'v_classCheckBox' => $classCheckBox,
                                                                                          'v_colLabel' => $colLabel,
                                                                                           ])?> 
                        <?php                                                                  
                    }
                    $composanteFieldsets = $effetFieldset->get('composantes');
                    $j=1;                    
                    foreach ($composanteFieldsets as $composanteFieldset){
                        $classComposanteHide = ' div-affiche';
                        if($hideComposanteFrom && ($j>=$hideComposanteFrom)){
                            $classComposanteHide = ' div-cache';
                        }                        
                        echo('<div class="div-composante'.$classComposanteHide.'">');
                        echo('<div class="title-element-cadre-logique">Composante '.$j.' </div>');
                        foreach ($composanteFieldset->getElements() as $elementsComposanteFieldset){
                            ?>
                        <?=$this->partial('partial/cadre-logique/view-rendering-element',['v_elements' => $elementsComposanteFieldset,
                                                                                          'v_setLabel' => $setLabel,
                                                                                          'v_colInput' => $colInput,
                                                                                          'v_classCheckBox' => $classCheckBox,
                                                                                          'v_colLabel' => $colLabel,
                                                                                           ])?> 
                        <?php                                                        
                        }
                       
                        $produitFieldsets = $composanteFieldset->get('produits');
                        $k=1;                    
                        foreach ($produitFieldsets as $produitFieldset){
                            $classProduitHide = ' div-affiche';
                            if($hideProduitFrom && ($k>=$hideProduitFrom)){
                                $classProduitHide = ' div-cache';
                            }                            
                            echo('<div class="div-produit'.$classProduitHide.'">');
                            echo('<div class="title-element-cadre-logique">Produit '.$k.' </div>');
                            foreach ($produitFieldset->getElements() as $elementsProduitFieldset){
                                ?>
                                <?=$this->partial('partial/cadre-logique/view-rendering-element',['v_elements' => $elementsProduitFieldset,
                                                                                                  'v_setLabel' => $setLabel,
                                                                                                  'v_colInput' => $colInput,
                                                                                                  'v_classCheckBox' => $classCheckBox,
                                                                                                  'v_colLabel' => $colLabel,
                                                                                                   ])?> 
                                <?php                                
                            }
                           
                            $activiteFieldsets = $produitFieldset->get('activites');                               
                            $l=1;                                                           
                                         
                            
                            foreach ($activiteFieldsets as $activiteFieldset){
                                $classHide = ' div-affiche';
                                if($hideActiviteFrom && ($l>=$hideActiviteFrom)){
                                    $classHide = ' div-cache';
                                }
                                echo('<div class="div-activite'.$classHide.'">');
                                    echo('<div class="title-element-cadre-logique">Activite '.$l.' </div>');
                                foreach ($activiteFieldset->getElements() as $elementsActiviteFieldset){                                       
                                    //echo $this->NslabsFormInputElement($elementsActiviteFieldset)
                                    ?>
                                <?=$this->partial('partial/cadre-logique/view-rendering-element',['v_elements' => $elementsActiviteFieldset,
                                                                                                  'v_setLabel' => $setLabel,
                                                                                                  'v_colInput' => $colInput,
                                                                                                  'v_classCheckBox' => $classCheckBox,
                                                                                                  'v_colLabel' => $colLabel,                                                                                                 
                                                                                                   ])?> 
                                <?php                                    
                                }                                
                                echo('</div>');                                
                                $l++;
                            }   
                            //$this->formCollection()->setWrapper('<div class="row clearfix"%4$s>%1$s%3$s</div>');
                            //$this->formCollection()->setDefaultElementHelper('NslabsFormInputDiv');
                            echo('<div class="template-activite">'.$this->formCollection()->renderTemplate($activiteFieldsets).'</div>');                        
                            echo('<div class="text-right"><input type="button" class="btn btn-vert btn-add-activite" name="btnAddActivite" value="Ajouter une activité"></div>');                            
                            echo('</div>');
                            $k++;
                        }
                        //echo('<div class="template-produit">'.$this->formCollection()->renderTemplate($produitFieldsets).'</div>');
                        //echo('<div class="mt-20 text-right"><button class="btn btn-vert btn-add-produit">Ajouter un produit</div>');
                        echo('<br>');
                        echo('<div class="text-right"><input type="button" class="btn btn-vert btn-add-produit" name="btnAddProduit" value="Ajouter un produit"></div>');
                        echo('</div>');
                        $j++;
                    }
                    //echo('<div class="template-composante">'.$this->formCollection()->renderTemplate($composanteFieldsets).'</div>');
                    echo('<br>');
                    echo('<div class="text-right"><input type="button" class="btn btn-vert btn-add-composante" name="btnAddComposante" value="Ajouter une composante"></div>');
                echo('</div>'); 
                $i++;
            }
            //echo('<div class="template-effet">'.$this->formCollection()->renderTemplate($effetsFieldset).'</div>');
            echo('<br>');
            echo('<div class="text-right"><input type="button" class="btn btn-vert btn-add-composante" name="btnAddEffet" value="Ajouter un effet"></div>');
        ?>
    </div>
<?php endif; ?>
<?php    
  }
?>

Here is my code for generating form elements. The question is how to generate the data-template with my first html code ?
Any help would be very appreciated because I stuck on this for weeks.

Thanks !

Hello,
I have found the solution to my problem !

Can you share your solution for the current problem? Thanks in advance! :+1:t3:

Here is my solution :
I Change my partial view for rendering elements from nested fieldset like this :

<?php 
foreach ($form as $element){    
    $classCheckBox='';
    if(!($element instanceof Laminas\Form\Element\Collection) && ($element->getAttributes()['type']=='multi_checkbox')){
        $classCheckBox = 'icheck-default';
    }    
    
?>
<!--We render elements wich are not collections-->
<?php if(!($element instanceof Laminas\Form\Element\Collection)): ?> 
    <?php if(!($element instanceof Laminas\Form\Element\Button)): ?>
    <div class="row clearfix">
        <?php if($setLabel): ?>
        <div class="mb-0 col-lg-<?=$colLabel?> col-md-<?=$colLabel?> col-sm-4 form-control-label">                                    
            <?= $this->formLabel($element) ?>
        </div>
        <?php endif; ?>
        <div class="mb-0 col-lg-<?=$colInput?> col-md-<?=$colInput?> col-sm-8">
            <div class="form-group <?=$classCheckBox?>">
                <?= $this->formElement($element) ?>            
            </div>
        </div>
        <div class="col-lg-12">
            <div class="form-group error-message mt-0">
                <?= $this->formElementErrors()->render($element) ?>
            </div>
        </div>
    </div>
    <?php else:?>
        <div class="text-right col-lg-12 mt-15">                                    
            <?= $this->formButton($element); ?>   
        </div>
    <?php endif;?>
<?php else : ?>
<?php 
  $effetFieldsets = $element->getFieldsets();  
  $effetsFieldset = $form->get('effets');
  
?>
    <div class="nested-fieldset">
        <?php 
            $this->formCollection()->setWrapper('<div class="nested-fieldset">%2$s%1$s%3$s</div>');
            $this->formCollection()->setElementHelper($this->formRow()->setPartial('partial/cadre-logique/view-rendering-cadre-logique-collection-elements-2'));
            echo $this->formCollection($effetsFieldset);            
        ?>
    </div>
<?php endif; ?>
<?php    
  }
?>

this line : $this->formCollection()->setWrapper('<div class="nested-fieldset">%2$s%1$s%3$s</div>'); is for surrounding each nested fieldset in a div with a class called ‘nested-fieldset’
And the view-rendering-cadre-logique-collection-elements-2 look like this :

<div class="row clearfix">
    <?php if(!($element instanceof Laminas\Form\Element\Button)): ?>
        <div class="mb-0 col-lg-4 col-md-4 col-sm-4 form-control-label">                                    
            <?= $this->formLabel($element) ?>
        </div>
        <div class="mb-0 col-lg-8 col-md-8 col-sm-8">
            <div class="form-group">
                <?= $this->formElement($element) ?>            
            </div>
        </div>
        <div class="col-lg-12">
            <div class="form-group error-message mt-0">
                <?= $this->formElementErrors()->render($element) ?>
            </div>
        </div>
    <?php else : ?>
        <div class="text-right col-lg-12 mt-15">                                    
            <?= $this->formButton($element); ?>   
        </div>
    <?php endif; ?>
</div>

And then here is my JS File for adding more elements for each fieldset :

$( document ).ready(function() {    
     ajouterActivite();
     ajouterProduit();
     ajouterComposante();
     ajouterEffet();
});

function ajouterActivite(){    
    $('body').on('click','.btnAddActivite',function (){          
        var parentNested = $(this).parent().parent().parent().find('.nested-fieldset');            
        var currentCount = $(parentNested).length - 1;                  
        var template = $(parentNested).find("span").attr("data-template");        
        template = template.replace(/__activite__/g, currentCount);
        $(this).parent().parent().before(template);
        
        return false;
    });
    appliquerMasqueMontant(); 
}

function ajouterProduit(){              
    $('body').on('click','.btnAddProduit',function () {
        var parentNested = $(this).closest('.nested-fieldset').find('.nested-fieldset');               
        var currentCount = $(parentNested).length;        
        currentCount = (currentCount -1)/3;        
        var template = $(parentNested).next("span").attr("data-template");        
        template = template.replace(/__produit__/g, currentCount);
        $(this).parent().parent().before(template);

        return false;
    });
    appliquerMasqueMontant(); 
}
function ajouterComposante(){
    $('body').on('click','.btnAddComposante',function () {
        var parentNested = $(this).closest('.nested-fieldset').find('.nested-fieldset');               
        var currentCount = $(parentNested).length;        
        currentCount = (currentCount -1)/5;        
        var template = $(parentNested).next("span").attr("data-template");        
        template = template.replace(/__composante__/g, currentCount);
        $(this).parent().parent().before(template);

        return false;
    });
    appliquerMasqueMontant(); 
}
function ajouterEffet(){
    $('body').on('click','.btnAddEffet',function () {
        var parentNested = $(this).parent().parent().find('.nested-fieldset');          
        var currentCount = $(parentNested).length;
        currentCount = (currentCount -2)/7;       
        var template = $(parentNested).find('.nested-fieldset').next("span").attr("data-template");                
        template = template.replace(/__effet__/g, currentCount);
        $(this).parent().before(template);

        return false;
    });
    appliquerMasqueMontant(); 
}

@froschdesign Here is my code.
Hope it will help someone !

1 Like

Hi @ebuddy,

Did you implement something in CommonForm to handle getInputFilterSpecification method in each Fieldset? Because it seems doesn’t validate (even if I set validationGroup)

Thanks

Sorry, I could verify if I add Fieldset instance to Form, its getInputFilterSpecification() doesn’t work

$myFieldset = new Fieldset\MyFieldset($this->entityManager);
$this->add($myFieldset->get('text_input'));

You have to use:

$myFieldset = new Fieldset\MyFieldset($this->entityManager);
$this->add($myFieldset);