phpzend-formzend-framework3zend-form-element

Zend Framework 3 - Add and Remove new input element section using javascript


I want to add multiple school locations in zend-form on click of anchor tag or button. So that zend form validation can be applied to all dynamically created fields Please see attached image.I want to clone div with in red border in image enter image description here

Below is SchoolController Class

<?php



namespace Application\Controller;

use Zend\Mvc\Controller\AbstractActionController;
use School\Service\SchoolManager;
use Doctrine\ORM\EntityManager;
use Zend\View\Model\ViewModel;
use Application\Form\AddSchoolForm;
use School\Entity\School;
use School\Entity\SchoolLocation;

class SchoolController extends AbstractActionController {

    /**
     * Entity manager.
     * @var Doctrine\ORM\EntityManager
     */
    private $entityManager;

    /**
     * School manager.
     * @var School\Service\SchoolManager 
     */
    private $schoolManager;

    public function __construct($entityManager, $schoolManager) {
        $this->entityManager = $entityManager;
        $this->schoolManager = $schoolManager;
    }

    public function addAction() {
        $form = new AddSchoolForm();


        // Check if user has submitted the form
        if ($this->getRequest()->isPost()) {

            // Fill in the form with POST data
            $data = $this->params()->fromPost();
            $form->setData($data);
            // Validate form
            if ($form->isValid()) {
                $reqData = array(
                    'name' => $data['name'],
                    'description' => $data['description'],
                    'active' => 1,
                    'school_location' => (object) array(
                        (object) array(
                            "apartment_number" => $data['apartment_number'],
                            "street_name" => $data['street_name'],
                            "city" => $data['city'],
                            "state" => $data['state'],
                            "pin" => $data['pin'],
                            "active" => 1)
                    )
                );

                $this->schoolManager->addSchool((object) $reqData);
            } else {
                print_r($form->getMessages());
                die("not valid data");
                $isLoginError = true;
            }
        }
        return new ViewModel([
            'form' => $form
        ]);
    }
}

Below is AddSchoolForm Class:

<?php

namespace Application\Form;

use Zend\Form\Element;
use Zend\Form\Form;
use Zend\InputFilter\InputFilter;

/**
 * This form is used to collect user's login, password and 'Remember Me' flag.
 */
class AddSchoolForm extends Form {

    /**
     * Constructor.
     */
    public function __construct() {
        // Define form name
        parent::__construct('addschool-form');

        // Set POST method for this form
        $this->setAttribute('method', 'post');

        $this->addElements();
        $this->addInputFilter();
    }

    /**
     * This method adds elements to form (input fields and submit button).
     */
    protected function addElements() {

        $this->add([
            'attributes' => array(
                'name' => 'name',
                'type' => 'text',
                'id' => 'name',
                'class' => 'form-control',
                'required' => 'required',
            ),
            'options' => [
                'label' => 'School Name',
            ],
        ]);

        // Add "desc" field
        $this->add([
            'attributes' => array(
                'name' => 'description',
                'type' => 'text',
                'id' => 'description',
                'class' => 'form-control',
                'required' => 'required',
            ),
            'options' => [
                'label' => 'Description',
            ],
        ]);


        $this->add([
            'type' => 'hidden',
            'name' => 'active',
            'value' => 1
        ]);

        // Add "school location" field
        $this->add([
            'attributes' => array(
                'name' => 'apartment_number',
                'type' => 'text',
                'id' => 'apartment_number',
                'class' => 'form-control'
            ),
            'options' => [
                'label' => 'Apartment Number',
            ],
        ]);


        $this->add([
            'attributes' => array(
                'name' => 'street_name',
                'type' => 'text',
                'id' => 'street_name',
                'class' => 'form-control'
            ),
            'options' => [
                'label' => 'Street Name',
            ],
        ]);



        $this->add([
            'attributes' => array(
                'name' => 'city',
                'type' => 'text',
                'id' => 'city',
                'class' => 'form-control'
            ),
            'options' => [
                'label' => 'City',
            ],
        ]);



        $this->add([
            'attributes' => array(
                'name' => 'state',
                'type' => 'text',
                'id' => 'state',
                'class' => 'form-control'
            ),
            'options' => [
                'label' => 'State',
            ],
        ]);


        $this->add([
            'attributes' => array(
                'name' => 'pin',
                'type' => 'text',
                'id' => 'pin',
                'class' => 'form-control'
            ),
            'options' => [
                'label' => 'PIN',
            ],
        ]);



        // Add the Submit button
        $this->add([
            'type' => 'submit',
            'name' => 'submit',
            'attributes' => [
                'value' => 'Sign in',
                'id' => 'submit',
            ],
        ]);
    }

    /**
     * This method creates input filter (used for form filtering/validation).
     */
    private function addInputFilter() {
        // Create main input filter
        $inputFilter = new InputFilter();
        $this->setInputFilter($inputFilter);

        // Add input for "email" field
        $inputFilter->add([
            'name' => 'name',
            'required' => true,
            'filters' => [
                ['name' => 'StringTrim'],
            ],
            'validators' => [
                [
                    'name' => 'StringLength',
                    'options' => [
                        'min' => 5,
                        'max' => 20
                    ],
                ],
            ],
        ]);


        $inputFilter->add([
            'name' => 'description',
            'required' => true,
            'filters' => [
            ],
            'validators' => [
                [
                    'name' => 'StringLength',
                    'options' => [
                        'min' => 5,
                        'max' => 64
                    ],
                ],
            ],
        ]);
    }

}

Below is view file add.phtml

<script type="text/javascript">
    function addSchoolLocation(){
        $( ".schoolLocation" ).clone().appendTo( ".schoolLocation" );
    }


    </script>
<!-- Content Header (Page header) -->
<section class="content-header">
    <ol class="breadcrumb">
        <li><a href="#"><i class="fa fa-dashboard"></i> Home</a></li>
        <li class="active">Add School</li>
    </ol>
</section>

<!-- Main content -->
<section class="content">
    <div class="row">
        <!-- left column -->
        <div class="col-md-12">
            <!-- general form elements -->
            <div class="box box-primary form-custome">
                <div class="box-header with-border">
                    <h3 class="box-title">Add School <ul class="add-icon-new">
                            <li><a href="#" class="i-down"><i class="fa fa-angle-down"></i></a></li>
                            <li><a href="#" class="i-refresh"><i class="fa fa-refresh" aria-hidden="true"></i>
                                </a></li>
                            <li><a href="#" class="i-close"><i class="fa fa-times" aria-hidden="true"></i></a></li>
                        </ul>
                    </h3>
                </div>
                <h5 class="form-heading">School Information</h5>
                <form role="form" method="post">
                    <div class="box-body">

                        <div class="form-group col-md-3 col-sm-6">
                            <?= $this->formLabel($form->get('name')); ?>
                            <?= $this->formElement($form->get('name')); ?>
                        </div>

                        <div class="form-group col-md-3 col-sm-6">
                            <?= $this->formLabel($form->get('description')); ?>
                            <?= $this->formElement($form->get('description')); ?>
                        </div>

                        <?= $this->formElement($form->get('active')); ?>         
                        <h5 class="form-heading">School Location</h5>
                        <div class="schoolLocation">
                            <div class="form-group col-md-3 col-sm-6">
                                <?= $this->formLabel($form->get('apartment_number')); ?>
                                <?= $this->formElement($form->get('apartment_number')); ?>
                            </div>
                            <div class="form-group col-md-3 col-sm-6">
                                <?= $this->formLabel($form->get('street_name')); ?>
                                <?= $this->formElement($form->get('street_name')); ?>
                            </div>
                            <div class="form-group col-md-3 col-sm-6">
                                <?= $this->formLabel($form->get('city')); ?>
                                <?= $this->formElement($form->get('city')); ?>
                            </div>
                            <div class="form-group col-md-3 col-sm-6">
                                <?= $this->formLabel($form->get('state')); ?>
                                <?= $this->formElement($form->get('state')); ?>
                            </div>
                            <div class="form-group col-md-3 col-sm-6">
                                <?= $this->formLabel($form->get('pin')); ?>
                                <?= $this->formElement($form->get('pin')); ?>
                            </div>
                        </div>
                        <div>
                            <a href="javascript:void(0);" onclick="addSchoolLocation();">Add School Location</a>
                            <a href="javascript:void(0);" id="addElement">Add School Location</a>
                        </div>

                        <div class=" form-group col-sm-12">

                            <button class="save">Save</button>
                            <button class="reset">Reset</button>
                        </div>
                    </div>
                </form>
            </div>

        </div>
    </div>
    <!-- /.row -->
</section>
<!-- /.content -->

I want to clone div with class schoollocation

Note*: I tried below solutions but nothing worked for me as those are not the solution for Zend Framework-3

  1. Zend Framework - Add new input element using javascript

  2. https://docs.zendframework.com/zend-form/collections/#form-collections


Solution

  • What you're looking for is the usage of Collections (which you linked) and Fieldsets.

    You use a Fieldset to represent an Entity. In this example the Fieldset is Location, attached to School.

    Also, the School as a One To Many relation with Location.

    As such, you would have a SchoolFieldset class, which needs a Collection Element.

    Below a very simplified example of the setup.

    Backend

    LocationFieldset

    class LocationFieldset
    {
        public function init()
        {
            parent::init();
    
            $this->add([
                'name' => 'name',
                'required' => true,
                'type' => Text::class,
                'options' => [
                    'label' => _('Name'),
                ],
            ]);
    
            // ... Add whatever for Location
        }
    }
    

    SchoolFieldset

    class SchoolFieldset
    {
        /**
         * @var LocationFieldset
         */
        protected $locationFieldset;
    
        public function __construct(LocationFieldset $locationFieldset) 
        {
            $this->locationFieldset($locationFieldset);
        }
    
        public function init()
        {
            parent::init();
    
            $this->add([
                'name' => 'name',
                'required' => true,
                'type' => Text::class,
                'options' => [
                    'label' => _('Name'),
                ],
            ]);
    
            $this->add([
                'type' => Collection::class,
                'required' => true,
                'name' => 'locations',
                'options' => [
                    'label' => _('Locations'),
                    'count' => 1,                     // Initial amount of Fieldsets on-load
                    'allow_add' => true,              // Allows creation of 0/multiple
                    'allow_remove' => true,           // Allows removal
                    'should_create_template' => true, // Creates template in the HTML in a <span data-template="the whole html here"></span> -> JavaScript this bit for duplication/removal
                    'target_element' => $this->locationFieldset, // A pre-loaded Fieldset must be passed here, not just the FQCN as you would for including a Fieldset not in a Collection
                ],
            ]);
    
            // ... Add whatever
        }
    }
    

    SchoolForm

    class SchoolForm extends CustomAbstractForm
    {
        public function init()
        {
            $this->add([
                'name' => 'school',
                'type' => SchoolFieldset::class,
                'options' => [
                    'use_as_base_fieldset' => true,
                ],
            ]);
    
            //Call parent initializer. (Default for me it adds a submit button)
            parent::init();
        }
    }
    

    Front-end

    In the view of the form, I load a bit of JavaScript. It's based on the demo data given in the Zend Framework documentation.

    Please note that those docs do not account for removal (so if you have HTML objects with id's 0-1-2 and you remove 1, it will count, come to 2 and create another 2, giving you 0-2-2 and thus an overwrite for the second one you already had).

    The JavaScript I have in a project at the moment is this (sorry, cannot give you all of it, but this should get you started):

    Buttons

    var $addButton = $('<button type="button" data-action="add-fieldset" class="btn btn-primary">Add another</button>');
    var $removeButton = $('<button type="button" data-action="remove-fieldset" class="btn btn-danger">Remove</button>');
    

    Usage

    $('body').on('click', 'button[type="button"][data-action="add-fieldset"]', function () {
        addCollectionFieldset(this);
    });
    
    $('body').on('click', 'button[type="button"][data-action="remove-fieldset"]', function () {
        removeCollectionFieldset(this);
    });
    
    function addCollectionFieldset(element) {
        var $element = $(element);
        var $fieldsetDataSpan = $element.siblings('span[data-name="fieldset-data"]');
        var fieldsetCount = $fieldsetDataSpan.data('fieldset-count');
    
        var escapedTemplate = $element.siblings('span[data-template]').data('template');
        var $replaced = $(escapedTemplate.replace(/__index__/g, fieldsetCount));
        $replaced.append($removeButton.clone());
    
        $($replaced).insertAfter($element.siblings('fieldset:last'));
        $('<hr>').insertBefore($element.siblings('fieldset:last'));
    
        $fieldsetDataSpan.data('fieldset-count', fieldsetCount + 1); // Up the count by one fieldset
    }
    
    function removeCollectionFieldset(element) {
        $(element).parent().remove();
    }
    

    Note: the "Remove" button is placed IN every Fieldset within the Collection. The "Add another" button is placed below the Collection.

    How you solve that issue, is up to you.

    View

    <?= $this->form($form) ?>
    <?php $this->inlineScript()->prependFile($this->basePath('js/form.js')) ?>
    

    Controller action

    public function addAction()
    {
        /** @var SchoolForm $form */
        $form = $this->getSchoolForm();
    
        /** @var Request $request */
        $request = $this->getRequest();
        if ($request->isPost()) {
            $form->setData($request->getPost());
    
            if ($form->isValid()) {
                /** @var School $school */
                $school = $form->getObject();
    
                $this->getObjectManager()->persist($school);
    
                try {
                    $this->getObjectManager()->flush();
                } catch (Exception $e) {
    
                    throw new Exception(
                        'Could not save. Error was thrown, details: ' . $e->getMessage(),
                        $e->getCode(),
                        $e->getPrevious()
                    );
                }
    
                return $this->redirectToRoute('schools/view', ['id' => $school->getId()]);
            }
        }
    
        return [
            'form' => $form,
            'validationMessages' => $form->getMessages() ?: '',
        ];
    }
    

    ControllerFactory

    class AddControllerFactory implements FactoryInterface
    {
        public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
        {
            /** @var ObjectManager $objectManager */
            $objectManager = $container->get(EntityManager::class);
    
            /** @var FormElementManagerV3Polyfill $formElementManager */
            $formElementManager = $container->get('FormElementManager');
            /** @var SchoolForm $schoolForm */
            $schoolForm = $formElementManager->get(SchoolForm::class);
    
            return new AddController($objectManager, $schoolForm);
        }
    }
    

    FormFactory

    class SchoolFormFactory implements FactoryInterface
    {
        public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
        {
            $objectManager = $container->get(EntityManager::class);
            $translator = $container->get('MvcTranslator');
            $inputFilterPluginManager = $container->get('InputFilterManager');
    
            $inputFilter = $inputFilterPluginManager->get(SchoolFormInputFilter::class); // Did not show this one
    
            /** @var SchoolForm $form */
            $form = new SchoolForm();
            $form->setObjectManager($objectManager);
            $form->setTranslator($translator);
            $form->setInputFilter($inputFilter);
    
            return $form;
        }
    }
    

    FormFactory

    class SchoolFieldsetFactory implements FactoryInterface
    {
        public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
        {        
            $objectManager = $container->get(EntityManager::class);
            $translator = $container->get('MvcTranslator');
    
            $fieldset = new SchoolFieldset();
            $fieldsetObject = new School();
    
            /** @var SchoolFieldset $fieldset */
            $fieldset = new $fieldset($objectManager(), 'school');
            $fieldset->setHydrator(
                new DoctrineObject($objectManager())
            );
            $fieldset->setObject($fieldsetObject);
            $fieldset->setTranslator($translator);
    
            return $fieldset;
        }
    }
    

    If you have a few moments to spare, I would advise you to check out more examples in a repo I created to help quickly create forms in ZF and ZF with Doctrine. The ReadMe with examples is here