For one of my objects I need to create some dynamic form rendering... But I cant figure out how to do this in Sonata Admin. For example when I create an object I have a field type. In this field I select a type that my object is going to be. Now when I select the type I want to make a field appear, based on the type. For example, if I select type "Carousel" I want to show a field that is selecting all object form entity Gallery. If I select type "Product" I want to display field with all products to selectt from... How can I acheve that?
Right now I have this:
/**
* #param FormMapper $formMapper
*/
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->with('Module', array(
'class' => 'col-md-6'
))
->add('position')
->add('type', null, array(
'attr' => array('class' => 'module_type')
))
->add('items', 'entity', array(
'class' => 'ApplicationSonataMediaBundle:Gallery'
))
->end()
;
}
And I have overriden the edit template:
{% extends 'SonataAdminBundle:CRUD:edit.html.twig' %}
{% block javascripts %}
{{ parent() }}
<script type="text/javascript">
$(document).ready(function () {
$(".module_type").change(function() {
});
});
</script>
{% endblock %}
As you can see the gallery is hardcoded now..
I cant figure out how to do this now... How to say that if the value selected is this, use that entity in field... The problem is that the way form is rendered in Sonata is very complicated... I dont understand it..
maybe I should use ajax? But again, when I send a value and get the response how to add a field without refresh?
Any help appreciated.
After researching forever to find a way to use dynamic dropdowns using ajax and sonata with symfony4 i would like to share my solution how it actually works for me.
In my case i have a district and this district has different cities. I have a company that first chooses a district and then depending of that it´s city.
What i did:
Create your entity for the districts
Create your entity for the cities
Go in your main class (in my case this is the company entity and add two entities for the districts and cities
/**
* #ORM\ManyToOne(targetEntity="App\Wdm\MainBundle\Entity\Model\Cities", inversedBy="id")
*/
private $city;
/**
* #ORM\ManyToOne(targetEntity="App\Wdm\MainBundle\Entity\Model\Districts", inversedBy="id")
*/
private $district;
Update your database schema and fill the cities and districts fields with some example data
Go into your AdminClass in the configureFormFields Function and add the following (make sure to use the 'choice_label' option correctly with the corresponding field in your district or city entity.
protected function configureFormFields(FormMapper $formMapper)
{ $formMapper
// Some other added fields
->add('district', EntityType::class, [
'choice_label' => 'name',
'class' => Districts::class,
'placeholder' => '',
])
->add('city', EntityType::class, [
'choice_label' => 'name',
'class' => Cities::class,
'placeholder' => '',
])
;
This should already work pretty well. You should now be able to have a dependent field. Let´s take a look to the AJAX magic now.
Go into your AdminClass (same as the configureFields-Class) and add the following
protected function configureRoutes(RouteCollection $collection)
{ $collection->add('reloadCities', 'reload-cities');
}
Now you have a route that you can access from your ajax url. Now create a new Controller Class, wherever you want...
<?php
namespace App\Wdm\MainBundle\Controller;
use Symfony\Component\HttpFoundation\Request;
use Sonata\AdminBundle\Controller\CRUDController as BaseController;
use App\Wdm\MainBundle\Entity\Model\Cities;
class CitiesController extends BaseController
{
public function reloadCitiesAction(Request $request)
{ $districtid = $request->request->get('id');
$cities = $this->getDoctrine()->getRepository(Cities::class)->findBy(array("district" => $districtid));
return $this->render("company/cities.html.twig", array("cities" => $cities));
}
}
... and don´t forget to register this controller in your services.yaml...
admin.company:
class: App\Wdm\MainBundle\Admin\CompanyAdmin
arguments:
- ~
- App\Wdm\MainBundle\Entity\Model\Company
- App\Wdm\MainBundle\Controller\CitiesController (THIS IS THE NEW ROW)
... and finally the little template that is being called in this function...
// THIS IS THE cities.html.twig
{% for city in cities %}
<option value="{{ city.id }}">{{ city.name }}</option>
{% endfor %}
So far so good. We now got the logic that get´s the data from the ajax call and returns it to your sonata admin edit form. The only thing thats missing now is the jquery code that is needed in the edit template of sonata admin.
Go into your AdminClass and insert the following code (e.g. before configureFormFields)
public function getTemplate($name)
{
switch ($name) {
case 'edit':
return 'company/cities_admin.html.twig';
break;
default:
return parent::getTemplate($name);
break;
}
}
Now we create this cities_admin.html.twig template that overrides the default template
{% extends 'SonataAdminBundle:CRUD:edit.html.twig' %}
{% block form %}
{{ parent() }}
<script type="text/javascript">
$(document).ready(function () {
$("#ID_OF_YOUR_DISTRICT_SELECT_FIELD").change(function () {
$.ajax({
url: "{{ admin.generateUrl('reloadCities') }}",
data: {
'id': $(this).val(),
'uniquid': '{{ admin.uniqid }}'
},
method: 'POST',
success: function (html) {
$("#ID_OF_YOUR_CITY_SELECT_FIELD").html(html);
},
error: function (data) {
// more code
}
});
});
});
</script>
{% endblock %}
That´s it. Should work like a charm.
Sonata provides you with the 'sonata_type_choice_field_mask' type which allows you to change the fields displayed on the form dynamically depending on the value of this 'sonata_type_choice_field_mask' input so you don't have to use ajax.
Here is the doc where you can find everything about sonata types and the choice field mask.
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->add('type', 'sonata_type_choice_field_mask', array(
'choices' => array(
//The list of available 'Type' here
'choice1',
'choice2'
),
'map' => array(
//What you want to display depending of the selected option
'choice1' => array(
// List of the fields displayed if choice 1 is selected
'field1', 'field3'
),
'choice2' => array(
// List of the fields displayed if choice 2 is selected
'field2', 'field3'
)
),
'placeholder' => 'Choose an option',
'required' => true
))
->add('field1', 'entity', array(/* Options for entity1 goes here */))
->add('field2', 'entity', array(/* Options for entity2 goes here */))
->add('field3')
;
}
If I have realy understand your need you have to use ajax of course, first you need to add new admin route to this EntityAdminController to do it you have to override the configureRoutes method and to add your new route like this :
protected function configureRoutes(RouteCollection $collection)
{
$collection->add('reloadCities', 'realod-cities');
}
Then you have to create a new controller which gonna have the action definition for your new route like :
use Sonata\AdminBundle\Controller\CRUDController as BaseController;
class CitiesController extends BaseController
{
public function reloadCitiesAction()
{
// some code
return $this->render(...);
}
}
Then you have to override the SonataAdminBundle:CRUD:edit.html.twig template and set your javascript event listener like this :
{% extends 'SonataAdminBundle:CRUD:edit.html.twig' %}
{% block form %}
{{ parent() }}
<script type="text/javascript">
$(document).ready(function () {
countries.change(function () {
$.ajax({
url: "{{ admin.generateUrl('reloadCities') }}",
data: {
'id': $(this).val(),
'uniquid': '{{ admin.uniqid }}'
},
method: 'POST',
success: function (html) {
// code...
},
error: function (data) {
// more code
}
});
});
});
</script>
Related
English is not my native language, sorry for that.
I have a meet entity (rendezVous) and in this entity, i have two others mapped entities doctor(docteur) and customer(client).
I want to change the list of doctors when choosing a customer.
For that, I create a form events in my RendezVousType, but the problem is when i choose a customer, the Client entity is empty in my formModifier.
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('client', EntityType::class, array(
'class' => 'AppBundle:Client',
'placeholder' => '',
));
$formModifier = function (FormInterface $form, Client $client = null) {
$idEspece = null === $client ? 0 : $client->getId();
$form->add('docteur', EntityType::class, array(
'class' => 'AppBundle:Docteur',
'placeholder' => '',
'query_builder' => function (DocteurRepository $er) use ($idEspece) {
return $er->getByClientEspece($idEspece);
},
));
};
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (FormEvent $event) use ($formModifier) {
$data = $event->getData();
$formModifier($event->getForm(), $data->getClient());
}
);
$builder->get('client')->addEventListener(
FormEvents::POST_SUBMIT,
function (FormEvent $event) use ($formModifier) {
$client = $event->getForm()->getData();
$formModifier($event->getForm()->getParent(), $client);
}
);
}
When I set a default value for $idEspece, the query builder returns the correct list.
Your form type is valid but Symfony don't do that automatically using ajax, then the doctor only appear after form submission.
Then to get the list of doctors using ajax you need add some additional logic.
Firstly, create a action in your controller to get the list of doctors and return a json
/**
* #Route(name="get_doctors", path="/get_doctors" )
*/
public function getDoctorsAction(Request $request)
{
$client = $request->get('rendez_vous')['client'];
$clients = $this->getDoctrine()
->getRepository('AppBundle:Docteur')
->getByClientEspece($client)
->select('s.id', 's.name')
->getQuery()
->getArrayResult();
$indexedClients = array_column($clients, 'name', 'id');
return new JsonResponse($indexedClients);
}
Add the following jquery plugin in your scripts.
https://appelsiini.net/projects/chained/
<script type="text/javascript" src="{{ asset('js/jquery.chained.min.js') }}"></script>
<script type="text/javascript" src="{{ asset('js/jquery.chained.remote.min.js') }}"></script>
Add the following javascript after your form to initialize the jquery plugin in your doctor input:
{{ form(form) }}
<script>
$("#rendez_vous_docteur").remoteChained({
parents: "#rendez_vous_client",
url: '{{ path('get_doctors') }}',
clear: true,
loading: "Loading..."
});
</script>
ADIVSE: review and update as needed ids and parameters used in the example.
Thanks for you help, but I solved my issue by myself by simply doing a
php app/console cache:clear
I'm sorry, if you loose your time for this
PS: I followed this tutorial on symfony doc for create form events.
I have showUsersAction()-method inside the Defaultcontroller which should render a form where it should be possible to select a user from a list, press a submit-button and then redirects to a route /showItems/{userId} where the items of a user are shown.
I know that it would be possible to do that easy with a link, but I want to make use of ChoiceType:
First I copied an example of ChoiceType from the Symfony documentation with a minimal change:
/**
* #Route("/showUsers", name="showUsers")
*/
public function showUsersAction(){
$users = $this->getDoctrine()->getManager()->getRepository('AppBundle:User')->findAll();
$form = $this->createFormBuilder()
->setMethod('POST')
->add('user', ChoiceType::class, [
'choices' => $users,
'choice_label' => function($user) {
/** #var User $user */
return strtoupper($user->getUsername());//here is the problem
},
'choice_attr' => function($user) {
return ['class' => 'user_'.strtolower($user->getUsername())];
},
])
->getForm();
return $this->render('default/showUsers.html.twig',
array('users' => $users, 'form' => $form->createView()));
}
I am sure $users gives an array with objects of the class User. When I execute the route in the browser I get following error message:
Error: Call to a member function getUsername() on a non-object
in src\AppBundle\Controller\DefaultController.php at line 50
Line 50 is the commented line return strtoupper($user->getUsername());
What is the problem and how can I solve?
And how can I get the selected User after I submitted via a submit button to the same route?
EDIT: (because of possible duplication)
Of course I know that the method getUsername() can not be called, because $user is a non-object, which should not be related to the Symfony documentation. So my question relates to a Symfony special solution which has absolutly nothing to do with 100 of other problems where the Error is the same.
Use entity type instead. Here is a link to documentation. It's a child type of a choice type, with exactly same functionality, and also every option returns an entity object.
Because I had trouble with setting up the entity type field too, I decided to post my solution for the whole action function and the twig file here:
action method:
/**
* #Route("/showUsers", name="showUsers")
*/
public function showUsersAction(Request $request){
// gets array of all users in the database
$users = $this->getDoctrine()->getManager()->getRepository('AppBundle:User')->findAll();
$form = $this->createFormBuilder()
->setMethod('POST')
->add('users', EntityType::class, array(
'class' => 'AppBundle:User',
'choices' => $users,
// This combination of 'expanded' and 'multiple' implements radio buttons
'expanded' => true,
'multiple' => false,
'choice_label' => function ($user) {
return $user->__toString();
}
))
// Adds a submit button to the form.
// The 'attr' option adds a class to the html rendered form
->add('selected', SubmitType::class, array('label' => 'Show User Items', 'attr' => array('class' => 'button')))
->getForm();
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// gets the selected user
$selectedUser = $form["users"]->getData();
// redirects to route 'showItems' with the id of the selected user
return $this->redirectToRoute('showItems', array('userId' => $selectedUser->getId()));
}
// renders 'default/showUsers.html.twig' with the form as an argument
return $this->render('default/showUsers.html.twig', array('form' => $form->createView()));
}
twig file:
{#
// app/Resources/views/default/showUsers.html.twig
Description:
twig file that implements a form in which one of all users can get
selected via radio button to show the items of the user after a click
on the submit button.
#author goulashsoup
#}
{% extends 'base.html.twig' %}
{% block title %}Users{% endblock %}
{% block body %}
<div class="users">
{{ form_start(form) }}
{% for user in form.users %}
{{ form_widget(user) }}
{{ form_label(user) }}
<br>
{% endfor %}
<br>
{{ form_end(form) }}
</div>
{% endblock %}
In Symfony I have a twig page that displays a form with text fields and check boxes. The form contains data for a question and four possible answers. The user can edit the data and select one answer that is correct.
At the moment I have all the text fields where the user can change the data and four check boxes. Instead of the check boxes I need radio buttons(this is to allow the user to select only one choice). Also I need the check boxes to be on the right hand side of text fields for each possible answer. How can I do this in Symfony. Would much appreciate some help.Thanks.
Using Collection of Forms to build the entire Form
Answer Form:
class AnswerFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('answer');
$builder ->add('isCorrect', null , array('label' => false,));
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array('data_class' => 'QuizBundle\Entity\Answer'));
}
public function getName()
{
return 'quiz_bundle_answer_form_type';
}
}
Question Form:
class QuestionFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('image');
$builder->add('question');
$builder->add('answers', CollectionType::class, array('entry_type' => AnswerFormType::class));
$builder->add('Submit', SubmitType::class, array('label' => 'Submit'));
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array('data_class' => 'QuizBundle\Entity\Question'));
}
public function getName()
{
return 'quiz_bundle_question_form_type';
}
}
This is my Twig:
{% extends 'base.html.twig' %}
{% block body %}
Edit record page
{{ form_start(form) }}
{{ form_row(form.image) }}
{{ form_row(form.question) }}
{% for answers in form.answers %}
<li>{{ form_row(answers.answer) }}</li>
{% endfor %}
{{ form_end(form) }}
{% endblock %}
You can not mix a radio input with text inputs to let the user edit them.
It is not possible with HTML.
No other choice, you have to :
Recreate the radio behavior with Javascript on the client side
Write some CSS to make your checkboxes looks like radio boxes.
Attach a form constraint which validates only one checkbox is selected after submit.
To put the text inputs at the right side, you have to customize the form widgets. Take a look at the documentation : http://symfony.com/doc/current/cookbook/form/form_customization.html
I understand what you are trying to do because of your previous question. For layout of the form, you can do something like this to make it look better:
{{ form_start(form) }}
{{ form_label(form.image) }}{{ form_widget(form.image) }}
{{ form_label(form.question) }}{{ form_widget(form.question) }}
{% for ans in form.answers %}
<li>{{ form_label(ans.answer) }}{{ form_widget(ans.answer) }}
{{ form_label(ans.isCorrect) }}{{ form_widget(ans.isCorrect) </li>
{{ form_end(form) }}
However, I don't think this is the solution for you. Like Alsatian mentioned, you need to use Javascript to check for "onchange" events for the radio buttons. You are creating a form class, which in my opinion doesn't always work for every design. You might try just a "createFormBuilder" instead and customize exactly as you need.
public function showQuestionFormAction($ansID, Request $request){
// Get Answers...
$em = $this->getDoctrine()->getManager();
$qb->select('a')
->from('AppBundle:Answer', 'a')
->where('a.answer_id = :ansID')
->setParameter('ansID', $ansID);
$ans = $qb->getQuery()->setMaxResults(1)->getOneOrNullResult();
$form = $this->createFormBuilder()
->add('image') // Not sure what type this is???
->add('question', EntityType::class, array(
'class' => 'AppBundle:Question',
'label' => 'Question:',
'choice_label' => 'question', // Label shown.
'choice_value' => 'question_id', // What data you want returned.
'attr' => array('class' => 'question_id'), // For css styling only, you may not need this.
'data' => $ques->getText(), // This is a method (getter) to get question text.
))
->add('ans1', RadioType::class, array(
'label' => '$ans->getAnswer1()',
'required' => false,
'attr' => array(
'onchange' => "check_answer('ans1')", // Javascript function.
'checked' => true,
),
))
->add('ans2', RadioType::class, array(
'label' => '$ans->getAnswer2()',
'required' => false,
'attr' => array(
'onchange' => "check_answer('ans2')", // Javascript function.
'checked' => false,
),
))
...
Then you can use a simple javascript function like so. I made this quickly, so there may be a better/simpler way to design this.
// script/check_answer.js
function check_answer(id){
var ans1 = document.getElementById("form_ans1");
var ans2 = document.getElementById("form_ans2");
var ans3 = document.getElementById("form_ans3");
var ans4 = document.getElementById("form_ans4");
// Check what is changed.
if(id == 'ans1'){
ans2.checked = false;
ans3.checked = false;
ans4.checked = false;
}
if(id == 'ans2'){
ans1.checked = false;
ans3.checked = false;
ans4.checked = false;
}
...
}
Hope that helps.
I'm using EasyAdminBundle and Symfony 3.0 and have a field "status" which is by default textfield and I want it to be choice type but with ability of showing or hiding some other field on change event using javascript.
The problem is that field is rendered but somehow it won't hide field on changing option.
Also, I'm not sure if my approach is good because symfony docs aren't clear enough and, for me, their approach isn't working.
So, code looks like this
MyBundle/Controller/AdminController
public function createEditForm($entity, array $entityProperties)
{
$editForm = parent::createEditForm($entity, $entityProperties);
if($entity instanceof Employee){
$editForm->remove('status');
$editForm->add('status', ChoiceType::class, array(
'choices' => array(
'Working' => "active",
'Not working' => 'inactive'
)
));
}
return $editForm;
}
public function createNewForm($entity, array $entityProperties)
{
$newForm = parent::createNewForm($entity, $entityProperties);
if($entity instanceof Employee){
$newForm->remove('status');
$newForm->add('status', ChoiceType::class, array(
'choices' => array(
'Choose an option' => null,
'Working' => true,
'Not working' => false
)
));
}
return $newForm;
}
MyBundle/Resources/views/Form/employeestatusfield.html.twig
{% extends 'EasyAdminBundle:default:new.html.twig' %}
{% form_theme form 'EmployeeBundle:Form:employeestatusfield.html.twig' %}
{% block _employee_status_widget %}
<script type="text/javascript">
alert('tiiw');
$(document).ready(function () {
toggleFields();
$("#employee_status").change(function () {
toggleFields();
});
});
function toggleFields() {
if ($("#employee_status").valueOf() == 1) {
$("#employee_quitAt").hide();
}
else
$("#employee_quitAt").show();
}
</script>
{% endblock %}
Thank you for your help.
I want to upload multiple files with POST request (without Ajax). Can I use Symfony 2's form collection field with type file like this:
Code in Entity:
public $pictures;
public function __construct()
{
$this->pictures = new \Doctrine\Common\Collections\ArrayCollection();
}
Code in Form Class:
$builder->add('pictures', 'collection', array(
'type' => 'file',
'required' => false,
'attr' => array(
'multiple' => 'multiple'
)
));
Code in Twig:
{% for picture in form.pictures %}
<td>
{{ form_widget(picture) }}
</td>
{% endfor %}
I tried, but it doesn't seem to work. It is not showing any errors, but it is not showing the input file either. Any ideas?
To actually render input types you will need to set the allow_add option in the collection to true and use the form prototype of the collection, javascript and a button to add file fields.
An example based in the documentation (Collection- adding and removing)
The form:
<form action="..." method="POST" {{ form_enctype(form) }}>
{# ... #}
{# store the prototype on the data-prototype attribute #}
<ul id="image-container" class="collection-container" data-prototype="{{ form_widget(form.images.vars.prototype) | e }}">
{% for imageField in form.images%}
<li>
{{ form_widget(imageField) }}
</li>
{% endfor %}
</ul>
Add image
</form>
The script:
<script type="text/javascript">
var imageCount;
jQuery(document).ready(function() {
$(document).on('click', '.collection-add', function () {
var $collectionContainer = $('#' + $(this).data('collection'));
if(!imageCount){imageCount = $collectionContainer.children().length;}
var prototype = $collectionContainer.attr('data-prototype');
var item = prototype.replace(/__name__/g, imageCount);
$collectionContainer.append(item);
imageCount++;
});
})
</script>
This is just an idea, there is still plenty to do depending on your needs.
If this wasn't what you were looking for, maybe you could call the add button click as a workaround.
If you want to show multiple input fields, no, it won't work. A collection type requires you to supply some data before rendering the fields. I've already tried it, and came up creating a separate entity (e.g. File) and and adding relationship to my target entity.
example:
class File
{
// properties
public $file; // this holds an UploadedFile object
// getters, setters
}
FileType:
....
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('file', 'file', [
'required' => false
])
;
}
Product:
class Product
{
// properties
private $images; // ManyToMany relationship
// setters, getters
}
ProductType:
->add('images', 'collection', [
'type' => 'YOUR_FILE_TYPE_NAME',
'by_reference' => false,
'required' => false
])
Product contoller:
public function someAction()
{
...
$image = new File();
$product->addImage($image);
}
I know this solution can be overkill and creates extra tables, but it works.
You need to specify the type of the widget in collection
Look at the: http://symfony.com/doc/2.0/reference/forms/types/collection.html
$builder->add('pictures', 'collection', array(
// each item in the array will be an "email" field
'type' => 'file',
// these options are passed to each "email" type
'options' => array(
'required' => false,
),
));
For further reading I suggest
http://symfony.com/doc/2.0/reference/forms/types/file.html
Also you need to add sth to this collection to display cuz it will be empty when initialize like in your constructor of entity.
The code in the form class is correct, but you also should modify the name of the formfield. The full_name should be form[pictures][] in your case.
It actually seems that modifying a form field name is not possible anymore in sf2.3, but it will be possible in sf2.4 again.
https://github.com/bamarni/symfony/commit/35639824e864ed8d4a4cc0d8360f2c73ae08b507#commitcomment-3627879