I'm on symfony and I use collection form to make a site to reserve tickets for a show.
The interface is simple, The user select the number of tickets he wants, then it display as much form prototype as tickets required. This part works well for me.
But I would like to display only 2 field ( name and surname ) not the age field ( It will be asked in another part of my form ) of my billet entity.
In the documentation they explain that you can display one field only (if I understand well ) :
<ul class="billets" data-prototype="{{ form_widget(form.billets.vars.prototype.surname)|e }}">
Or the all entity fields :
<ul class="billets" data-prototype="{{ form_widget(form.billets.vars.prototype)|e }}">
But not 2 fields, because when I try this, it display only the first field :
<ul class="billets" data-prototype="{{ form_widget(form.billets.vars.prototype.name)|e }}">
<ul class="billets" data-prototype="{{ form_widget(form.billets.vars.prototype.surname)|e }}">
Here is my billet type :
class BilletType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('surname', TextType::class)
->add('name', TextType::class)
->add('dateOfBirth', BirthdayType::class)
;
}
}
to avoid rendering fields I do it this way with option : {'render_rest': false}) }
{{ form_widget(edit_form._token) }}// mandatory but hidden
{{ form_end(edit_form, {'render_rest': false}) }} //closing the form
This way only fields specified in twig are rendered.
Try this:
Create custom BilletType like this:
class CustomBilletType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('surname', TextType::class)
->add('name', TextType::class)
;
}
}
Imbricate this CustomFormType in your main form:
class BilletType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('billets', CollectionType::class, array(
'entry_type' => new CustomBilletType,
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false, //use this for set dateOfBirth for all billet in your collection
))
->add('dateOfBirth', BirthdayType::class)
;
}
}
You will have your collection of billet with only dateOfBirth field.
For persist your dateOfBirth for all your billet, see in entity like this:
/**
* Constructor
*/
public function __construct()
{
$this->billets = new ArrayCollection();
}
/**
* Add billets
*
* #param \AppBundle\Entity\Billet $billet
* #return Billet
*/
public function addBillet(Billet $billet)
{
$this->billets[] = $billet;
$billet->setDateOfBirth($this->dateOfBirth); //Set the same date for all your billet
return $this;
}
I hope I understand your problem..
Related
what should i do to change this form field to be a range slider with percentage in laravel voyager
i dont know where to change the code in voyager, or maybe i should make a custom formfield?`<?php
namespace App\FormFields;
use TCG\Voyager\FormFields\AbstractHandler;
class NumberFormField extends AbstractHandler
{
protected $codename = 'number';
public function createContent($row, $dataType, $dataTypeContent, $options)
{
return view('formfields.number', [
'row' => $row,
'options' => $options,
'dataType' => $dataType,
'dataTypeContent' => $dataTypeContent
]);
}
}`
I am trying to make a form that changes based on user response, I have a ChoiceType::class for the first part with 'yes' or 'no' as the options. If the user selects 'yes' I want the second part of the form to show up to get their response to that, but if they select 'no' I just want to keep that second form hidden.
This is the form I have so far
public function buildForm(FormBuilderInterface $builder, array $options)
{
->add('attending', ChoiceType::class, [
'choices' => [
'yes' => true,
'no' => false,
],
'attr' => [
'class' => 'attendanceStatus'
],
'mapped' => false,
'required' => true,
'label' => 'Will you be attending?',
'placeholder' => 'Please make selection',
])
->add('bringingGuest', ChoiceType::class, [
'choices' => [
'yes' => true,
'no' => false,
],
I wrapped the forms in a class and gave each form an ID
<div class="attendance">
<div id="attendance-status">
{{ form_label(form.attending) }}
{{ form_errors(form.attending) }}
{{ form_widget(form.attending) }}
</div>
<div id="guest" style="display: none;">
{{ form_label(form.bringingGuest) }}
{{ form_errors(form.bringingGuest) }}
{{ form_widget(form.bringingGuest) }}
</div>
</div>
I'm not the greatest with javascript but I tried to do an if statement like this
if ('.attending' == true) {
document.getElementById('guest').style.display = 'block';
}
I've been at this for a bit of time now and I can't seem to figure out how to do it properly. I thought it would be something like having an event listener for the user selection and then just using javascript to show the second form if conditions are met.
This is, like you probably already know, a javascript question. Like you said, you can use an event listener, for example to run a function whenever a value changes. Here's an example:
const someId = document.getElementById('some-id');
const example = document.getElementById('example');
someId.addEventListener('change', doSomething);
function doSomething() {
if (someId.value === "yes") {
example.innerHTML = "YES!"
} else {
example.innerHTML = ""
}
}
<select name="name" id="some-id">
<option value="no">no</option>
<option value="yes">yes</option>
</select>
<div id="example">
</div>
All you need to do is check what IDs you should target and what you want to happen on which event. To find out more about events, see: https://developer.mozilla.org/en-US/docs/Web/Events.
You may want to check How to Dynamically Modify Forms Using Form Events page in Symfony docs.
In your case it will be Dynamic Generation for Submitted Forms section.
It involves 2 types of events - Form events on PHP/Symfony side and JS mostly "passive" role to listen for element change and replace the HTML.
The idea is next:
On user action on the HTML element send a request to backend and render the new state for the page based on the element's changed value. Here you get full page HTML as a response.
Then just replace dependent chunks of the page with the new rendered pieces of HTML you get from the response.
Here are main parts (copy-pasted from the docs):
// src/Form/Type/SportMeetupType.php
namespace App\Form\Type;
use App\Entity\Position;
use App\Entity\Sport;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\FormInterface;
// ...
class SportMeetupType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('sport', EntityType::class, [
'class' => Sport::class,
'placeholder' => '',
])
;
$formModifier = function (FormInterface $form, Sport $sport = null) {
$positions = null === $sport ? [] : $sport->getAvailablePositions();
$form->add('position', EntityType::class, [
'class' => Position::class,
'placeholder' => '',
'choices' => $positions,
]);
};
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (FormEvent $event) use ($formModifier) {
// this would be your entity, i.e. SportMeetup
$data = $event->getData();
$formModifier($event->getForm(), $data->getSport());
}
);
$builder->get('sport')->addEventListener(
FormEvents::POST_SUBMIT,
function (FormEvent $event) use ($formModifier) {
// It's important here to fetch $event->getForm()->getData(), as
// $event->getData() will get you the client data (that is, the ID)
$sport = $event->getForm()->getData();
// since we've added the listener to the child, we'll have to pass on
// the parent to the callback functions!
$formModifier($event->getForm()->getParent(), $sport);
}
);
}
// ...
}
in JS:
{# templates/meetup/create.html.twig #}
{{ form_start(form) }}
{{ form_row(form.sport) }} {# <select id="meetup_sport" ... #}
{{ form_row(form.position) }} {# <select id="meetup_position" ... #}
{# ... #}
{{ form_end(form) }}
<script>
var $sport = $('#meetup_sport');
// When sport gets selected ...
$sport.change(function() {
// ... retrieve the corresponding form.
var $form = $(this).closest('form');
// Simulate form data, but only include the selected sport value.
var data = {};
data[$sport.attr('name')] = $sport.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 position field ...
$('#meetup_position').replaceWith(
// ... with the returned one from the AJAX response.
$(html).find('#meetup_position')
);
// Position field now displays the appropriate positions.
}
});
});
</script>
I'm trying to add a new field to the shipping address in Magento 2.3.4.
I would like to add it "Magento way" that's why I used these tutorials:
https://devdocs.magento.com/guides/v2.3/howdoi/checkout/checkout_new_field.html
https://www.edmondscommerce.co.uk/handbook/Platforms/Magento-2/Guides/Custom-Shipping-Address-Field/
The field appears correctly on the frontend, but after adding new address and filling that field and click "Ship here":
I got that error:
Here is my code:
1) At first, I thought that it is not necessary (this step is not in Magento 2 devdocs but appears in second tutorial) - app/code/Company/Module/Setup/Patch/Data/AddVipCodeAttribute.php:
<?php
namespace Company\Module\Setup\Patch\Data;
use Magento\Catalog\Model\Product;
use Magento\Customer\Setup\CustomerSetupFactory;
use Magento\Eav\Setup\EavSetup;
use Magento\Eav\Setup\EavSetupFactory;
use Magento\Framework\DB\Ddl\Table;
use Magento\Framework\Setup\ModuleDataSetupInterface;
use Magento\Framework\Setup\Patch\DataPatchInterface;
use Magento\Framework\Setup\Patch\PatchRevertableInterface;
class AddVipCodeAttribute implements DataPatchInterface, PatchRevertableInterface
{
/**
* #var ModuleDataSetupInterface
*/
private $moduleDataSetup;
/**
* #var EavSetupFactory
*/
private $eavSetupFactory;
/**
* #var CustomerSetupFactory
*/
private $customerSetupFactory;
/**
* Constructor
*
* #param ModuleDataSetupInterface $moduleDataSetup
* #param EavSetupFactory $eavSetupFactory
*/
public function __construct(
ModuleDataSetupInterface $moduleDataSetup,
EavSetupFactory $eavSetupFactory,
CustomerSetupFactory $customerSetupFactory
) {
$this->moduleDataSetup = $moduleDataSetup;
$this->eavSetupFactory = $eavSetupFactory;
$this->customerSetupFactory = $customerSetupFactory;
}
/**
* #inheritdoc
*/
public function apply()
{
$this->moduleDataSetup->getConnection()->startSetup();
/** #var CustomerSetup $customerSetup */
$customerSetup = $this->customerSetupFactory->create(['setup' => $this->moduleDataSetup]);
$customerSetup->addAttribute('customer_address', 'vip_code', [
'label' => 'Vip account code',
'input' => 'text',
'type' => Table::TYPE_TEXT,
'source' => '',
'required' => false,
'position' => 333,
'visible' => true,
'system' => false,
'is_used_in_grid' => false,
'is_visible_in_grid' => false,
'is_filterable_in_grid' => false,
'is_searchable_in_grid' => false,
'backend' => ''
]);
$attribute = $customerSetup->getEavConfig()->getAttribute('customer_address', 'vip_code')
->addData(['used_in_forms' => [
'adminhtml_customer_address',
'adminhtml_customer',
'customer_address_edit',
'customer_register_address',
'customer_address',
]]);
$attribute->save();
$this->moduleDataSetup->getConnection()->endSetup();
}
/**
* #inheritDoc
*/
public function revert()
{
}
/**
* #inheritdoc
*/
public function getAliases()
{
return [];
}
/**
* #inheritdoc
*/
public static function getDependencies()
{
return [];
}
}
2)I created a plugin class:
etc/frontend/di.xml
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<type name="Magento\Checkout\Block\Checkout\LayoutProcessor">
<plugin name="add_custom_field_checkout_form" type="Company\Module\Plugin\Checkout\LayoutProcessor" sortOrder="100"/>
</type>
</config>
And layout processor app/code/Company/Module/Plugin/Checkout/LayoutProcessor.php
<?php
namespace Company\Module\Plugin\Checkout;
use Magento\Checkout\Block\Checkout\LayoutProcessor as LayoutProcessorCore;
use Magento\Customer\Model\Session;
class LayoutProcessor
{
/**
* #var Session
*/
protected $session;
public function __construct(
Session $session
) {
$this->session = $session;
}
/**
* #param LayoutProcessorCore $subject
* #param array $jsLayout
*
* #return array
*/
public function afterProcess(
LayoutProcessorCore $subject,
array $jsLayout
) {
$customAttributeCode = 'vip_code';
$customField = [
'component' => 'Magento_Ui/js/form/element/abstract',
'config' => [
// customScope is used to group elements within a single form (e.g. they can be validated separately)
'customScope' => 'shippingAddress.custom_attributes',
'customEntry' => null,
'template' => 'ui/form/field',
'elementTmpl' => 'ui/form/element/input',
'tooltip' => [
'description' => 'Vip accounts code. Example: 123123123ASD',
],
],
'dataScope' => 'shippingAddress.custom_attributes' . '.' . $customAttributeCode,
'label' => 'Vip code',
'provider' => 'checkoutProvider',
'sortOrder' => 0,
'validation' => [
'required-entry' => false
],
'options' => [],
'filterBy' => null,
'customEntry' => null,
'visible' => true,
];
$jsLayout['components']['checkout']['children']['steps']['children']['shipping-step']['children']
['shippingAddress']['children']['shipping-address-fieldset']['children'][$customAttributeCode] = $customField;
return $jsLayout;
}
}
3)JS files:
app/code/Company/Module/view/frontend/requirejs-config.js
var config = {
config: {
mixins: {
'Magento_Checkout/js/action/set-shipping-information': {
'Company_Module/js/add-new-field': true
}
}
}
};
app/code/Company/Module/view/frontend/web/js/add-new-field.js
/*jshint browser:true jquery:true*/
/*global alert*/
define([
'jquery',
'mage/utils/wrapper',
'Magento_Checkout/js/model/quote'
], function ($, wrapper, quote) {
'use strict';
return function (setShippingInformationAction) {
return wrapper.wrap(setShippingInformationAction, function (originalAction) {
var shippingAddress = quote.shippingAddress();
if (shippingAddress['extension_attributes'] === undefined) {
shippingAddress['extension_attributes'] = {};
}
shippingAddress['extension_attributes']['vip_code'] = shippingAddress.customAttributes['vip_code'];
console.log(shippingAddress);
// pass execution to original action ('Magento_Checkout/js/action/set-shipping-information')
return originalAction();
});
};
});
[UPDATE]
app/code/BartCompany/VipAccounts/etc/extension_attributes.xml:
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Api/etc/extension_attributes.xsd">
<extension_attributes for="Magento\Quote\Api\Data\AddressInterface">
<attribute code="vip_code" type="string" />
</extension_attributes>
</config>
Can anybody help me with that, I'm working on it almost a week, and tried a lot of solutions, but it is for me very important to do that by custom_attributes field.
I will be grateful, thank You.
I think i found the issue, here is a follow:
https://github.com/magento/magento2/issues/26740
I using symfony and have template with array with some entities array, and need create in for in check box for all entity and when checked some entities and click ready go to action with ids(from all check box) - example - taskExecution.id
I don't used symfony form with type entity because taskExecutions complicated DTO, from this DTO i need only id for to send on another action
$taskExecutions = $this->getTaskExecution()
->getTaskExecutionByFilter($form->getData());
return [
'form' => $form->createView(),
'taskExecutions' => $taskExecutions
];
{% for taskExecution in taskExecutions %}
<input class="searchType" type="checkbox" name="SharingNotification" id={{ taskExecution.id }}>
<label class="searchtype2label">{{ taskExecution.id }}</label>
</input>
{% endfor %}
{% javascripts
'#EconomyBundle/Resources/public/js/check-task-executions.js'
filter='?yui_js' combine=true %}
<script src="{{ asset_url }}"></script>
{% endjavascripts %}
add js
$('.searchType').click(function() {
alert($(this).attr('id'));
if(this.checked){
$.ajax({
type: "POST",
url: '/manage/outbound_invoices/task_executions/ids',
data: $(this).attr('id'), //--> send id of checked checkbox on other page
success: function(data) {
alert('it worked');
alert(data);
$('#container').html(data);
},
error: function() {
alert('it broke');
},
complete: function() {
alert('it completed');
}
});
}
});
this my action
/**
* give task executions ids for created row.
*
* #Route("/manage/outbound_invoices/task_executions/ids", name="ids_task_executions_")
* #Method({"POST", "GET"})
*/
public function getIdsTaskExecutionsAction(Request $request)
{
$ids = $request->get('ids');
}
I don't know js, help please for understand how get check box value (1 or 0) and entity id parameter and send to another action
I don't think you need javascript for that. Instead you should have a look to the Symfony doc on how to use a form without data_class
your form will looks like :
<?php
class TaskExecutionType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('taskExecution', EntityType::class, array(
'class' => 'AppBundle/TaskExecution',
'expanded' => true,
'multiple' => true
))
->add('submit', SubmitType::class)
;
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'csrf_protection' => false
));
}
/**
* #return string
*/
public function getName()
{
return 'execution_task_type';
}
}
And in your controller:
<?php
/**
* give task executions ids for created row.
*
* #Route("/manage/outbound_invoices/task_executions/ids", name="ids_task_executions_")
* #Method({"POST", "GET"})
*/
public function getIdsTaskExecutionsAction(Request $request)
{
$form = $this->createForm(TaskExecutionType::class, null, array(
'method' => 'POST',
'action' => 'ids_task_executions'
));
$form->handleRequest($request);
if ($form->isValid()) {
$data = $form->getData(); //this will be an array of all the TaskExecution entities you selected
//your own logic
}
return $this->render('template.html.twig', array(
'form' => $form->createView()
));
}
I have two entities: Issue and Notes, and an issue can have multiple notes. I defined them like this:
class Issue {
// ...
/**
* #ORM\OneToMany(targetEntity="Note", mappedBy="issue")
*/
protected $notes;
public function getNotes() {
return $this->notes->toArray();
}
public function addNote($note) {
if (!$this->notes->contains($note)) $this->notes->add($note);
}
public function removeNote($note) {
if ($this->notes->contains($note)) $this->notes->removeElement($note);
}
}
class Note {
// ...
/**
* #ORM\Column(type="string", nullable=false)
*/
protected $description;
/**
* #ORM\ManyToOne(targetEntity="Issue", inversedBy="notes")
* #ORM\JoinColumn(name="issue", referencedColumnName="id", nullable=false)
*/
protected $issue;
public function getDescription() // ...
public function setDescription() // ...
public function getIssue() // ...
public function setIssue() // ...
}
I defined an IssueType to create a form that embeds a NoteType form:
class IssueType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
// ...
->add('notes', 'collection', ['type' => new NoteType()]);
}
}
class NoteType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder->add('description', 'textarea');
}
}
This works well when I want to create a new issue because the array of notes only contains one (blank) note created by the IssueController, which the user can fill in using the form. However, once the issue is created, I've got a view issue page that shows all the notes, and at the bottom there is a form to add a new note. The problem is that the collection form creates an input for a new (blank) note as well as for all the previous notes (see image below).
Is there any way I can only include the input for the new (blank) note form using Symfony, or do I need to remove the previous notes with JavaScript?
EDIT:
Here's the code for the IssueController:
public function newAction(Request $request) {
$em = $this->getDoctrine()->getManager();
$issue = new Issue();
$note = new Note();
$issue->addNote($note);
$note->setIssue($issue);
$form = $this->createForm('issue', $issue);
$form->handleRequest($request);
// ...
if ($form->isValid()) {
$em->persist($issue);
$em->persist($note);
$em->flush();
return $this->redirectToRoute(...);
}
return $this->render('/issue/new.html.twig', [
'issue' => $issue,
'form' => $form->createView()
]);
}
The note edit boxes appear because your form is instructing to create an editable collection for the one-to-many relationship. Putting something in a form type means it's typically going to be editable, or at the very least presented as form.
If you want your form to only be able to ADD a new note, you must remove that collection property of the form.
Instead of
->add('notes', 'collection', ['type' => new NoteType()]);
have
->add('newNote', 'textarea', ['label' => 'Add note', 'mapped' => false];
Your controller will need amendments too.
public function newAction(Request $request) {
$em = $this->getDoctrine()->getManager();
$issue = new Issue();
$form = $this->createForm('issue', $issue);
$form->handleRequest($request);
if ($form->isValid()) {
if ($newNote = trim($form['newNote']->getData())) {
$note = new Note();
$issue->addNote($note);
$note->setIssue($issue);
$note->setDescription($newNote);
$em->persist($note); // Probably not needed as you're cascading persist
}
$em->persist($issue);
$em->flush();
return $this->redirectToRoute(...);
}
return $this->render('/issue/new.html.twig', [
'issue' => $issue,
'form' => $form->createView()
]);
}
This will only show an input for a new note. To show existing notes you'll need to do this in your view, for example:
<h2>Notes</h2>
{% for note in issue.notes if note.description %}
<p>{{ loop.index }}: {{ note.description }}</p>
{% else %}
<p>No notes have been added for this issue.</p>
{% endfor %}