Symfony2: Form fragment themeing and TWIG - forms

Can anyone explain why this code:
{% form_theme form _self %}
{% block avo_gallery_upload_widget %}
{% spaceless %}
<label for="name">Name:</label>
{{ form_widget(form.name) }}
<label for="description">Description:</label>
{{ form_widget(form.description) }}
{% endspaceless %}
{% endblock avo_gallery_upload_widget %}
Throws
Method "name" for object "Symfony\Component\Form\FormView" does not exist in MyBundle:Default:upload.html.twig at line 13
For reference: line 13 is {{ form_widget(form.name) }}
But when wrapped in IF clause:
{% form_theme form _self %}
{% block avo_gallery_upload_widget %}
{% spaceless %}
{% if form.name is defined %}
<label for="name">Name:</label>
{{ form_widget(form.name) }}
<label for="description">Description:</label>
{{ form_widget(form.description) }}
{% endif %}
{% endspaceless %}
{% endblock avo_gallery_upload_widget %}
Suddenly everything works fine!
For reference - this is how form looks like:
class GalleryUploadType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder
->add('name', 'text')
->add('description', 'textarea')
;
}
public function getName()
{
return 'avo_gallery_upload';
}
public function getDefaultOptions(array $options){
return array('data_class' => 'Me\MyBundle\Entity\GalleryUpload');
}
}

fabpot closed github issue (2012-07-03) with comment:
Every month, I spend hours trying to reproduce the problem without luck. So, I'm giving up for now as there is probably something else going on in your application (as it works fine for almost everyone). If you have any new information that can be relevant, feel free to reopen a new ticket. Thanks.
Since it probably is something wrong in my application and there is nothing new I could add to the question I am closeing this question.
If you happen to encounter this error have a look at this workaround.
If you have any additional info on reproducing the problem, post it here.

Related

Symfony & Easy Admin : How to apply easyadmin template to custom form

I created a custom form for and integrated it in easyadmin. the forms is displayed, filled and action is working, but the templating is not good :
Here is my Twig :
{% extends "#EasyAdmin/page/content.html.twig" %}
{% form_theme form with easyadmin_config('design.form_theme') only %}
{% block body_id 'easyadmin-edit-User-1' %}
{% block body_class 'edit edit-user' %}
{% block content_title %}
<h1 class="title">Edit Account</h1>
{% endblock %}
{% block content_footer_wrapper '' %}
{% block main %}
{% block entity_form %}
{{ form_start(form) }}
{{ form_widget(form) }}
<button type="submit" class="btn btn-primary">Update</button>
{{ form_end(form) }}
{% endblock entity_form %}
{% endblock %}
{% block body_javascript %}
{{ include('#EasyAdmin/default/includes/_select2_widget.html.twig') }}
{% endblock %}
and my controller :
class UserController extends EasyAdminController
{
public function editaccountAction(UserInterface $loggedUser, Request $request) {
$repository = $this->getDoctrine()->getRepository(User::class);
$id = $loggedUser->getId();
$entity = $repository->find($id);
$form = $this->createForm(UserType::class, $entity);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
/** #var Article $article */
$entity= $form->getData();
$em = $this->getDoctrine()->getManager();
$em->persist($entity);
$em->flush();
$this->addFlash('success', 'Account Saved');
return $this->redirectToRoute('easyadmin');
}
return $this->render('user/editaccs.html.twig', [
'form' => $form->createView(),
]);
}
}
How can I have the same form presentation as edit?
Before
{{ form_start(form) }}
add
{% form_theme form '#EasyAdmin/form/bootstrap_4.html.twig' %}
https://symfony.com/doc/current/form/form_themes.html#applying-themes-to-single-forms
UPD:
In EasyAdmin 4.3 theme would be
{% form_theme form '#EasyAdmin/crud/form_theme.html.twig' %}

Pass a variable to a parent form in twig

I need to pass a variable to a parent form in twig:
{# This block is called for the children and the parent #}
{% block form_rows %}
{% for child in form %}
{% if child.vars.foo == 'bar' %}
{% set form.vars = form.vars|merge({'key': 'value'}) %}
{% endif %}
{# Parent check children key var #}
{% if child.vars.key %}
{# do something #}
{% endif %}
{# Will call children form_rows block recursively #}
{{ form_row(child) }}
{% endfor %}
{% endblock form_rows %}
This of course don't work because you cannot set a variable like this and form is an object not an array.
I couldn't find any solution until now.
Ok, the whole thing is a bad idea, but it should be possible since Twig v1.2 using the attribute function. For that you need a setter Method in you form object, eg:
class YourFormClass {
public $vars;
/* your class code comes here */
public function setVars($newVars) {
$this->vars = $newVars;
}
}
Assuming that the twig form variable is an instance of YourFormClass you can now use the attribute function like this:
{% set newVal = form.vars|merge({'key': 'value'}) %}
{{ attribute(form, 'setVars', [newVal]) }}
{{ dump(form.vars) }}

How is symfony form rendered in the view

I was sure I understand this process, but when I dug deeper I saw I am wrong ;(
Let's take simple form, please notice that this form contains 3 fields
$form = $this->createFormBuilder($defaultData, ['csrf_protection' => false])
->add('email', 'email')
->add('name', 'text')
->add('message', 'textarea')
->getForm()
->createView();
which is rendered as
{{ form(form, {'attr': {'novalidate': 'novalidate'}}) }}
into
Built in "form" block from vendor\symfony\symfony\src\Symfony\Bridge\Twig\Resources\views\Form\form_div_layout.html.twig looks like:
{% block form %}
{% spaceless %}
{{ form_start(form) }}
{{ form_widget(form) }}
{{ form_end(form) }}
{% endspaceless %}
{% endblock form %}
and form_widget(form)
{% block form_widget %}
{% spaceless %}
{% if compound %}
{{ block('form_widget_compound') }}
{% else %}
{{ block('form_widget_simple') }}
{% endif %}
{% endspaceless %}
{% endblock form_widget %}
{% block form_widget_simple %}
{% spaceless %}
{% set type = type|default('text') %}
<input type="{{ type }}" {{ block('widget_attributes') }} {% if value is not empty %}value="{{ value }}" {% endif %}/>
{% endspaceless %}
{% endblock form_widget_simple %}
Let's modify form and form_widget a bit:
{% block form %}
{% spaceless %}
{{ form_start(form) }}
-form_widget before-
{{ form_widget(form) }}
-form_widget after-
{{ form_end(form) }}
{% endspaceless %}
{% endblock form %}
{% block form_widget %}
{% spaceless %}
- form_widget call -
{% if compound %}
- form_widget compound-
{{ block('form_widget_compound') }}
{% else %}
- form_widget simple-
{{ block('form_widget_simple') }}
{% endif %}
{% endspaceless %}
{% endblock form_widget %}
then as an output I got (listing 5):
<form name="form" method="post" action="" novalidate="novalidate">
-form_widget before-
- form_widget call -
- form_widget compound-
<div id="form" novalidate="novalidate">
<div>
<label for="form_email" class="required">Email</label>
<input type="email" id="form_email" name="form[email]" required="required" />
</div>
<div>
<label for="form_name" class="required">Name</label>
- form_widget call -
- form_widget simple-
<input type="text" id="form_name" name="form[name]" required="required" />
</div>
<div>
<label for="form_message" class="required">Message</label>
<textarea id="form_message" name="form[message]" required="required">Type your message here</textarea>
</div>
</div>
-form_widget after-
</form>
we can easly notcie the flow
form -> form_widget (input parameter is the whole form) -> form_widget_compound -> form_rows (iterates form elements into next function) -> form_row -> form_widget (this call form element is passed as parameter)
so here is the time for question, if form_widget is called 4 times (or more), once for form, 3 times for fields then why in listing 5 'form_widget call' text appears only 2 times?
or other words how email and message were rendered?
The form theming in SF2 uses inheritance. That means that if blocks named email_widget and textarea_widget are defined, neither your e-mail nor your textarea fields will use form_widget. They will rather respectively use their own widget blocks: email_widget and textarea_widget.
Well, in form_div_layout.twig, these two widget blocks are defined. Thus form_widget is not called for 2 of your fields. Thus your message is displayed 2 times instead of 4.
If you want to customize the rendering of these fields, you will have to create your own block definitions email_widget and textarea_widget in your custom form theme file.
Edit:
The default form theme files are defined under Symfony\Bridge\Twig\Resources\views\Form. The default file used is form_div_layout.html.twig.
Although the inheritance logic itself is indeed in the PHP code of the TWIG bridge, it does not define which block inherit from which other block.
Inheritance is actually defined in your FormType classes. Each form type class sports a getParent() method that return the name of the form from which it inherits. The name of a form type is the result of the method getName() of its associated form type class. For instance, with a built-in example, Symfony\Component\Form\Extension\Core\Type\TextareaType:
Textarea >> Text >> Field >> Form
All you have to do is to look for the methods getParent() and getName(). Thus, by default, when rendering the textarea row, Twig will search the block textarea_row, text_row, field_row and finally form_row (the base default row). The first of these blocks that is defined in your form theme is rendered.
The definition of the blocks themselves happen in the form theme files.

Customize form field rendering

I would like to customize the rendering of a form field in the edit page from sonata admin bundle to include an applet that uses the text content of a field.
I know that I have to edit the configureFormFields function in the admin class, but I need to know 3 things:
What is the syntax to provide a field form template
Where to put the template file ( which directory )
What the template have to looks like.
Found a solution
What i have done is:
Created a field type, lets call it myfieldType in myCompany\myBundle\Form\Type\myfieldType.php
namespace myCompany\myBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
class myfieldType extends AbstractType
{
public function getParent()
{
return 'text';
}
public function getName()
{
return 'myfield';
}
}
Registered the Type in app/config/services.yml
myCompany.myBundle.form.type.myfield:
class: myCompany\myBundle\Form\Type\myfieldType
tags:
- { name: form.type, alias: myfield }
In my myentityAdmin class,
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->add('myfieldname', 'myfield')
...
}
and
public function getFormTheme() {
return array('myCompanymyBundle:Admin:myfield_edit.html.twig');
}
and the template :
{# src/mycompany/myBundle/Resources/views/Form/myfield_edit.html.twig #}
{% block myfield_widget %}
{% spaceless %}
{{ block('textarea_widget') }}
{% endspaceless %}
{% endblock %}
And now i can access the form field value by the twig variable "value" !
So easy... when you got it.
user1254498's solution won't work unless the block name prefix matches the name of the form type. At least with the last version of sonata admin bundle (2.2.12). In this case:
{# src/mycompany/myBundle/Resources/views/Form/myfield_edit.html.twig #}
{% block myfield_widget %}
{% spaceless %}
{{ block('textarea_widget') }}
{% endspaceless %}
{% endblock %}
And, regarding getFormTheme(), you shoud return also the parent theme, otherwise you may break the whole style...
public function getFormTheme()
{
return array_merge(
parent::getFormTheme(), array(
'mycompanyBundle:Form:myfield_edit.html.twig')
);
}
Also, you can access the admin service in the twig template with the variable sonata_admin.admim.
In your services.yml file you define the template for your edit Action:
app.admin.product:
class: AppBundle\Admin\ProductAdmin
arguments: [~, AppBundle\Entity\Product, AppBundle:Admin\Product]
tags:
- {name: sonata.admin, manager_type: orm, group: Products, label: Products}
calls:
- [ setTemplate, [edit, AppBundle:Product:edit.html.twig]]
In that template you can then override templates for fields in your form:
{% extends 'SonataAdminBundle:CRUD:base_edit.html.twig' %}
{% form_theme form.selectall 'AppBundle:Form:selectall.html.twig' %}
{% form_theme form.Country 'AppBundle:Form:country.html.twig' %}
Then my template looks like that:
{% block form_row %}
<div class="form-group">
{{ form_label(form) }}
{% set c = 0 %}
{% for i in form %}
{% set c = c+1 %}
{% if (c == 1) %}
<div style="float: left; width: 20%;">
{% endif%}
{{ form_row(i) }}
{% if ((c == 60) or (form|length == loop.index)) %}
</div>
{% set c = 0 %}
{% endif%}
{% endfor %}
</div>
{% endblock form_row %}
In this case, my countries check boxes appear in column of 60 elements, not in one column with the whole list of elements.
Hope this is helpful to someone else.

Symfony 2 custom form field type: how to add javascript and css only once?

I want to use javascript in custom Symfony 2 form field type extension. So, I have Twig extension template like this:
{% block some_widget %}
<input ... />
<script src="some.js"></script>
<link href="some.css" />
{% endblock %}
But I want to have these script and link tags only once in my HTML, ideally in head tag, without modifing base template. I tried to extend Twig blocks, but I have no access to action template blocks inside form template. Or maybe something like this:
{# widget tempate #}
{% block some_widget %}
<input ... />
{{ use_javascript('some.js') }}
{{ use_css('some.css') }}
{% endblock %}
{# main action template #}
...
<head>
{{ dump_javascripts() }}
{{ dump_css() }}
</head>
...
How to do this with Symfony 2 Forms + Twig?
P.S. Sorry for my bad english.
I had to write a self contained form widget that requires javascript, I was able to achieve what you are trying to do through the event_dispatcher listening on the kernel.response to append the javascript at the end of the Symfony\Component\HttpFoundation\Response. Here's a snippet of my form type :
<?php
namespace AcmeBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\FormInterface;
class AcmeFileType extends AbstractType{
private $twig;
private $dispatcher;
public function __construct(\Twig_Environment $twig, EventDispatcherInterface $dispatcher){
$this->twig = $twig;
$this->dispatcher = $dispatcher;
}
public function buildView(FormView $view, FormInterface $form, array $options){
$javascriptContent = $this->twig->render('AcmeBundle:Form:AcmeFileType.js.twig', array());
$this->dispatcher->addListener('kernel.response', function($event) use ($javascriptContent) {
$response = $event->getResponse();
$content = $response->getContent();
// finding position of </body> tag to add content before the end of the tag
$pos = strripos($content, '</body>');
$content = substr($content, 0, $pos).$javascriptContent.substr($content, $pos);
$response->setContent($content);
$event->setResponse($response);
});
}
...
When you define your form type in your services.yml it looks like this :
acme.form.acme_file_type:
class: AcmeBundle\Form\AcmeFileType
arguments:
- #twig
- #event_dispatcher
tags:
- { name: form.type, alias: acmefile }
So now, everytime you build a form with acmefile the javascript will be appended to the <body>. This solution does not prevent the javascript from being present multiple time though, but you should easily be able to improve this to suit your needs.
You can also play around with the $response object to modify the headers instead if you wish.
The best way is to provide the separate template with css & scripts loading.
With the comments in readme so only thing developer will must do is to
{% block stylesheets %}
{{ parent() }}
include "#MyBestBundle/Resources/view/styles.html.twig"
{% endblock %}
or try to intercept the form rendering with DI and add the assets. But it more difficult to do , if possible to implement.
My way of doing was by creating a custom twig extension where I add JS to a buffer and during form rendering and later dump it at the of my layout.
Something like this:
<?php
namespace AppBundle\Twig;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
class WysiwygExtension extends AbstractExtension
{
/**
* #var array
*
* A pool of elements IDs for Wysiwyg binding.
*/
private $wysiwygElements = [];
/**
* {#inheritdoc}
*/
public function getFunctions()
{
return array(
new TwigFunction('addWysiwygBinding', [$this, 'addWysiwygBinding']),
new TwigFunction('popWysiwygBindings', [$this, 'popWysiwygBindings']),
);
}
public function addWysiwygBinding(string $id): void
{
$this->wysiwyglements[] = $id;
}
public function popWysiwygBindings(): array
{
$elements = array_unique($this->wysiwygElements);
$this->wysiwygElements = [];
return $elements;
}
}
Then form-fields.html.twig:
...
{% block wysiwyg_widget %}
{% apply spaceless %}
{{ form_widget(form) }}
{% do addWysiwygBinding(id) %}
{% endapply %}
{% endblock %}
...
Then layout.html.twig:
<!DOCTYPE html>
<html>
<head>
...
</head>
<body>
...
{% set ids = popWysiwygBindings() %}
{% if ids is not empty %}
{% javascripts
'bundles/admin/plugins/wysiwyg_1.js'
'bundles/admin/plugins/wysiwyg_2.js'
%}
<script type="text/javascript" src="{{ asset_url }}"></script>
{% endjavascripts %}
{% endif %}
{% for id in ids %}
{{ include('_wysiwyg.html.twig', { id: id }) }}
{% endfor %}
</body>
</html>
This is how I use it. Hope it's what you're looking for.
base.html.twig
<head>
{% block stylesheets %}
css...
{% endblock %}
</head>
foo.html.twig
{% extends '::base.html.twig' %}
{% block stylesheets %}
{{ parent() }}
css that you need in foo.html.twig
{% endblock %}
I found some 'dirty' method used by many peoples in other situations.
We check the loading of script on client side. In case we have a zlkladr.js file, that have a global object 'zlkladr'
{% block our_widget %}
{% spaceless %}
...
<script>
// We must load the script only once, even if many widgets on form
if ( !window.zlkladr ) {
document.write('<script src="{{ asset('bundles/kladr/js/zlkladr.js') }}"></sc'+'ript>');
}
</script>
{% endspaceless %}
{% endblock %}