I am trying to use custom form rendering for category fields which is child form.
When i use it in normal everything works, but i have to use custom form theme to render it differently
{{ form_widget(form.category, {
attr: {
class: 'category',
},
}) }}
My category form type:
class CategoryType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('name', EntityType::class, [
'class' => Category::class,
'choice_label' => function (Category $choice, $key, $id) {
return new TranslatableMessage($choice->getName());
},
'label' => false,
'placeholder' => null,
'expanded' => true,
'required' => false,
])
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Category::class,
]);
}
}
In my main twig with form i have
{% form_theme form.category 'form/custome_theme_categories.html.twig' %}
I created twig view in form/custome_theme_categories.html.twig where i can put
{{ dump(foo) }}
and i don't event get the error that is't wrong or any kind of HTML. It doesn't meter what i put in my new view: i get empty HTML in result.
Is custom field theme done otherwise?
I have an embedded forms with a entity field Activity in a sub form dynamically populated with data from a field Module whose data are set via ajax request.
But when i submit the form i have a validation error ("this value is not valid") and the activity field is blank.
The principle :
the main form (Session) has an entity field module and contains a collection (Timeslot).
The Timeslot subform has a field activity whose content depends on the Module value.
My forms :
class SessionType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('module', EntityType::class, [
'label' => 'module.name',
'required' => true,
'class' => Module::class,
'placeholder' => 'choose',
])
->add('timeslots', CollectionType::class, [
'required' => true,
'constraints' => new Valid(),
'prototype_name' => '__timeslot_prot__',
'entry_type' => TimeslotType::class,
'entry_options' => ['module' => isset($module) ? $module->getId() : 0],
'by_reference' => false,
'allow_add' => true,
'allow_delete' => true,
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Session::class,
'module' => null,
]);
}
}
And the Timeslot
class TimeslotType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$module = $options['module'];
$builder
->add('activity', EntityType::class, [
'class' => Activity::class,
'multiple' => false,
'required' => true,
'constraints' => new NotBlank(),
'query_builder' => function (ActivityRepository $activity) use ($module) {
if (null !== $module) {
$qb = $activity->createQueryBuilder('a')
->join('a.activityGroups', 'ag')
->join('ag.module', 'm')
->where('m.id = :mid')
->setParameter('mid', $module)
->orderBy('a.name', 'ASC');
return $qb;
} else {
$qb = $activity->createQueryBuilder('a')
->where('a.id = 0');
return $qb;
}
},
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Timeslot::class,
'module' => null,
]);
}
}
I also have a javascript script which fills the Activity select options with a query through an ajax call.
I also tried to modify my TimeslotType following this symfony doc
but i stumble upon the problem that in my case, the Module field is not in the same formBuilder than Activity, so the POST_SUBMIT eventlistener can't be applied in my case.
class TimeslotType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$module = $options['module'];
$formModifier = function (FormInterface $form, Module $module = null) {
$activities = [];
if (null !== $module){
foreach($module->getActivityGroups() as $ag){
foreach($ag->getActivities() as $activity){
$activities[] = $activity;
}
}
}
$form->add('activity', EntityType::class, [
'label' => 'timeslot.activity',
'label_attr' => ['class' => 'mandatory', 'data-extrainfo' => 'panel_timeslot'],
'attr' => [
'class' => 'activitieslist',
],
'class' => Activity::class,
'multiple' => false,
'required' => true,
'constraints' => new NotBlank(),
'choices' => $activities,
'choices_as_values' => true,
]);
};
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (FormEvent $event) use ($formModifier) {
// the entity : Timeslot
$data = $event->getData();
$formModifier($event->getForm(), $module);
}
);
//$builder->get('module') ... Makes no sense here since the module attribute is not a Timeslot, but belongs to Session
/*
$builder->get('module')->addEventListener(FormEvents::PRE_SUBMIT,
function (FormEvent $event) use ($formModifier) {
$module = $event->getForm()->getData();
$formModifier($event->getForm()->getParent(), $module);
}
);
*/
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Timeslot::class,
'module' => null,
]);
}
}
How can i resolve my problem and validate my form ?
===
Edit
The ajax request is called when the module field is changed
function updateActivities(block) {
var module = $('#session_module').val();
if (module != ''){
$.ajax({
url: Routing.generate('project_module_activities_ajax', {}),
data: { 'module': module },
method: 'POST',
success: function (activities) {
var sel = '';
for (var i = 0; i < activities.length; i++) {
sel += '<option value="' + activities[i].id + '">' + activities[i].name + '</option>'
}
$('#litimeslot'+block).find('.activitieslist').each(function () {
$(this).html(sel);
})
}
});
}
}
And the route project_module_activities_ajax simply returns an array of activities for the module from a query
something like :
[
['id' : 1, 'name' : 'activity 1'],
['id' : 2, 'name' : 'activity 2'],
}
Edit 2
if i add this eventlistener in the TimeslotType :
$builder->addEventListener(FormEvents::PRE_SUBMIT,
function (FormEvent $event) use ($moduleId) {
$form = $event->getForm();
$data = $event->getData();
echo '<pre>form AFTER=';var_dump($form->getData());echo '</pre>';
echo '<pre>data AFTER=';var_dump($data);echo '</pre>';
echo '<pre>module AFTER';var_dump($moduleId);echo '</pre>';
}
);
I have
form AFTER= NULL
data AFTER= array(
["activity"]=>
string(3) "573" <= which is the value i selected
...
)
module AFTER int(0)
I have large form, i need to split it in multiple form when on mobile view.
Desktop = 1 large form
Mobile = 2-3 smaller form, when i valid the 1 form, then new page 2 form, and so on..
I would like to do it in responsive way NOT sub-domaine like (http://mobile/blah.com)
PS: I want to avoid third party bundle !!
Advice, recommandation, direction anything than can help me
My controller:
public function ownerRegisterAction(Request $request)
{
$em = $this->getDoctrine()->getManager();
$owner = new Owner();
$form = $this->createCreateForm($owner);
$form->handleRequest($request);
if($form->isSubmitted() && $form->isValid())
{
$password = $this->get('security.password_encoder')
->encodePassword($owner->getOwner(), $owner->getOwner()->getPassword());
$owner->getOwner()->setPassword($password);
$owner->getOwner()->setStatus('owner');
$owner->getOwner()->setIsValid(0);
$em->persist($owner);
$em->flush();
// Login users after registration
$this->get('apx_auth_after_register')->authenticateUser($tenant->getTenant());
$response = $this->forward('PagesBundle:SearchOwner:search', ['owner' => $owner]);
return $response;
}
return $this->render('::form/owner-register.html.twig', array(
'form' => $form->createView()
));
}
My Form Type :
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('corporate', CorporateType::class, ['expanded' => true, 'multiple' => false, 'label' => false])
//->add('postcode', NumberType::class,['label' => false])
->add('type', TypeType::class, ['expanded' => true, 'multiple' => false,'label' => false])
->add('room', NbRoomType::class,['expanded' => true, 'multiple' => false,'label' => false])
->add('rent', NumberType::class,['label' => false])
->add('area', NumberType::class,['label' => false])
->add('images', CollectionType::class, [
'entry_type' => ImagesFlatType::class,
'allow_add' => true,
'required' => false,
'allow_delete' => true,
'label' => false,
])
->add('fee', NumberType::class, ['label' => false, 'required' => false])
// to be defined in list by city
->add('transport', TextType::class,['label' => false, 'required' => false])
->add('furnished', FurnishedType::class, ['expanded' => true, 'multiple' => false,'label' => false])
->add('vip', LesVipType::class, ['expanded' => true, 'multiple' => true,'label' => false])
->add('feature', FeatureType::class, ['expanded' => true, 'multiple' => true,'label' => false])
->add('description', TextareaType::class,['label' => false, 'required' => false])
// City ajax call to be fix
->add('location', EntityType::class, ['label' => false,
'class' => 'PagesBundle:City',
'choice_label' => 'zone']);
Thanks all
Nico
Well, I don't think it is a hard task. Actually it's seems somewhat obvious (at least for me though).
You could to use something like TabView or Accordion-like view. All of this can be achieved by using pure CSS (and maybe Javascript).
As you can see it is not related to Symfony at all. By using CSS + Media-queries I'm sure you can get done the desired UI.
Without mobile subdomain and with css help only
When you use FormBuilder::add() you create an instance of FormType which can be :
the entire form,
a field,
a sub form.
So they all behave the same way.
Then you can easily split your FormType in 3 SubFormTypes :
namespace AppBundle\Form;
/*
* When forms get heavy, it becomes handy to use aliases
* on import use statements since it eases the reading in code
* and reduces the list of imports.
*/
use Type as AppType;
use Symfony\Bridge\Doctrine\Form\Type as OrmType;
use Symfony\Component\Form\Extension\Core\Type as CoreType;
// An abstract class which all sub form will extend.
abstract class AbstractSubFormType extends AbstractType
{
/*
* We need to add a submit button on the sub form
* which will be only visible on mobile screens.
*
* Then to get help of css class :
* use ".submit-sub_owner { display: 'none'; }" for desktop screens,
* and ".submit-sub_owner { display: 'block'; }" for mobile screens.
*
* Plus we need dynamic hiding of next sub forms from controller :
* use ".sub_owner-next { display: 'none'; }" for mobile screens.
*/
public function buildForm(FormBuilderInterface $builder, array $options = array())
{
$builder->add('submit', CoreType\SubmitType::class,
'attr' => array(
// Needed to target sub owner controller.
'formaction' => $options['sub_action'],
// Hides sub owners submit button on big screens.
'class' => 'submit_sub_owner',
)
));
}
public function configureOptions(OptionsResolver $resolver)
{
$subNextNormalizer = function (Options $options, $subNext) {
$hideNextSubOwners = isset($options['sub_next']) && $subNext;
// Sets "attr" option of this sub owner type.
$options['attr']['class'] = $hideNextSubOwners ? 'sub_owner-next' : '';
}
// Allows custom options.
$resolver->setDefaults(array('sub_action' => '', 'sub_next' => false));
// Hides sub owners exept the first stage from main controller.
$resolver->setNormalizer('sub_next', $subNextNormalizer);
}
}
// Stage 1
class SubOwnerOne extends AppType\AbstractSubFormType
{
public function buildForm(FormBuilderInterface $builder, array $options = array())
{
// Arbitrary cut of your example
$builder
->add('corporate', AppType\CorporateType::class)
->add('type', AppType\TypeType::class)
->add('room', AppType\NbRoomType::class);
// Call parent to optionnaly add sumbit button
parent::builForm($builder, $options);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array('expanded' => true, 'multiple' => false));
}
public function getName()
{
return 'sub_owner_1';
}
}
// Stage 2
// note that this one is tricky because there is an image that may be a file.
class SubOwnerTwo extends AppType\AbstractSubFormType
{
public function buildForm(FormBuilderInterface $builder, array $options = array())
{
// Arbitrary cut of your example
$builder
//->add('postcode')
->add('area')
->add('rent')
->add('images', CoreType\CollectionType::class, array(
'entry_type' => AppType\ImagesFlatType::class,
'allow_add' => true,
'allow_delete' => true,
'required' => false,
));
// Call parent to optionnaly add sumbit button
parent::builForm($builder, $options);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefault('data_class', CoreType\NumberType::class);
}
public function getName()
{
return 'sub_owner_2';
}
}
// Last stage
class SubOwnerThree extends AppType\AbstractSubFormType
{
public function buildForm(FormBuilderInterface $builder, array $options = array())
{
// Arbitrary cut of your example
$builder
->add('fee', CoreType\NumberType::class, array('required' => false))
// to be defined in list by city
->add('transport', CoreType\TextType::class, array('required' => false))
->add('furnished', AppType\FurnishedType::class) // define options in class
->add('vip', AppType\LesVipType::class) // when belongs to AppType
->add('feature', AppType\FeatureType::class) // ...
->add('description', CoreType\TextareaType::class, array('required' => false))
// City ajax call to be fix
->add('location', OrmType\EntityType::class, array(
'class' => 'PagesBundle:City',
'choice_label' => 'zone',
));
// Call parent to optionnaly add sumbit button
parent::builForm($builder, $options);
}
public function getName()
{
return 'sub_owner_3';
}
}
One FormType to wrap them all :
class OwnerType extends AbstractType
{
public function createForm(FormBuilderInterface $builder, array $options = array())
{
$sub = isset($option['sub_action']) ? $options['sub_action'] : false;
$next = isset($option['sub_next']) ? $options['sub_next'] : false;
$builder
->add('stage_one', AppType\SubOwnerOne::class, array(
'sub_action' => $sub, // get form action from controllers.
))
->add('stage_two', AppType\SubOwnerTwo::class, array(
'sub_action' => $sub,
'sub_next' => $next, // hide sub owners from main controller on mobile screens.
))
->add('final', AppType\SubFormTypeThree::class, array(
'sub_action' => $sub,
'sub_next' => $next,
))
->add('submit', CoreType\SubmitType::class, array(
'attr' => array(
// Allows using ".submit-owner { display: 'none'; }" for mobile screens.
'class' => 'submit-owner',
),
));
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'label' => false,
// Allows custom options
'sub_action' => '',
'sub_next' => false,
));
}
// ...
}
Controllers :
// Holds two controllers, one for owner type and another for sub owner types.
class OwnerController extends \Symfony\Bundle\FrameworkBundle\Controller\Controller
{
/*
* Main controller.
*
* This handles initial request
* and renders a view with a form view of OwnerType
*/
public function ownerRegisterAction(Request $request)
{
$owner = new Owner();
// Customizes options
$form = $this->createFormBuilder($owner, array(
// Uses route name of the other controller
'sub_action' => $this->generateUrl('handle_sub_owners'),
// Hides next stages
'sub_next' => true,
))->getForm();
$form->handleRequest($request);
if($form->isSubmitted && $form->isValid()) {
$password = $this->get('security.password_encoder')
->encodePassword($owner->getOwner(), $owner->getOwner()->getPassword());
$owner->getOwner()->setPassword($password);
$owner->getOwner()->setStatus('owner');
$owner->getOwner()->setIsValid(0);
$em = $this->getDoctrine()->getManager();
$em->persist($owner);
$em->flush();
// Login users after registration
$this->get('apx_auth_after_register')->authenticateUser($tenant->getTenant());
// Forwards quiting registration process
return $this->forward('PagesBundle:SearchOwner:search', array('owner' => $owner));
}
// This view should use css rules from above comments
return $this->render('::form/owner-register.html.twig', array(
'form' => $form->createView(),
));
}
/**
* Secondary controller handles sub forms
* and renders one form view among the sub forms.
*
* #Route('/_sub_owners', name="handle_sub_owners")
*/
public function handleSubOwnersAction(Request $request)
{
// Customize sub form action
$action = array('sub_action' => $this->generateUrl('handle_sub_owners'));
// Option "sub_next" will default to false.
$form = $this->createForm(AppType\OwnerType::class, new Owner(), $action);
$form->handleRequest($request);
$subOwner1 = $form->get('stage_one');
$subOwner2 = $form->get('stage_two');
$finalOwner = $form->get('final');
// Last stage is done, reforwards to the main controller.
if ($finalOwner->isSubmitted() && $finalOwner->isValid()) {
// Submits $data to new OwnerType as $form has been submitted by "handleRequest()" call
$owner = $this->createForm(AppType\OwnerType::class, new Owner());
$owner->get('stage_one')->submit(json_decode($finalOwner->get('j_stage_one')->getData()));
$owner->get('stage_two')->submit(json_decode($finalOwner->get('j_stage_two')->getData()));
$owner->get('final')->submit($finalOwner->getData());
// Form in main controller will handle the request again,
// so we need to pass normalized data.
return $this->forward('App:Owner:ownerRegister', array(), $owner->getNormData())
}
// Stage 2 is done
if ($subOwner2->isSubmitted() && $subOwner2->isValid()) {
// Gets back json of stage 1
$finalOwner->add('j_stage_one', 'hidden', array(
// Unmaps this hidden field as it won't match any property of Owner
'mapped' => false,
'data' => $subOwner1->get('j_stage_1')->getData(),
));
// Saves json of stage 2
$finalOwner->add('j_stage_two', 'hidden', array(
'mapped' => false,
'data' => json_encode($subOwner2->getData(), true),
));
// Renders final stage
return $this->render('::form/owner-register.html.twig', array(
'form' => $finalOwner->createView(),
));
}
// Stage 1 is done
if ($subOwner1->isSubmitted() && $subOwner1->isValid()) {
// Save json of $subOwner1
$subOwner2->add('j_stage_one', 'hidden', array(
'mapped' => false,
'data' => json_encode($subOwner1->getData(), true),
));
// Render stage 2
return $this->render('::form/owner-register.html.twig', array(
'form' => $subOwner2->createView(),
));
}
// Else renders stage 1
return $this->render('::form/owner-register.html.twig', array(
'form' => $subOwner1->createView(),
));
}
}
View
{# ::form/owner-register.html.twig #}
...
<style type="text/css">
/* #media mobile screens */
.sub_owner-next, .submit-owner { display: 'none'; }
.submit-sub_owner { display: 'block'; }
/* #media desktop screens */
.submit-sub_owner { display: 'none'; }
</style>
...
{{ form_start(form) }}
{% form sub_form in form %}
{{ form_start(sub_form) }}
{{ form_widget(sub_form) }}
{{ form_end(sub_form) }}
{% endear %}
{{ form_end(form) }}
...
Alternative use Mobile Detection
There are many php libraries to detect mobile browsers.
For example https://github.com/serbanghita/Mobile-Detect
which provides a bundle for symfony :
https://github.com/suncat2000/MobileDetectBundle
So you can even use a helper in a previous implementation :
$mobileDetector = $this->get('mobile_detect.mobile_detector');
$mobileDetector->isMobile();
$mobileDetector->isTablet()
If you want to split form using responsive way, you should format form output in your view (e.g. TWIG file). Place form parts in separate containers and then use CSS media queries and JS. Good luck!
I did something a few years back in which a passed a 'step' var into the formType which I incremented after each post.
I used an if statement in the formType to build the form depending on the step value.
I'm new to Symfony. I want to do collection of forms.
I'm getging something like:
class SkillType extends AbstractType
public function getName()
{
return 'skills_cfg';
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name', 'text', array(
'label' = 'name'
))
->add('name_short', 'text', array(
'label' => 'short name'
));
}
}
And I get a Form where I want to use collection on that form is before:
class AbsType extends AbstractType
{
private $object;
public function __construct($object)
{
$this->object = $object;
}
public function getName()
{
return '';
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('row', 'collection', array(
'type' => $this->object,
'allow_add' => true,
'prototype' => true,
))
->add('addMore', 'button', array(
'attr' => array(
'value' => 'add more'
),
))
->add('submit', 'submit', array(
'attr' => array(
'value'=>'save',
),
'label' => 'save'
));
}
}
And in my controller i getting forms like that:
class InstallController extends Controller
{
public function indexAction($configPage, Request $request)
{
$skillForm= $this->createForm(new AbsType(new SkillType()));
$SkillConfig=new SkillsCfg();
if($request->isMethod('POST')) {
$skillForm->handleRequest($request);
if ($skillForm->isValid()) {
$em=$this->getDoctrine()->getManager();
$em->persist($SkillConfig);
$em->flush();
}
}
return array(
'form' => $skillForm->createView(),
'page' => $configPage
);
}
The entity and view looks like that:
/**
* #var string
*
* #ORM\Column(name="name", type="string", length=30)
*/
private $name;
/**
* #var string
*
* #ORM\Column(name="name_short", type="string", length=10)
*/
private $nameShort;
Of course i get getters and setters.
View:
var emailCount = '{{ form.row|length }}';
jQuery(document).ready(function() {
jQuery('#addMore').click(function(e) {
e.preventDefault();
var emailList = jQuery('#fields-list');
// grab the prototype template
var newWidget = emailList.attr('data-prototype');
// replace the "__name__" used in the id and name of the prototype
// with a number that's unique to your emails
// end name attribute looks like name="contact[emails][2]"
newWidget = newWidget.replace(/__name__/g, emailCount);
emailCount++;
// create a new list element and add it to the list
var newLi = jQuery('<li></li>').html(newWidget);
newLi.appendTo(emailList);
});
})
</script>
{% endblock javascripts %}
{% block body %}
{{ form_start(form) }}
<ul id="fields-list"
data-prototype="{{ form_widget(form.row.vars.prototype)|e }}">
{% for newRow in form.row %}
<li>
{{ form_errors(newRow) }}
{{ form_widget(newRow) }}
</li>
{% endfor %}
</ul>
{{ form_end(form) }}
{% endblock body %}
And I get an error:
An exception occurred while executing 'INSERT INTO skills_cfg (name,
name_short) VALUES (?, ?)' with params [null, null]:
SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'name'
cannot be null
Why does it happen? And how do I solve it?
Change the indexAction like this:
public function indexAction($configPage, Request $request)
{
$em=$this->getDoctrine()->getManager();
$skillForm= $this->createForm(new AbsType(new SkillsCfg()));
$skillForm->handleRequest($request);
if ($skillForm->isValid())
{
foreach($form->get('row')->getData() as $row)
{
$em->persist($row);
}
$em->flush();
}
return array(
'form' => $skillForm->createView(),
'page' => $configPage
);
}
If its still not working start debugging. Try to get the data from row and check if this is an array.
I have a form with dynamic choice field "languages". The languages can be diffrent for diffrent categories. I want them tho be all checked by default, so I use 'data' option for this. But it only works if I dont use Events. If I try to use it in my $formModifier function to add languages dynamically it will not work, and all languages are unchecked... Any idea how to fix it ?
Here is my buildForm function from ProjectType class
public function buildForm(FormBuilderInterface $builder, array $options)
{
(...)
$builder
->add('category', 'choice', array(
'choices' => Category::$NAMES,
'multiple' => false,
'placeholder' => '',
))
->add('name', 'text')
->add('description', 'textarea', array(
'attr' => array('rows' => '10', 'class' => 'tinymce'),
))
->add('save', 'submit', array('label' => 'form.save'));
$formModifier = function (FormInterface $form, User $user, $category = null) {
$languages = null === $category ? array() : $this->userReputation->getAllowedLanguages($user, $category);
$form->add('languages', 'choice', array(
'multiple' => true,
'expanded' => true,
'choices' => $languages,
'data' => array_keys($languages),
));
};
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (FormEvent $event) use ($formModifier, $user) {
$project = $event->getData();
$formModifier($event->getForm(), $user, $project->getCategory());
}
);
$builder->get('category')->addEventListener(
FormEvents::POST_SUBMIT,
function (FormEvent $event) use ($formModifier, $user) {
$category = $event->getForm()->getData();
$formModifier($event->getForm()->getParent(), $user, $category);
}
);
}
I have this script in view to make ajax calls when category is changed by the user:
<script>
function requireReadyFunction() {
var $category = $('#project_category');
// When category gets selected ...
$category.change(function() {
// ... retrieve the corresponding form.
var $form = $(this).closest('form');
// Simulate form data, but only include the selected category value.
var data = {};
data[$category.attr('name')] = $category.val();
// Submit data via AJAX to the form's action path.
$.ajax({
url : $form.attr('action'),
type: $form.attr('method'),
data : data,
success: function(html) {
// Replace current languages field ...
$('#project_languages').replaceWith(
$(html).find('#project_languages')
);
}
});
});
}
</script>