Symfony2 multiple forms in different JQuery UI tabs but single page - php

I'm facing a problem that I can summarize as it follows:
I have a TWIG template page like this (reg.html.twig):
{% extends "::base.html.twig" %}
{% block body %}
<ul class="tabs">
<li class="left">tab1</li>
<li class="left">tab2</li>
<li class="left">tab3</li>
<li class="right">tab4</li>
</ul>
<div class="tabs_container">
<div id="tab1" class="blocco-tab">
<form action="{{ path('AAA') }}" method="post" {{ form_enctype(form) }}>
<div id="name_field">
{{ form_row(form.name) }}
</div><!-- /name_field -->
<div id="address">
{{ form_row(form.addresses[0].road) }}
</div><!-- /address_field -->
</form>
</div>
<div id="tab2" class="blocco-tab">
<form action="{{ path('BBB') }}" method="post" {{ form_enctype(form) }}>
<div id="surname_field">
{{ form_row(form.surname) }}
</div><!-- /surname_field -->
</form>
</div>
</div> <!-- contenitore_tabs -->
{% endblock %}
Fields name, surname and addresses belong to a sample Symfony2 entity Person.
addresses is the first and only element of a collection of addresses (I need this as collection for other reasons)
The working JS file is:
jQuery(document).ready(function() {
$(".blocco-tab").hide();
$("ul.tabs li:first").addClass("active").show();
$(".blocco-tab:first").show();
$("ul.tabs li").click(function() {
$("ul.tabs li").removeClass("active");
$(this).addClass("active");
$(".blocco-tab").hide();
var activeTab = $(this).find("a").attr("href");
$(activeTab).fadeIn();
return false;
});
});
The Entity file:
class Person {
protected $name;
protected $surname;
protected $addresses;
public function __construct(){
$this->addresses = new ArrayCollection();
}
}
And in the DefaultController:
public function tab1Action(Request $request){
$person = new Person();
$address = new Address();
$addr_coll = new ArrayCollection();
$addr_coll->add($address);
$tab1_type = new Tab1Type();
$person->setAddresses($addr_coll);
$form = $this->createForm($tab1_type, $person);
if ($request->getMethod() == 'POST')
{
$form->bindRequest($request);
if ($form->isValid())
/*ecc ecc ecc*/
}
public function tab2Action(Request $request){
$person = new Person();
$tab2_type = new Tab2Type();
$form = $this->createForm($tab2_type, $person);
if ($request->getMethod() == 'POST')
{
$form->bindRequest($request);
if ($form->isValid())
/*ecc ecc ecc*/
}
Actually I took the way of having every FormType having all fields that I don't need rendered but put 'hidden' and 'property_path' => false, because I can't render only my desired fields cause the other ones will cause errors at runtime (they're null) , but I still get problems handling both cases in a joined way.
Putting every form in a different page (== different Route), with different Controllers, everything works fine, so it's not a problem related to basic use of symfony, It's the integration of N forms in a single page with JQuery UI that makes me cry.
Fixed that I have to use this tabs, how can I solve?
Do I have to make a single Action handling everything?
Do I have to make a single form?
Do I miss something?
Thanks in advance, I hope I've been clear in explaining my issue.

You just used the same variable for different forms
<div id="tab1" class="blocco-tab">
<form action="{{ path('AAA') }}" method="post" {{ form_enctype(**form1**) }}>
<div id="name_field">
{{ form_row(**form1**.name) }}
</div><!-- /name_field -->
<div id="address">
{{ form_row(**form1**.addresses[0].road) }}
</div><!-- /address_field -->
</form>
</div>
<div id="tab2" class="blocco-tab">
<form action="{{ path('BBB') }}" method="post" {{ form_enctype(**form2**) }}>
<div id="surname_field">
{{ form_row(**form2**.surname) }}
</div><!-- /surname_field -->
</form>
</div>

Try with single form
{% extends "::base.html.twig" %}
{% block body %}
<form action="{{ path('AAA') }}" method="post" {{ form_enctype(form) }}>
<ul class="tabs">
<li class="left">tab1</li>
<li class="left">tab2</li>
<li class="left">tab3</li>
<li class="right">tab4</li>
</ul>
<div class="tabs_container">
<div id="tab1" class="blocco-tab">
<div id="name_field">
{{ form_row(form.name) }}
</div><!-- /name_field -->
<div id="address">
{{ form_row(form.addresses[0].road) }}
</div><!-- /address_field -->
</div>
<div id="tab2" class="blocco-tab">
<div id="surname_field">
{{ form_row(form.surname) }}
</div><!-- /surname_field -->
</div>
</div> <!-- contenitore_tabs -->
</form>
{% endblock %}
Then you have in ocontroller aaaAction()
public function aaaAction(Request $request){
$person = new Person();
$address = new Address();
$addr_coll = new ArrayCollection();
$addr_coll->add($address);
$person->setAddresses($addr_coll);
$form = $this->createForm(new PersonType(), $person);
if ($request->getMethod() == 'POST')
{
$form->bindRequest($request);
if ($form->isValid())
/*ecc ecc ecc*/
}
and class for form builder like
class PersonType extends AbstractType {
public function buildForm(FormBuilder $builder, array $options)
{
$builder
->add('name', null, array())
->add('surname', null, array())
->add('addresses', null, array())
;
}
public function getName()
{
return 'person';
}
public function getDefaultOptions(array $options)
{
return array(
'data_class' => 'Acme\YourBundle\Entity\Person',
);
}
}

as I mentioned before I solved wrapping all tabs in a single form. Both of your solutions are ok, thank you for your time.
Linuxatico

In case anyone else has this problem, you can solve it like this (I am not sure if this is the most elegant solution but I think its better than making one big form for everything). This example mixes the profile and changePassword FOSUserBundle forms:
Inside your main show template (Profile:show.html.twig in my case):
{% if profileForm is defined %}
{% include 'MyBundle:Profile:edit.html.twig' with {'form':profileForm} %}
{% else %}
{% render 'MyBundle:Profile:edit' %}
{% endif %}
Repeat for changePassword:
{% if passwdForm is defined %}
{% include 'MyBundle:ChangePassword:changePassword.html.twig' with {'form':passwdForm} %}
{% else %}
{% render 'FOSUserBundle:ChangePassword:changePassword' %}
{% endif %}
In your controllers (add else):
if ($form->isValid()) {
.....
else {
return $this->container->get('templating')->renderResponse('FOSUserBundle:Profile:show.html.'.$this->container->getParameter('fos_user.template.engine'),
array('user' => $user, 'profileForm' => $form->createView()));
}
Add the profileForm and passwdForm accordingly. In my case the modified controllers were the ProfileController and ChangePasswordControllers (FOSUserBundle overrides).
As for your tabs, you can add javascript (or twig) to open the tab if any error is found.
Hope this helps :)

Related

Octobercms Component - Limit results on relation

I have a plugin with 2 components. One is for 'posts', and the other is for a 'profile'. The profile belongs to a user and hasMany 'posts'.
However when I access the relationship it loads every post. I just want to load 5 then paginate or lazy load, how can I do this?
Profile.php model
public $hasMany = [
'posts' => [
'Redstone\Membership\Models\Post',
'table' => 'redstone_membership_profiles_posts',
]
];
public $belongsTo = [
'user' => [
'Rainlab\User\Models\User',
]
];
Post.php model
public $belongsTo = [
'profile' => ['Redstone\Membership\Models\Profile',
'table' => 'redstone_membership_profiles_posts',
]
];
Profile.php component
protected function loadProfile()
{
$id = $this->property('profileUsername');
$profile = new UserProfile
$profile = $profile->where(['slug' => $id]);
$profile = $profile->first();
return $profile;
}
profile/default.htm - component view
{% set profile = __SELF__.profile %}
{% set posts = __SELF__.profile.posts %}
{% for post in posts %}
<div class="card">
<div class="card-header">
<div class="ml-2">
<div class="h5 m-0">{{ profile.title }}</div>
</div>
{{ post.published_at|date('M d') }}
</div>
<div class="card-body text-left">
{{ post.content|raw }}
</div>
</div>
{% else %}
<h1>This user has not made any posts.</h1>
{% endfor %}
Well you can either do something with the OctoberCMS pagination service or a php function or you could build a function through twig.
PHP using slice: This is assuming when you call $profile->posts you get a collection of posts. You could also add a query to the Url like example.com/profile?q=15 to change 5 to 10 15 etc I added an if statement to check to make sure the input is numeric and greater than 5.
protected function loadProfile()
{
$id = $this->property('profileUsername');
$profile = UserProfile::where(['slug' => $id])->first();
if (is_numeric(Input::get('q')) == true && Input::get('q') > 5) {
$profile->posts = $profile->posts->slice(0, Input::get('q'));
} else {
$profile->posts = $profile->posts->slice(0, 5);
}
return $profile;
}
Twig using slice: This is done very similar to the PHP way but done in the htm file instead.
PHP -
protected function getQuery()
{
if (is_numeric(Input::get('q')) == true && Input::get('q') > 5) {
return Input::get('q');
} else {
return 5;
}
}
Twig -
{% set query = __SELF__.getQuery %}
{% set profile = __SELF__.profile %}
{% set posts = __SELF__.profile.posts | slice(0, query) %}
{% for post in posts %}
<div class="card">
<div class="card-header">
<div class="ml-2">
<div class="h5 m-0">{{ profile.title }}</div>
</div>
{{ post.published_at|date('M d') }}
</div>
<div class="card-body text-left">
{{ post.content|raw }}
</div>
</div>
{% else %}
<h1>This user has not made any posts.</h1>
{% endfor %}

Symfony rendering templates from ajax

I have the following ajax I call:
$('.checkbox').click(function () {
$.ajax({
type:'POST',
url: '/loadProducts',
data: {},
success: function(response) {
$('.js-products').html(response);
}});
return false;
});
Now when I call my endpoint loadProducts, I wants to get products, render a product template(s) and return all of them.
In my endpoint I could do this:
/**
* #Route("/loadProducts", name="loadProducts")
*/
public function loadProducts() {
/* #var \AppBundle\Service\ProductService $productService */
$productService = $this->get('app.product');
$products = $productService->getProducts();
$productItems = [];
foreach ($products as $product) {
$productItems = $this->render('home/productItem.html.twig', [
'product' => $product
]);
}
$response = new Response();
$response->headers->set('Content-Type', 'application/json');
$response->setContent(json_encode($productItems));
return $response;
}
But that only renders one product. How could I return rendering multiple product.html.twig files? (apart from creating a new template that renders all)
You will get something like this. Your action will start to render products.html.twig which have a loop that renders multiple times product.html.twig
products.html.twig
{% extends base_template %}
{% block body %}
<div class="container">
<div class="row">
{% for product in products %}
<div class="col-md-3">
{% include 'shop/product.html.twig' %}
</div>
{% endfor %}
</div>
</div>
{% endblock %}
product.html.twig
<div class="product">
<img src="somesource.jpg">
<div class="description">
<h3>{{ product.name }}</h3>
<p>{{ product.description }}</p>
</div>
</div>

OctoberCMS form request not submitted

I'm following this tutorial to create a simple OctoberCMS plugin.
Here is content of /acme/demo/components/todo/default.htm page:
{% set tasks = __SELF__.tasks %}
<form data-request="{{ __SELF__ }}::onAddItem" data-request-success="$('#inputItem').val('success')">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Tasks assigned to: {{__SELF__.name}} </h3>
</div>
<div class="panel-body">
<div class="input-group">
<input name="task" type="text" id="inputItem" class="form-control" value=""/>
<span class="input-group-btn">
<button type="submit" class="btn btn-primary">Add</button>
</span>
</div>
</div>
<ul class="list-group" id="result">
{% for task in tasks %}
<li class="list-group-item">
{{ task }}
<button class="close pull-right">×</button>
</li>
{% endfor %}
</ul>
</div>
</form>
And here is the content of /acme/demo/components/Todo.php:
<?php namespace Acme\Demo\Components;
use Cms\Classes\ComponentBase;
use Acme\Demo\Models\Task;
class Todo extends ComponentBase
{
/**
* This is a person's name.
* This variable will be available in the page as a twig variable
* #var string
*/
public $name;
/**
* The collection of tasks.
* #var array
*/
public $tasks;
public function componentDetails()
{
return [
'name' => 'Todo Component',
'description' => 'A database driven TODO list'
];
}
public function defineProperties()
{
return [];
}
public function init()
{
// This will execute when the component is first initialized, including AJAX events.
}
public function onRun()
{
$this->name = 'Meysam';
$this->tasks = Task::lists('title');
}
public function onAddItem()
{
$taskName = post('task');
$task = new Task();
$task->title = $taskName;
$task->save();
}
}
The problem is that onAddItem is never called. It seems that the form is not submitted properly when I add a new item. Does anybody know how I can fix this? I thought maybe the ajax libraries are missing, so I included {% framework %} as well, which again didn't help:
{% set tasks = __SELF__.tasks %}
{% framework %}
<form data-request="{{ __SELF__ }}::onAddItem" data-request-success="$('#inputItem').val('success')">
Please note that my model is working and $this->tasks = Task::lists('title'); returns the list of tasks.
The problem was that I should have included the jquery file as well:
<script src="{{ [
'assets/javascript/jquery.js',
]|theme }}"></script>
{% framework %}

Extend the ChoiceType to get a new option in a form

In a form I need to associate each radio button of an input to a different image.
I would call the construction of this form this way:
$builder = $this->formFactory->createBuilder();
$builder->add('input_name', 'my_choice', array(
'data' => 'n',
'choices' => array('c1' => 'choice1', 'c2' => 'choice2'),
'required' => true,
'expanded' => true,
'multiple' => false,
'images' => array('choice1.jpg', 'choice2.jpg')));
$form = $builder->getForm();
Meaning the radio_button choice1 will be associated to choice1.jpg and the radio_button choice2 will be associated to choice2.jpg.
In plaint HTML using Bootstrap I want this result
<form method="post" action="{{ path('my_path')}}" name="myform">
<div class="btn-group btn-group-justified" data-toggle="buttons">
<label id="btn-choice1" class="btn active">
<input type="radio" name="is_choice" id="c1" value="Choice1" checked>
<img id="choice1_img" src="{{ asset('bundles/myBundle/icons/choice1.png') }}" alt="Choice 1">
</label>
<label id="btn-choice2" class="btn">
<input type="radio" name="is_choice" id="c2" value="Choice2"><img id="choice2_img" src="{{ asset('bundles/myBundle/icons/choice2.png') }}" alt="Choice 2">
</label>
</div>
<button class="btn" type="submit" name="submit">Submit</button>
</form>
I read the official Symfony doc related to this: http://symfony.com/fr/doc/2.3/cookbook/form/create_custom_field_type.html
but the example is very poor.
I need to describe to Symfony where to look for this new option but cannot figure out how to achieve this. I read hundreds of examples on the net but nothing explaining the logic behind how to do this.
So far My new Type is like this below:
<?php
namespace myProject\Bundle\MyBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\FormInterface;
class MyChoiceType extends AbstractType
{
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars = array_replace($view->vars, array(
'images' => $options['images']
));
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'images' => array()
));
}
public function getParent()
{
return 'choice';
}
public function getName()
{
return 'my_choice';
}
}
Would you be able to explain me how to describe this new Type to Symfony (what methods to call with what parameters).
I breaked the code for hours but Symfony is making so muchfor a simple form creation that I lost the path. FormRegistry, FormType, OptionsResolverInterface, BaseType, FormFactory and so many others classes are involved that it makes it difficult to get the global picture.
Otherwise if one can point me to an architecture document explaining the logical behind this it could also be fine.
Found the solution.
I needed also to access the images new option through a Form theme redefinition. That was the difficult part since I didn't know how and what I could access from the form theme template.
Here is how I succeeded in doing it:
{% block my_choice_widget %}
{% spaceless %}
{% if expanded %}
<ul {{ block('widget_container_attributes') }}>
{% for child in form %}
<li>
{{ form_widget(child) }}
{{ form_label(child) }}
{% if images is defined %} {{images[loop.index-1]}} {% endif %}
</li>
{% endfor %}
</ul>
{% else %}
{# just let the choice widget render the select tag #}
{{ block('choice_widget') }}
{% endif %}
{% endspaceless %}
{% endblock %}
The part I added to the Symfony form them template is:
{% if images is defined %} {{images[loop.index-1]}} {% endif %}
That displays the image name in front of the proper radio button.
That should be refined to make sure there are enough images to populate all the loops.
A simple twig test on images count compared to loop.last element will make it.

Advanced routing parameter handling in twig and Symfony2

I have this configuration:
A page containing a search field, at the submit in the same page I want a list of every result matching the research, everyone linking to a corresponding route. For example if I find 4 elements, I want that in the resulting page 4 links Azienda1, Azienda2 ecc.
Now I get this error:
An exception has been thrown during the rendering of a template ("The "ABCAziendaBundle_visualizza_azienda" route has some missing mandatory parameters ("id_azienda").") in ::base.html.twig at line 27.
500 Internal Server Error - Twig_Error_Runtime
1 linked Exception:
MissingMandatoryParametersException
Here are the key files,
#config.yml
ABCAziendaBundle_visualizza_azienda:
pattern: /visualizza_azienda/{id_azienda}
defaults: { _controller: ABCAziendaBundle:Default:showAzienda }
requirements:
id_azienda: \d+
ABCAziendaBundle_azienda_index:
pattern: /
defaults: { _controller: ABCAICAziendaBundle:Default:indexAzienda }
#DefaultController.php
public function indexAziendaAction(Request $request) {
$searchFormType = new SearchAziendaType();
$form = $this->createForm($searchFormType);
if ($request->getMethod() == 'POST')
{
$form->bindRequest($request);
if ($form->isValid())
{
$data = $form->getData();
$em = $this->getDoctrine()->getEntityManager();
$aziende = $em->getRepository('ABCAziendaBundle:Azienda')->findAziendaByAliasOrRagioneSocialeSubstring($data["search_field"]);
return $this->render('ABCAziendaBundle:Default:indexAzienda.html.twig', array('form' => $form->createView(), 'aziende' => $aziende));
}
}
return $this->render('ABCAziendaBundle:Default:indexAzienda.html.twig', array('form' => $form->createView()));
}
public function showAziendaAction($id_azienda) {
echo "non entra qui";
}
#indexAzienda.html.twig
{% extends "::base.html.twig" %}
{% block pagetitle %}ABC{% endblock %}
{% block body %}
<h2>Ricerca azienda</h2>
<div id="form_container">
<form action="{{ path('ABCAziendaBundle_azienda_index') }}" method="post" {{ form_enctype(form) }}>
{{ form_label(form.search_field, "Ricerca Azienda") }}
{{ form_widget(form.search_field) }}
<input type="submit" />
</form>
<button>nuova azienda</button>
</div>
{% if aziende is defined %}
{% for azienda in aziende %}
<div class="areaTot">
{{azienda.alias}}
</div>
{% endfor %}
{% else %}
<div class="areaTot">
<p>"NIENTE"</p>
</div>
{% endif %}
{% endblock %}
I think its a typo on your side:
<a href="{{ path('ABCAziendaBundle_visualizza_azienda', { 'azienda_id' : azienda.id }) }}">
Should be:
<a href="{{ path('ABCAziendaBundle_visualizza_azienda', { 'id_azienda' : azienda.id }) }}">
The difference is the route parameter, you wrote azienda_id initialy, but the route parameter name is id_azienda
Should clear the error.
Regards,
Matt

Categories