symfony twig form theming - php

I have a Symfony Form for a file upload looking like
$builder
->add('imageFile', FileType::class, [
'mapped' => false,
'required' => false,
'label' =>'user.edit.select_image',
'constraints' => $coverConstraints
])
->getForm();
because of a bug in bootstrap 4 i have to theme it so it can show Select a file to upload in the box
{% set tr = 'user.edit.select_image|trans' %}
<div class="card-body">
{{ form_start(CoverForm) }}
{{ form_row(CoverForm.imageFile, {
attr: {
'placeholder': tr
}
}) }}
{{ form_errors(CoverForm) }}
{{ form_widget(CoverForm) }}
<button type="submit" class="btn btn-dark">{{ 'user.edit.form_submit'|trans }}</button>
{{ form_end(CoverForm) }}
</div>
<script>
$('.custom-file-input').on('change', function(event) {
var inputFile = event.currentTarget;
$(inputFile).parent()
.find('.custom-file-label')
.html(inputFile.files[0].name);
});
</script>
My problem is that that variable tr doesn't executed
Do you have any ideea how to aproach this ?

{% set tr = 'user.edit.select_image'|trans %}
As trans is a filter.

Related

form display image depending on choice type

im learning symfony. I'm trying to display an image when i select a choice from dropdown menu in the form.
this is my form
$collection = array(
'jewel1' => 'jewel1.jpg',
'jewel2' => 'jewel2.jpg',
.
.
.
)
$builder
->add('product_group_id', HiddenType::class,['data'=> $productId])
->add('jewel_name', TextType::class)
->add('jewel_rotation', IntegerType::class)
->add('price', IntegerType::class)
->add('image_name', ChoiceType::class, [
'label' => 'Jewel Image',
'required' => false,
'choices' => $collection
])
->add('save', SubmitType::class, ['label' => 'Save']);
and this is my twig (i don't know what to do yet)
{% block javascript %}
<script>
$(function() {
$('form.image_name.vars.value').change(
var tt = $(this).val();
alert(tt);
)
})
</script>
{% endblock %}
{% block main %}
{{form_start(form)}}
{{form_widget(form)}}
<div class="jewels">
{% set path = 'customRing/jewel/' ~ form.image_name.vars.value ~%}
{{form.image_name.vars.value}}
<img src="{{ asset(path, 'save_image') }}" width="200" height="200"/>
</div>
{{form_end(form)}}
{% endblock %}
so when i select one choice in the dropdown menu, the preview image will be display so i can press the submit button with the right image.
however, I have no idea how i can get a changed value from choicetype before sumbit.
You can get the changed value of the select via javascript and display it in the img tag
<script>
document.getElementById('YOUR_IMAGE_SELECT').addEventListener('change', function() {
document.getElementById('YOUR_IMG').src = "customRing/jewel/" + this.value;
});
</script>

Get the file object in the form's controller, from a javascript array collection prototype in Symfony 5

Here is what i'm trying to do :
I have a classic Folder entity which contains Sku entities in a OneToMany relation as an ArrayCollection. So far so good.
I want the FolderType to dynamically create as many Skus as I want in it. For that, I followed the Symfony fast track method to generate prototypes in javascript. Works great.
Now, in this folder form, "skus" are an array collection of entities. In the SkuType, I have files to uploads. Here are the forms (Folder and Sku) :
$builder
->add('norlogReference', TextType::class, [
'label' => 'Référence du dossier'
])
->add('skus', CollectionType::class, [
'entry_type' => SkuType::class,
'entry_options' => ['label' => false],
'allow_add' => true,
'allow_delete' => true,
'prototype' => 'skus'
]);
As for the SkuType, here are my upload fields :
->add('picture_1', FileType::class, [
'label' => 'Image 1 du produit',
'mapped' => false,
'required' => false,
'attr' => ['accept' => 'image/*']
])
->add('picture_2', FileType::class, [
'label' => 'Image 2 du produit',
'mapped' => false,
'required' => false,
'attr' => ['accept' => 'image/*']
])
Here is the controller recieving the form :
/**
* #Route("/new", name="folder_new")
*/
public function new(Request $request): Response
{
$entityManager = $this->getDoctrine()->getManager();
$norlogFolder = new NorlogFolder();
$form = $this->createForm(NorlogFolderType::class, $norlogFolder);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
foreach ($norlogFolder->getSkus() as $sku) {
I want to get both file objects here, pass them in handleUploadedFile(), and use $sku->setPicture1() and setPicture2 with the $newfileName returned by the private method.
$sku->setFolder($norlogFolder);
$sku->setSKU($norlogFolder->getNorlogReference() . '-' . $sku->getSKU());
}
$entityManager->persist($norlogFolder);
$entityManager->flush();
return $this->redirectToRoute('folder_edit', ['id' => $norlogFolder->getId()]);
}
return $this->render('folder/new.html.twig', [
'form' => $form->createView(),
]);
}
and the little private method I want to use in order to handle the file :
private function handleUploadedFile(UploadedFile $uploadedFile): string
{
$destination = $this->getParameter('kernel.project_dir').'/public/uploads/sku_medias';
$originalFilename = pathinfo($uploadedFile->getClientOriginalName(), PATHINFO_FILENAME);
$newFilename = $originalFilename . '-' . uniqid() . '.' . $uploadedFile->guessExtension();
$uploadedFile->move(
$destination,
$newFilename
);
return $newFilename;
}
And the folder form view :
{{ form_start(form, {
attr: {
class: 'sku-form'
}
}) }}
<div class="form-group">
{{ form_row(form.norlogReference, {
attr: {
placeholder: 'ex: XOTP-25',
class: 'sku-form-input'
}
}) }}
</div>
<div class="form-group sku-row skus" data-prototype="{{ form_widget(form.skus.vars.prototype)|e('html_attr') }}">
{% if form.skus|length > 0 %}
{% for sku in form.skus %}
<div class="row sku-bloc bg-light">
<div class="field-title">
<p class="display-3 text-center">Produit #{{ loop.index }}</p>
<a href="{{ path('sku_delete', {id: sku.vars.data.id}) }}"
class="btn bg-danger btn-sku-remove"><i class="fas fa-trash-alt"></i></a>
</div>
<span class="btn btn-secondary form-extend-btn">Voir</span>
<div class="col-12 sku-fields-bloc">
<div class="form-control">
{{ form_row(sku.SKU, {
attr: {
placeholder: 'ex: XVF-25663',
class: 'sku-form-input'
}
}) }}
</div>
<div class="container">
<div class="row">
{{ form_label(sku.marque) }}
{{ form_widget(sku.marque, {
attr: {
class: 'sku-form-input'
}
}) }}
{{ form_label(sku.taille) }}
{{ form_widget(sku.taille, {
attr: {
class: 'sku-form-input'
}
}) }}
{{ form_label(sku.designation) }}
{{ form_widget(sku.designation, {
attr: {
class: 'sku-form-input'
}
}) }}
{{ form_label(sku.couleur) }}
{{ form_widget(sku.couleur, {
attr: {
class: 'sku-form-input'
}
}) }}
{{ form_label(sku.etat) }}
{{ form_widget(sku.etat, {
attr: {
class: 'sku-form-input'
}
}) }}
{{ form_label(sku.composition) }}
{{ form_widget(sku.composition, {
attr: {
class: 'sku-form-input'
}
}) }}
</div>
</div>
<div class="form-control mt-3">
{{ form_row(sku.picture_1, {
attr: {
placeholder: 'Image 1',
class: 'sku-form-input'
}
}) }}
</div>
<div class="form-control mt-3">
{{ form_row(sku.picture_2, {
attr: {
placeholder: 'Image 2',
class: 'sku-form-input'
}
}) }}
</div>
</div>
</div>
{% endfor %}
{% else %}
<div class="row">
<div class="col-12 mt-4">
<p class="p-3 bg-info">Pas encore de {{ form_label(form.skus) }} ajoutés</p>
{{ form_widget(form.skus) }}
</div>
</div>
{% endif %}
</div>
<div class="row">
<div class="col-4">
<button type="button" class="btn btn-primary btn-nav w-100 mt-2 mb-2 add_item_link"
data-collection-holder-class="skus">
Ajouter Sku
</button>
</div>
<div class="col-4">
<button type="submit"
class="btn btn-success btn-nav w-100 mt-2 mb-2">{{ button_label|default('Enregistrer') }}</button>
</div>
<div class="col-4">
<a href="{{ path('folder_list') }}"
class="btn btn-primary btn-nav w-100 mt-2 mb-2">Retour liste</a>
</div>
</div>
{{ form_end(form) }}
So the question is : how to get these picture_1 and 2 objects (and not path which I already have by remapping them to true), by Sku, in the controller side, in order to handle the files as usual ?
I tried :
$request->files->get() (result = null, and I have the enctype right in the form)
$sku->getPicture_1() (result = temporary linux path, but is there a simple way to retrieve the file from that, and on every OS ?)
Trying to access the $form from inside the sku loop (result = nada)
$request->get('picture_1') and that + ->getData()
Various weird tries I forgot which gave me nothing
I might miss something obvious here, but I can't think properly on this project anymore.
Please don't mind the necessary refactoring for now =) Ty !
With 'mapped' => false, you need to handle the collection of uploads like this:
if ($form->isSubmitted() && $form->isValid()) {
foreach ($form->get('skus') as $formChild)
{
// Get the unmapped picture fields
$uploadedPicture1 = $formChild->get('picture_1')->getData();
$uploadedPicture2 = $formChild->get('picture_2')->getData();
// Get the sku
$sku = $formChild->getData();
// Upload the pictures
$picture1Filename = $this->handleUploadedFile($uploadedPicture1);
$picture2Filename = $this->handleUploadedFile($uploadedPicture2);
// Set the new filenames onto the sku
$sku->setPicture1($picture1Filename);
$sku->setPicture2($picture2Filename);
// Your original code
$sku->setFolder($norlogFolder);
$sku->setSKU($norlogFolder->getNorlogReference() . '-' . $sku->getSKU());
}
}
$entityManager->persist($norlogFolder);
$entityManager->flush();
return $this->redirectToRoute('folder_edit', ['id' => $norlogFolder->getId()]);
}
Edit: do check if a picture was actually uploaded, as in the other answer. :-)
Your picuter_1 and picture_2 are mapped: false.
That means, you have to use ->getData() (or maybe ->getNormData()) on each SkuType or your NorlogFolderType
instead of looping through SKUs entities collection, loop through sku formtypes and get the data directly from 'picture_1' and 'picture_2' field
if ($form->isSubmitted() && $form->isValid())
{
if ($form->has('skus'))
{
foreach ($form->get('skus') as $skuForm)
{
// always check if field named 'picture_1' is there
if ($skuForm->has('picture_1'))
{
/** #var \Symfony\Component\HttpFoundation\File\UploadedFile $firstPic */
$firstPic = $skuForm->get('picture_1')->getData();
//todo: maybe check if null
$picOne = $this->handleUploadedFile($firstPic);
}
// do the same with picture_2 (and others if any)
}
}
}

Display custom message for checkbox (terms of service/privacy)

I am still new to Symfony and Php, so I am using the CheckboxType code from Symfony docs.
Currently no message is appearing if a user attempts to register and the checkbox is unchecked, but it will still prevent the user from making a account.
(1) I would like a error message to appear next to the checkbox in red stating the box must be checked in order to proceed. I would also like to customize this message.
Thank you!
Register.html
{% extends 'base.html.twig' %}
{% block body %}
<div class="container">
<div class="row">
<div class="col-xs-12">
<h1>Register!</h1>
{{ form_start(form) }}
{{ form_row(form.username) }}
{{ form_row(form.email) }}
{{ form_row(form.plainPassword.first, {
'label': 'Password'
}) }}
{{ form_row(form.plainPassword.second, {
'label': 'Repeat Password'
}) }}
Terms of service
{{ form_widget(form.termsAccepted) }}
cancel
<button type="submit" class="btn btn-primary" formnovalidate>
Register
</button>
<br></br>
<p>Privacy Policy
{{ form_end(form) }}
</div>
</div>
</div>
{% endblock %}
RegistrationForm.php
class UserRegistrationForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('email', EmailType::class)
->add('username', TextType::class)
->add('plainPassword', RepeatedType::class, ['type' => PasswordType::class])
->add('termsAccepted', CheckboxType::class, array(
'mapped' => false,
'constraints' => new IsTrue(),));
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => User::class,
'validation_groups' => ['Default', 'Registration']
]);
}
}
The problem is that you are using:
{{ form_widget(form.termsAccepted) }}
for rendering the checkbox. This will only render the widget, whereas:
{{ form_row(form.termsAccepted) }}
as is used by all the user fields would contain the label, the widget and the error message. If you want to keep the widget, e.g. because using form_row messes up the template somehow you could render the errors individually using:
{{ form_errors(form.termsAccepted) }}
You might also want to check out the documentation on Form Customization.

Easy admin bundle modal dialog for custom action?

I'm working with Easy Admin Bundle for Symfony2. How can I use my own modal dialog with text input for the custom action in the list?
Let's say custom action is called rename and after hitting button Rename in the list I wanna in modal dialog write a new name of something. Then after hitting button OK I wanna call ranameAction in the controller which will do everything needed, but parameter called name must be sent there.
I have the solution but it is not good, there must be some better way.
I created _simple_form.html.twig:
{{
form(rename_form, {
action: rename_form.vars.action ~ '&referer=' ~ referer,
method: 'POST',
attr: { id: 'rename-form', style: 'display: none' }
})
}}
<div id="modal-rename" class="modal fade">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-body">
<h4>{{ 'rename_modal.title'|trans(_trans_parameters, 'EasyAdminBundle') }}</h4>
<p>{{ 'rename_modal.content'|trans(_trans_parameters, 'EasyAdminBundle') }}</p>
<br />
<input class="form-control" type="text" />
</div>
<div class="modal-footer">
<button type="button" data-dismiss="modal" class="btn">
{{ 'action.cancel'|trans(_trans_parameters, _translation_domain) }}
</button>
{% if easyadmin_action_is_enabled(view, 'rename', _entity_config.name) %}
{% set _rename_action = easyadmin_get_action(view, 'rename', _entity_config.name) %}
<button type="button" data-dismiss="modal" class="btn btn-danger" id="modal-rename-button" formtarget="{{ _rename_action.target }}">
{% if _rename_action.icon %}<i class="fa fa-{{ _rename_action.icon }}"></i>{% endif %}
{{ 'rename_modal.action'|trans(_trans_parameters, 'EasyAdminBundle') }}
</button>
{% endif %}
</div>
</div>
</div>
</div>
In config.yml is a new option:
list:
actions:
rename: { name: rename, type: method, label: 'Rename', css_class: 'btn btn-primary', icon: pencil }
In list.html.twig which I'm using:
{% block main %}
{% block rename_form %}
{% set referer = paginator.currentPage == paginator.nbPages and 1 != paginator.currentPage and 1 == paginator.currentPageResults.count
? path('easyadmin', app.request.query|merge({ page: app.request.query.get('page') - 1 }))
: app.request.requestUri
%}
{{ include('#EasyAdmin/default/includes/_simple_form.html.twig', {
view: 'list',
referer: referer|url_encode,
rename_form: rename_form_template,
_translation_domain: _entity_config.translation_domain,
_trans_parameters: _trans_parameters,
_entity_config: _entity_config,
}, with_context = false) }}
{% endblock rename_form %}
{% endblock %}
{% block body_javascript %}
{{ parent() }}
<script type="text/javascript">
$(function() {
$('#modal-rename-button').on('click', function(e) {
e.preventDefault();
var name = $('#name').val();
var renameForm = $('#rename-form');
renameForm.attr('action', renameForm.attr('action').replace('__name__', name));
});
$('a.action-rename').on('click', function(e) {
e.preventDefault();
var id = $(this).parents('tr').first().data('id');
$('#modal-rename').modal({ backdrop: true, keyboard: true })
.off('click', '#modal-rename-button')
.on('click', '#modal-rename-button', function () {
var renameForm = $('#rename-form');
renameForm.attr('action', renameForm.attr('action').replace('__id__', id));
renameForm.trigger('submit');
});
});
});
</script>
{% endblock %}
And finally controller:
protected function createRenameForm($entityName, $entityId, $name)
{
$formBuilder = $this->get('form.factory')->createNamedBuilder('rename_form')
->setAction($this->generateUrl('easyadmin', array('action' => 'rename', 'entity' => $entityName, 'id' => $entityId, 'name' => $name)))
->setMethod('POST')
;
$formBuilder->add('submit', LegacyFormHelper::getType('submit'), array('label' => 'rename_modal.action', 'translation_domain' => 'EasyAdminBundle'));
$formBuilder->add('_easyadmin_rename_flag', LegacyFormHelper::getType('hidden'), array('data' => '1'));
return $formBuilder->getForm();
}
public function listAction() {
$this->dispatch(EasyAdminEvents::PRE_LIST);
$fields = $this->entity['list']['fields'];
$paginator = $this->findAll($this->entity['class'], $this->request->query->get('page', 1), $this->config['list']['max_results'], $this->request->query->get('sortField'), $this->request->query->get('sortDirection'), $this->entity['list']['dql_filter']);
$this->dispatch(EasyAdminEvents::POST_LIST, array('paginator' => $paginator));
return $this->render($this->entity['templates']['list'], array(
'title' => 'Branches',
'entity' => $this->entity['name'],
'currentBranch' => $this->repo->getCurrentBranchName(),
'paginator' => $paginator,
'fields' => $fields,
'delete_form_template' => $this->createDeleteForm($this->entity['name'], '__id__')->createView(),
'rename_form_template' => $this->createRenameForm($this->entity['name'], '__id__', '__name__')->createView(),
));
}
I had a similar issue because I wanted to show a modal before to perform a custom action. So what I did was modify the button action with jquery
$(document).ready(function () {
var $archiveButton = $('a.action-archive');
$archiveButton.attr('href', '#');
$archiveButton.attr('data-toggle', 'modal');
$archiveButton.attr('data-target', '#archive-modal');
$archiveButton.attr('data-url', '/url/to/load/form/inside/modal/body');
});
I don't know if it is the better way, but it helped me to show some info to the user before execute the archiveAction() in the AdminController
EDITED: a way to access POST variables
You can access to POST variables through the Container
public function renameAction()
{
$name = $this->container->get('request')->get('your variable');
var_dump($name);
die();
return $this->redirectToRoute('admin', ['entity' => 'Branch', 'action' => 'list']);
}
Play a while with the $this->container->get('request') to find what you are looking for

Symfony2 collection of forms issue

I'm trying to create a collection of forms which consists of 3 global parts and and fourth part containing 3 fields that are allowed to be replicated. Both form names are "search". I've no idea what am I doing wrong but in prototype instead of having the whole form created I get the following:
<input type="search" id="search_statistics_collection___name__" name="search[statistics_collection][__name__]" required="required" class="form-control" ><a href='#' class='add-statistic btn'>Dodaj statystykę</a>
View:
{{ form_start(searchForm) }}
{{ form_row(searchForm.league) }}
{{ form_row(searchForm.range) }}
{{ form_row(searchForm.season) }}
<div id="single-proto" data-prototype="{{ form_widget(searchForm.statistics_collection.vars.prototype)|e }}<a href='#' class='add-statistic btn'>Dodaj statystykę</a>"></div>
{% for single in searchForm.statistics_collection %}
<div class="single-statistic">
{{ form_label(single.statistic) }}
{{ form_widget(single.statistic) }}
{{ form_widget(single.sign) }}
{{ form_widget(single.value) }}
Dodaj statystykę
</div>
{% endfor %}
{{ form_end(searchForm) }}
Form class: (relevant parts)
SearchType.php:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('league', 'choice', array(
'label'=>'Wybierz ligi',
'choices'=>array(
'all'=>'Wszystkie ligi',
'favourite'=>'Moje ulubione ligi',
'bookie'=>'Ligi mojego bukmachera'
)
))
->add('range', 'choice', array(
'label'=>'Wybierz zakres',
'choices'=>array(
'all'=>'Cały mecz - wszystkie mecze',
'home'=>'Cały mecz - dom',
'guest'=>'Cały mecz - wyjazd',
'half_all'=>'Do przerwy - wszystkie mecze',
'half_home'=>'Do przerwy - dom',
'half_guest'=>'Do przerwy - wyjazd',
)
))
->add('season', 'choice', array(
'label'=>'Wybierz sezon',
'choices'=>Statistics::$seasons
))
->add('statistics_collection', 'collection', array(
'label'=>'Wybierz statystykę',
'type' => new SearchSubType(),
'allow_add' => true,
'allow_delete' => true,
'prototype' => true,
))
->setData(array(
'statistics_collection' => array(
array('', '', ''),
)
))
;
}
SearchSubType.php
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('statistic', 'choice', array(
'label'=>'Wybierz statystykę',
'required'=>false,
'choices'=>Statistics::$statistics
))
->add('sign', 'choice', array(
'label'=>'',
'required'=>false,
'choices'=>array(
'gt'=>'>',
'lt'=>'<',
'eq'=>'=',
'gteq'=>'>=',
'lteq'=>'<='
)
))
->add('value', 'text', array(
'label'=>'',
'required'=>false,
))
;
}
Please help :)
Solution
The subform included within collection has to have other name than parent one. Otherwise it all mess up :)
You should do like:
{{ form_start(searchForm) }}
{{ form_row(searchForm.league) }}
{{ form_row(searchForm.range) }}
{{ form_row(searchForm.season) }}
<ul class="statistics_collection" data-prototype="{{ form_widget(searchForm.statistics_collection.vars.prototype)|e }}"></ul>
{{ form_end(searchForm) }}
and also do't forget to imply jquery in your twig file like this
{% block javascripts %}
{{ parent() }}
<script type="text/javascript">
var collectionHolder = $('ul.statistics_collection');
// setup an "add a tag" link
var $addValueLink = $('Upload Pictures');
var $newLinkLi = $('<li></li>').append($addValueLink);
jQuery(document).ready(function() {
// add the "add a statistics_collection" anchor and li to the tags ul
collectionHolder.append($newLinkLi);
$addValueLink.on('click', function(e) {
addValueForm(collectionHolder, $newLinkLi);
});
});
function addValueForm(collectionHolder, $newLinkLi) {
// Get the data-prototype we explained earlier
var prototype = collectionHolder.attr('data-prototype');
// Replace '__name__' in the prototype's HTML to
// instead be a number based on the current collection's length.
var newForm = prototype.replace(/__name__/g, collectionHolder.children().length);
// Display the form in the page in an li, before the "Add a tag" link li
var $newFormLi = $('<li></li>').append(newForm);
$newLinkLi.before($newFormLi);
}
</script>
{% endblock %}
For more details you may follow How to Embed a Collection of Forms .

Categories