FormBuilder

Description

This part of Forms lib allows user to create custom forms in runtime and store them into database. The library itself provides only the structure with fields to be displayed for the end-user, e.x. as a contact form.

Main classes

  • \Vegas\Forms\FormFactory - generates form object based on provided data, used in frontend controllers
  • \Vegas\Forms\InputSettings - groups available settings for specific input

Basic input types

Following inputs can be added out-of-the-box to the created form:

  • Datepicker - textfield with jQuery datepicker
  • Email - textfield with e-mail address validation
  • RichTextArea - textarea field with WYSYWIG editor
  • Select - dropdownlist with specified data (see Data providers section)

When creating a form, you are able to specify following settings per each of them:

  • Unique ID - HTML ID selector for input field
  • Required field - prevent sending empty content from the input
  • Label - additional text description to be displayed around
  • Default value - predefined value of input
  • Placeholder text - input placeholder shadow
  • Data provider - available options, applicable only to select lists - see Data providers

Setup

To make FormBuilder available in the application, you have to create a service provider inside app/services directory.


use Phalcon\DiInterface;
use Vegas\DI\ServiceProviderInterface;

/**
 * Class FormFactoryServiceProvider
 */
class FormFactoryServiceProvider implements ServiceProviderInterface
{
    const SERVICE_NAME = 'formFactory';

    /**
     * {@inheritdoc}
     */
    public function register(DiInterface $di)
    {
        $di->set(self::SERVICE_NAME, function() {
            return new \Vegas\Forms\FormFactory();
        }, true);
    }

    /**
     * {@inheritdoc}
     */
    public function getDependencies()
    {
        return [];
    }
}

Example usage

The example below show how to put some logic - it covers submitting form data into some URL address. We assume i18n service is enabled and application uses Mongo DBMS.

  1. Create a module named Foo with standard directory structure:
    Foo
    ├── config
    |   ├── config.php
    |   └── routes.php
    ├── controllers
    |   ├── backend
    |   |   └── FooController.php
    |   └── frontend
    |       └── FooController.php
    ├── models
    |   └── Form.php
    ├── views
    |   ├── backend
    |   |   └── foo
    |   |       ├── _form.volt
    |   |       ├── edit.volt
    |   |       └── new.volt
    |   └── frontend
    |       └── foo
    |           └── show.volt
    └── Module.php
    
  2. Create a form with additional settings to be used by our application.
    namespace Foo\Forms;
    
    use Phalcon\Forms\Element\Text;
    use Vegas\Forms\InputSettings,
        Vegas\Forms\Element\Cloneable;
    use Phalcon\Validation\Validator\PresenceOf;
    use Vegas\Validation\Validator\SizeOf,
        Vegas\Validation\Validator\Email,
        Vegas\Validation\Validator\Url;
        
    class Form extends \Vegas\Forms\Form
    {
        /**
         * Initializes backend form of pages
         */
        public function initialize()
        {
            $name = (new Text('name'))
                ->setAttribute('placeholder', 'Name')
                ->setLabel($this->i18n->_('Form title'))
                ->addValidator(new PresenceOf);
            $this->add($name);
            
            $url = (new Text('url'))
                ->setAttribute('placeholder', 'URL')
                ->addValidator(new Url)
                ->setLabel($this->i18n->_('Target URL'));
            $this->add($url);
    
            $items = (new Cloneable('inputs'))
                ->setAssetsManager($this->assets)
                ->setBaseElements((new InputSettings)->getElements())
                ->addValidator(new SizeOf(array('min' => 1)))
                ->setLabel($this->i18n->_('Inputs'));
    
            $this->add($items);
        }
    }
    
    This form is a recommended minimum:
    - name field will be translated into slug
    - url field will be the target URL where POST is going to be sent
    - inputs fields is a list of inputs added to the created form

  3. Create a model to store created forms in database:
    namespace Foo\Models;
    
    use Vegas\Mvc\CollectionAbstract;
    
    class Form extends CollectionAbstract
    {
        public function getSource()
        {
            return 'vegas_forms';
        }
        
        public function beforeCreate() {
            parent::beforeCreate();
            $this->generateSlug($this->name);
        }
    }
    
  4. Fill frontend controller with following code allowing to display form:
    namespace Foo\Controllers\Frontend;
    
    use Vegas\Mvc\Controller\ControllerAbstract,
        Foo\Models\Form;
    
    class FooController extends ControllerAbstract
    {
        public function showAction($slug)
        {
            $formModel = Form::findFirst(['conditions' => ['slug' => $slug]]);
            
            $this->view->setVar('model', $formModel);
            $this->view->setVar('form', $this->di->getShared('formFactory')->createForm($formModel->inputs));
        }
    }
    
  5. Fill backend controller with simple CRUD code:
    namespace Foo\Controllers\Backend;
    
    use Vegas\Mvc\Controller;
    
    class FooController extends Controller\Crud
    {
        protected $formName = 'Foo\Forms\Form';
        protected $modelName = 'Foo\Models\Form';
        
        public function initialize()
        {
            parent::initialize();
            
            $redirectToRootPath = function() {
                $this->response->redirect(['for' => '/']);
            };
    
            $this->dispatcher->getEventsManager()->attach(Controller\Crud\Events::AFTER_CREATE, $redirectToRootPath);
            $this->dispatcher->getEventsManager()->attach(Controller\Crud\Events::AFTER_UPDATE, $redirectToRootPath);
            $this->dispatcher->getEventsManager()->attach(Controller\Crud\Events::AFTER_DELETE, $redirectToRootPath);
        }
    }
    
  6. Fill frontend view show.volt with following code:
    <h1>{{ model.name }}</h1>
    
    <form action="{{ model.url }}" method="post" role="form">
    {% for element in form %}
        {% do element.setAttribute('class', element.getAttribute('class')~' form-control') %}
        {% set hasErrors = form.hasMessagesFor(element.getName()) %}
    
        <div class="form-group{% if hasErrors %} has-error{% endif %}">
            <label for="{{ element.getName() }}">{{ element.getLabel() }}</label>
            {% if hasErrors %}
                <span class="help-block">
                    {% for error in form.getMessagesFor(element.getName()) %}
                        {{ error }}
                    {% endfor %}
                </span>
            {% endif %}
            {{ element }}
        </div>
    {% endfor %}
    
    <input type="submit" value="{{ i18n._('Submit') }}" class="btn btn-primary" /> 
    </form>
    
  7. Fill backend view new.volt with following code (edit.volt can be implemented nearly the same way):
    <div class="row widget">
        <div class="col-xs-12">
            <div class="widget widget-default-spacer">
                <div class="spacer spacer30"></div>
            </div>
            <div class="widget widget-page-header">
                <h1>{{ i18n._('Add new form') }}</h1>
            </div>
            <div class="widget widget-default-spacer">
                <div class="spacer spacer22"></div>
            </div>
            <div class="widget widget-admin-page">
                <div class="well">
                    <div class="row widget">
                        <div class="col-md-12">
                                <div class="form-edit form-horizontal">
                                    <form action="{{ url.get(['for':'admin/form', 'action':'create']) }}" method="post" role="form">
                                        {{ partial('backend/foo/_form', ['form': form]) }}
                                    </form>
                                </div>
                            </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
    
  8. Fill backend partial _form.volt with following code:
    {% for element in form %}
        {% do element.setAttribute('class', element.getAttribute('class')~' form-control') %}
        {% set hasErrors = form.hasMessagesFor(element.getName()) %}
    
        <div class="form-group{% if hasErrors %} has-error{% endif %}">
            {% if element != form.get('inputs') %}
                <label for="{{ element.getName() }}" class="col-md-3" control-label">{{ element.getLabel() }}</label>
                <div class="col-md-9">
                    {{ element }}
                    {% if hasErrors %}
                        <span class="help-block">
                            {% for error in form.getMessagesFor(element.getName()) %}
                                {{ error }}
                            {% endfor %}
                        </span>
                    {% endif %}
                </div>
            {% else %}
                <div vegas-cloneable="1" class="form-group-add">
                    {% for row in element.getRows() %}
                    <fieldset>
                        <div class="form-group-add-item">
                        {% for item in row.getElements() %}
                            <div class="form-group">
                                        <label class="col-md-2 control-label">{{ i18n._(item.getLabel()) }}</label>
                                        <div class="col-md-10">{{ item }}</div>
                            </div>
                        {% endfor %}
                        </div>
                    </fieldset>
                    {% endfor %}
                </div>
            {% endif %}
        </div>
    {% endfor %}
     <div class="form-group form-group-button">
        <div class="col-md-offset-2 col-md-10">
            <button type="submit" class="btn btn-form-submit">{{ i18n._('Submit') }}</button>
            <a href="{{ url.get(['for':'/']) }}" class="btn btn-form-cancel">{{ i18n._('Cancel') }}</a>
        </div>
    </div>
    {% endfor %}
    
    <input type="submit" value="{{ i18n._('Submit') }}" class="btn btn-primary" /> 
    </form>
    
  9. Add appropriate routes inside module's routes.php:
    return [
        // Form display action
        'form/show' => [
            'route' => '/form/{slug}',
            'paths' => [
                'module' => 'Foo',
                'controller' => 'Frontend\Foo',
                'action' => 'show',
            ],
            'params' => []
        ],
        // Form builder manipulation actions
        'admin/form' => [
            'route' => '/admin/form/{action}',
            'paths' => [
                'module' => 'Foo',
                'controller' => 'Backend\Foo',
            ],
            'params' => []
        ]
    ];
    

Finally, build a form named Bar form using [APP_URL]/admin/form/new address.
As a result, generated slug will be bar-form, the form will be available on [APP_URL]/form/bar-form.

Data providers

These are small classes which provide data for select lists in created forms. The only requirement for them is to implement \Vegas\Forms\DataProvider\DataProviderInterface.

An example which provides country codes to a select list:

namespace Foo\Models;

class CountryCodeDataProvider implements \Vegas\Forms\DataProvider\DataProviderInterface
{
    public function getName()
    {
        return 'Country';
    }
    
    public function getData()
    {
        return [
            'AX' => 'Ålandseilanden',
            'AF' => 'Afghanistan', 
            // ...
            'SE' => 'Zweden',
            'CH' => 'Zwitserland'
        ];
    }
    
    public function setOptions(array $options)
    {
    }
}

And add the reference to the class into app/modules/Foo/config/config.php file:

//...
'formFactory'   => [
    'dataProviders' => [
        // Keeps all classes used to provide data for multiple data input types
        // Use fully qualified class names implementing DataProviderInterface as values
        // The order here is how these options will be listed when selecting one.
        '\Foo\Models\CountryCodeDataProvider',
    ]
]
//...

After these steps, a new option with label Country will appear in the builder form.