Dropdown Menus with KnpMenuBundle and Bootstrap 3 - symfony

I've just added the KnpMenuBundle to my Symfony project.
I also added Bootstrap to make it look nicer.
Now I want to create a Submenu or Dropdownmenu.
Is there a simple example / way how to do this?
I've searched a lot of examples but either they require an additional bundle or do not work
Update
I need to know how I have to add attributes to my menu children in order to render it with bootstrap
Best regards
Oliver

It's rather straightforward. For attributes, see the KnpMenu docs.
An example:
base.html.twig, which appears in templates with {% extends "base.html.twig" %}:
...
{% block stylesheets %}
<link href="{{ asset('css/bootstrap.css') }}" rel="stylesheet" media="all">
<link href="{{ asset('css/pdf.css') }}" rel="stylesheet" media="all">
<link href="{{ asset('css/jquery-ui.min.css') }}" rel="stylesheet" media="all" />
{% endblock %}
...
{% block nav %}
{% spaceless %}
<nav class="navbar navbar-default">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#"></a>
</div>
<div class="collapse navbar-collapse navbar-ex1-collapse">
{{ knp_menu_render('AppBundle:Builder:mainMenu', {'style': 'navbar'}) }}
{{ knp_menu_render('AppBundle:Builder:logoutMenu', { 'style': 'navbar-right' }) }}
</div>
</nav>
{% endspaceless %}
{% endblock nav %}
...
{% block javascripts %}
<script
src="https://code.jquery.com/jquery-1.11.3.js"
integrity="sha256-IGWuzKD7mwVnNY01LtXxq3L84Tm/RJtNCYBfXZw3Je0="
crossorigin="anonymous"></script>
<script src="{{ asset('js/bootstrap.js') }}"></script>
<script src="{{ asset('js/jquery-ui.min.js') }}"></script>
<script src="{{ asset('js/project.js') }}"></script>
{% endblock %}
Builder.php:
...
class Builder implements ContainerAwareInterface
{
use ContainerAwareTrait;
public function mainMenu(FactoryInterface $factory, array $options)
{
$checker = $this->container->get('security.authorization_checker');
$menu = $factory->createItem('root');
$menu->addChild('Home', array('route' => 'homepage'));
$menu->addChild('Receipt');
$menu['Receipt']->addChild('Add receipt', [
'route' => 'receipt_add'
]);
$menu['Receipt']->addChild('Edit receipt', [
'route' => 'receipt_edit'
]);
$menu->addChild('Artist');
$menu['Artist']->addChild('Add artist', [
'route' => 'artist_add'
]);
$menu['Artist']->addChild('Edit artist', [
'route' => 'artist_edit'
]);
$menu['Artist']->addChild('Add existing to show', [
'route' => 'existing_artists'
]);
$menu->addChild('Tickets');
$menu['Tickets']->addChild('Add block', [
'route' => 'block_add'
]);
$menu['Tickets']->addChild('Edit block', [
'route' => 'block_edit'
]);
$menu->addChild('Show');
$menu['Show']->addChild('Add show', [
'route' => 'show_add'
]);
$menu['Show']->addChild('Edit show', [
'route' => 'show_edit'
]);
$menu->addChild('Reports', [
'route' => 'reports'
]);
if ($checker->isGranted('ROLE_ADMIN')) {
$menu->addChild('Admin');
$menu['Admin']->addChild('Ticket block reassign', [
'route' => 'block_reassign'
]);
$menu['Admin']->addChild('Users', [
'route' => 'easyadmin'
]);
$menu['Admin']->addChild('Delete artist', [
'route' => 'artist_delete'
]);
}
return $menu;
}
...

Related

Symfony: Printing Multiple Forms Per View

I created a form with builderform. The view shows a list of private messages when you click one of the messages, a modal window opened, all of this works, but when I copy the form into the modal, only the last private message shows this form, others modal windows doesn't print this.
This is my form:
class MessageReplyFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('reply', TextareaType::class, array(
'label'=>false,
'required'=>true,
"attr" => array('placeholder'=>'Escribe aquí tu mensaje...')
))
->add('image', FileType::class, [
'label' => false,
'required' => false,
'data_class' => null,
'mapped' => false,
'attr'=>[
'class' => 'input-image',
'lang'=>'es',
'placeholder' => "Subir imagen ..."
],
'constraints' => [ new Image(['maxSize' => '500k'])
]
])
->add('send', SubmitType::class, array(
"label" => 'send',
"attr" => array(
"class" => "form-submit btn btn-default btn-vibrisas btn-reply"
)
))
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => MessageReply::class,
]);
}
}
Here my view...
<div class="card">
<div class="card-body publications-container">
<h1 class="pub">Mensajes privados</h1>
<hr>
{% for p in private_messages %}
<div class="card message-card">
<div class="card-body message-body" >
<!-- Information about each message -->
</div>
</div>
<!-- The Modal -->
<div id="openMessage-{{ p.id }}" tabindex="-1" class="openMessage modal fade" role="dialog">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<!-- Modal Header -->
<div class="modal-header">
<!-- basic message information -->
</div>
<!-- Modal body -->
<div class="modal-body">
<div class="message-f">
{{ p.emitter.name }}<br>
{{ p.message }}<br>
{% if p.image != null %}
<div class="openImage" data-toggle="modal" data-target="#openImage-{{ p.id }}" href="/uploads/users/messages/{{ p.image }}">
<img class="message-image" src="{{ asset('/uploads/users/messages/'~p.image) }}"/>
</div>
<div id="openImage-{{ p.id }}" class="openImage modal" tabindex="-1" role="dialog">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<img src="{{ asset('/uploads/users/messages/'~p.image) }}"/>
</div>
</div>
</div>
{% endif %}
</div>
{% form_theme message_reply 'bootstrap_4_layout.html.twig' %}
{{ form_start(message_reply) }}
<input type="text" name="idmessage" required class="form-control border-input hidden" placeholder="" value="{{ p.id }}">
{{ form_end(message_reply) }}
</div>
</div>
</div>
</div>
<!-- end modal -->
{% endfor %}
</div>

Adding multi-level menu's with Timber

I am not familiar with Timber - in-fact this is the first time I've ever heard of it, but I'm helping a charity out who's web developer have left them in the lurch.
I've worked most things out - except getting sub-menu's working.
When items are added to the sub-menu location in Wordpress, they are appearing on the same level on the main menu.
Was wondering if someone could help me out?
Code is below for the various "twigs" - but let me know if you need the functions code as well to help out.
menu.twig:
{% for item in menu %}
<li class="{{ prefix }}__li {{ item.classes | join(' ') }}">
<a class="{{ prefix }}__a" target="{{ item.target }}" href="{{ item.link }}"><span>{{ item.title }}</span></a>
{% include "menu.twig" with {'menu': item.get_children} %}
</li>
{% endfor %}
{% endif %}
header.twig
<header class="header">
<div class="header__secondary">
<div class="container">
<div class="row">
<div class="col-xs-12">
<nav class="header__nav nav-secondary">
<ul class="nav-secondary__ul">
{% include "menu.twig" with {
'menu': menu.header_secondary.items,
'prefix': 'nav-secondary'
} %}
<li class="nav-secondary__li nav-secondary__li--cart">
<a class="nav-secondary__a cart-customlocation" href="{{ cart_url }}"></a>
</li>
</ul>
</nav>
</div>
</div>
</div>
</div>
<div class="header__primary">
{% block header %}
<div class="container">
<div class="row">
<div class="col-xs-6 col-md-2">
<a href="/" class="header__logo">
<img src="{{ site.theme.link }}/dist/img/logo.jpg" alt="{{ site.name }} Logo">
</a>
</div>
<div class="col-xs-6 col-md-10">
<nav class="header__nav nav" role="navigation">
<ul class="nav__ul">
{% include "menu.twig" with {
'menu': menu.header_primary.items,
'prefix': 'nav'
} %}
</ul>
</nav>
<div class="hamburger hamburger--spring">
<div class="hamburger-box">
<div class="hamburger-inner"></div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
</div>
</header>
functions.php
<?php
/**
* Timber starter-theme
* https://github.com/timber/starter-theme
*
* #package WordPress
* #subpackage Timber
* #since Timber 0.1
*/
if ( ! class_exists( 'Timber' ) ) {
add_action( 'admin_notices', function() {
echo '<div class="error"><p>Timber not activated. Make sure you activate the plugin in ' . esc_url( admin_url( 'plugins.php' ) ) . '</p></div>';
});
add_filter('template_include', function( $template ) {
return get_stylesheet_directory() . '/static/no-timber.html';
});
return;
}
Timber::$dirname = array('views');
Timber::$autoescape = false;
/**
* We're going to configure our theme inside of a subclass of Timber\Site
* You can move this to its own file and include here via php's include("MySite.php")
*/
class StarterSite extends Timber\Site {
/** Add timber support. */
public function __construct() {
add_action( 'after_setup_theme', array( $this, 'theme_supports' ) );
add_filter( 'timber_context', array( $this, 'add_to_context' ) );
add_filter( 'get_twig', array( $this, 'add_to_twig' ) );
add_action( 'init', array( $this, 'register_post_types' ) );
add_action( 'init', array( $this, 'register_taxonomies' ) );
add_action( 'wp_enqueue_scripts', [$this, 'load_scripts'] );
parent::__construct();
register_nav_menu('header-secondary', 'Header Secondary');
register_nav_menu('header-primary', 'Header Primary');
remove_action('woocommerce_before_main_content', 'woocommerce_breadcrumb', 20, 0); // kill woo breadcrumbs
remove_action( 'woocommerce_before_shop_loop' , 'woocommerce_catalog_ordering', 30 ); // kill woo sorting
remove_action( 'woocommerce_before_shop_loop' , 'woocommerce_result_count', 20 ); // kill woo # results
add_filter( 'woocommerce_add_to_cart_fragments', [$this, 'woocommerce_header_add_to_cart_fragment']);
add_filter( 'woocommerce_get_image_size_gallery_thumbnail', function( $size ) {
return array(
'width' => 400,
'height' => 400,
'crop' => 0,
);
});
add_action( 'after_setup_theme', function() {
add_theme_support( 'woocommerce' );
} );
}
/** This is where you can register custom post types. */
public function register_post_types() {
$name = "adoption";
$singular = "Adoption";
$plural = "Adoptions";
$labels = array(
'name' => _x("$plural", 'en'),
'singular_name' => _x("$singular", 'en'),
'all_items' => "All $plural",
'add_new' => _x("Add New $singular", 'en'),
'add_new_item' => _x("Add New $singular", 'en'),
'edit_item' => _x("Edit $singular", 'en'),
'new_item' => _x("New $singular", 'en'),
'view_item' => _x("View $singular", 'en'),
'search_items' => _x("Search $plural", 'en'),
'not_found' => _x("No $singular found", 'en'),
'not_found_in_trash' => _x("No $singular found in Trash", 'en'),
'parent_item_colon' => _x("Parent $singular:", 'en'),
'menu_name' => _x("$plural", 'en'),
);
$args = array(
'labels' => $labels,
'hierarchical' => false,
'description' => $plural,
'supports' => array(
'title',
'editor',
'thumbnail',
'revisions'
),
'taxonomies' => ['adoption_type'],
'public' => true,
'show_ui' => true,
'show_in_menu' => true,
'menu_position' => 4,
'menu_icon' => 'dashicons-welcome-view-site',
'show_in_nav_menus' => true,
'publicly_queryable' => true,
'exclude_from_search' => false,
'has_archive' => true,
'query_var' => true,
'can_export' => true,
'rewrite' => true,
'capability_type' => 'post'
);
register_post_type($name, $args);
$name = "event";
$singular = "Event";
$plural = "Events";
$labels = array(
'name' => _x("$plural", 'en'),
'singular_name' => _x("$singular", 'en'),
'all_items' => "All $plural",
'add_new' => _x("Add New $singular", 'en'),
'add_new_item' => _x("Add New $singular", 'en'),
'edit_item' => _x("Edit $singular", 'en'),
'new_item' => _x("New $singular", 'en'),
'view_item' => _x("View $singular", 'en'),
'search_items' => _x("Search $plural", 'en'),
'not_found' => _x("No $singular found", 'en'),
'not_found_in_trash' => _x("No $singular found in Trash", 'en'),
'parent_item_colon' => _x("Parent $singular:", 'en'),
'menu_name' => _x("$plural", 'en'),
);
$args = array(
'labels' => $labels,
'hierarchical' => false,
'description' => $plural,
'supports' => array(
'title',
'editor',
'thumbnail',
'revisions'
),
'public' => true,
'show_ui' => true,
'show_in_menu' => true,
'menu_position' => 4,
'menu_icon' => 'dashicons-welcome-view-site',
'show_in_nav_menus' => true,
'publicly_queryable' => true,
'exclude_from_search' => false,
'has_archive' => true,
'query_var' => true,
'can_export' => true,
'rewrite' => true,
'capability_type' => 'post'
);
register_post_type($name, $args);
if(function_exists('acf_add_options_page')) {
acf_add_options_page([
'page_title' => 'Theme Content',
'menu_title' => 'Theme Content',
'menu_slug' => 'theme-content',
'capability' => 'edit_posts',
'redirect' => false
]);
}
if(function_exists('acf_add_options_page')) {
acf_add_options_page([
'page_title' => 'Donation Settings',
'menu_title' => 'Donation Settings',
'menu_slug' => 'donation-settings',
'capability' => 'edit_posts',
'redirect' => false
]);
}
}
/** This is where you can register custom taxonomies. */
public function register_taxonomies() {
$labels = array(
'name' => 'Adoption Types',
'singular_name' => 'Adoption Type',
'menu_name' => 'Adoption Type'
);
$rewrite = array(
'slug' => 'adoptions',
'with_front' => true,
'hierarchical' => false,
);
$args = array(
'labels' => $labels,
'hierarchical' => true,
'public' => true,
'show_ui' => true,
'show_admin_column' => true,
'show_in_nav_menus' => true,
'show_tagcloud' => false,
'rewrite' => $rewrite,
);
register_taxonomy( 'adoption_type', 'adoption', $args );
}
public function load_scripts() {
wp_enqueue_style( 'main', get_stylesheet_directory_uri() . '/dist/css/main.min.css' );
wp_enqueue_script('slick-js', get_stylesheet_directory_uri() . '/dist/js/plugins/slick.min.js', 'jquery', false, true);
wp_enqueue_script('main-js', get_stylesheet_directory_uri() . '/dist/js/main.js', ['jquery', 'slick-js'], false, true);
}
/** This is where you add some context
*
* #param string $context context['this'] Being the Twig's {{ this }}.
*/
public function add_to_context( $context ) {
$context['options'] = get_fields('options');
$context['menu']['header_primary'] = new Timber\Menu('header-primary');
$context['menu']['header_secondary'] = new Timber\Menu('header-secondary');
$context['shop_url'] = get_permalink(woocommerce_get_page_id('shop' ));
$context['site'] = $this;
if (get_the_ID()) {
// load page modules
require_once('src/modules.php');
}
return $context;
}
public function theme_supports() {
// Add default posts and comments RSS feed links to head.
add_theme_support( 'automatic-feed-links' );
/*
* Let WordPress manage the document title.
* By adding theme support, we declare that this theme does not use a
* hard-coded <title> tag in the document head, and expect WordPress to
* provide it for us.
*/
add_theme_support( 'title-tag' );
/*
* Enable support for Post Thumbnails on posts and pages.
*
* #link https://developer.wordpress.org/themes/functionality/featured-images-post-thumbnails/
*/
add_theme_support( 'post-thumbnails' );
/*
* Switch default core markup for search form, comment form, and comments
* to output valid HTML5.
*/
add_theme_support(
'html5', array(
'comment-form',
'comment-list',
'gallery',
'caption',
)
);
/*
* Enable support for Post Formats.
*
* See: https://codex.wordpress.org/Post_Formats
*/
add_theme_support(
'post-formats', array(
'aside',
'image',
'video',
'quote',
'link',
'gallery',
'audio',
)
);
add_theme_support( 'menus' );
}
/** This Would return 'foo bar!'.
*
* #param string $text being 'foo', then returned 'foo bar!'.
*/
public function myfoo( $text ) {
$text .= ' bar!';
return $text;
}
/** This is where you can add your own functions to twig.
*
* #param string $twig get extension.
*/
public function add_to_twig( $twig ) {
$twig->addExtension( new Twig_Extension_StringLoader() );
$twig->addFilter( new Twig_SimpleFilter( 'myfoo', array( $this, 'myfoo' ) ) );
return $twig;
}
function woocommerce_header_add_to_cart_fragment( $fragments ) {
global $woocommerce;
ob_start();
?>
<a class="cart-customlocation" href="<?php echo $woocommerce->cart->get_cart_url(); ?>" title="<?php _e('View your shopping cart', 'woothemes'); ?>"><?php echo sprintf(_n('%d item', '%d items', $woocommerce->cart->cart_contents_count, 'woothemes'), $woocommerce->cart->cart_contents_count);?> - <?php echo $woocommerce->cart->get_cart_total(); ?></a>
<?php
$fragments['a.cart-customlocation'] = ob_get_clean();
return $fragments;
}
}
new StarterSite();
EDIT: Based on feedback received, the menu.twig code is as follows - however, not showing any menus now:
{% if items|default(menu.items) %}
<ul>
{% for item in items|default(menu.items) %}
<li class="{{ prefix }}__li {{ item.classes | join(' ') }}">
<a class="{{ prefix }}__a" target="{{ item.target }}" href="{{ item.link }}"><span>{{ item.title }}</span></a>
{% include "menu.twig" with {'menu': item.get_children} %}
</li>
{% endfor %}
</ul>
{% endif %}
EDIT 2: Below is header.twig as there's some more menu stuff in there...
<header class="header">
<div class="header__secondary">
<div class="container">
<div class="row">
<div class="col-xs-12">
<nav class="header__nav nav-secondary">
<ul class="nav-secondary__ul">
{% include "menu.twig" with {
'menu': menu.menu_header_secondary.items,
'prefix': 'nav-secondary'
} %}
<li class="nav-secondary__li nav-secondary__li--cart">
<a class="nav-secondary__a cart-customlocation" href="{{ cart_url }}"></a>
</li>
</ul>
</nav>
</div>
</div>
</div>
</div>
<div class="header__primary">
{% block header %}
<div class="container">
<div class="row">
<div class="col-xs-6 col-md-2">
<a href="/" class="header__logo">
<img src="{{ site.theme.link }}/dist/img/logo.jpg" alt="{{ site.name }} Logo">
</a>
</div>
<div class="col-xs-6 col-md-10">
<nav class="header__nav nav" role="navigation">
<ul class="nav__ul">
{% include "menu.twig" with {
'menu': menu.menu_header_primary.items,
'prefix': 'nav'
} %}
</ul>
</nav>
<div class="hamburger hamburger--spring">
<div class="hamburger-box">
<div class="hamburger-inner"></div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
</div>
</header>
EDIT 3 - current config after #Gchtr input and getting the correct menus to display in the correct locations. Still no sub-menus:
menu.twig:
{% set items = items|default(menu.items) %}
{% if items %}
<ul>
{% for item in items %}
<li class="{{ prefix }}__li {{ item.classes | join(' ') }}">
<a class="{{ prefix }}__a" target="{{ item.target }}" href="{{ item.link }}"><span>{{ item.title }}</span></a>
{% if item.children %}
{% include "menu.twig" with { items: item.children } %}
{% endif %}
</li>
{% endfor %}
</ul>
{% endif %}
header.twig:
<header class="header">
<div class="header__secondary">
<div class="container">
<div class="row">
<div class="col-xs-12">
<nav class="header__nav nav-secondary">
<ul class="nav-secondary__ul">
{% include "menu.twig" with {
'menu': menu_header_secondary,
'prefix': 'nav-secondary'
} %}
<li class="nav-secondary__li nav-secondary__li--cart">
<a class="nav-secondary__a cart-customlocation" href="{{ cart_url }}"></a>
</li>
</ul>
</nav>
</div>
</div>
</div>
</div>
<div class="header__primary">
{% block header %}
<div class="container">
<div class="row">
<div class="col-xs-6 col-md-2">
<a href="/" class="header__logo">
<img src="{{ site.theme.link }}/dist/img/logo.jpg" alt="{{ site.name }} Logo">
</a>
</div>
<div class="col-xs-6 col-md-10">
<nav class="header__nav nav" role="navigation">
<ul class="nav__ul">
{% include "menu.twig" with {
'menu': menu_header_primary,
'prefix': 'nav'
} %}
</ul>
</nav>
<div class="hamburger hamburger--spring">
<div class="hamburger-box">
<div class="hamburger-inner"></div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
</div>
</header>
functions.php (snippet of the Header Stuff instead of the whole lot):
public function add_to_context( $context ) {
$context['options'] = get_fields('options');
$context['menu_header_primary'] = new Timber\Menu( 'header-primary' );
$context['menu_header_secondary'] = new Timber\Menu( 'header-secondary') ;
$context['shop_url'] = get_permalink(woocommerce_get_page_id('shop' ));
$context['site'] = $this;
In your StarterSite class in the add_to_context() method, you’re adding your menus like so:
$context['menu']['header_primary'] = new Timber\Menu('header-primary');
$context['menu']['header_secondary'] = new Timber\Menu('header-secondary');
When you now want to use access menu.items in your Twig, that doesn’t work, because you don’t access the nested value. It should be menu.header_primary.items. To simplify this, I’d change that to use separate context entries:
$context['menu_header_primary'] = new Timber\Menu( 'header-primary' );
$context['menu_header_secondary'] = new Timber\Menu( 'header-secondary') ;
Then, for your Twig file, you’ll always need to pass a menu variable.
{% set items = items|default(menu.items) %}
{% if items %}
<ul>
{% for item in items %}
<li class="{{ prefix }}__li {{ item.classes | join(' ') }}">
<a class="{{ prefix }}__a" target="{{ item.target }}" href="{{ item.link }}"><span>{{ item.title }}</span></a>
{% if item.children %}
{% include "menu.twig" with { items: item.children } %}
{% endif %}
</li>
{% endfor %}
</ul>
{% endif %}
Edit
In your header.twig, you’ll need to remove the nesting from menu include as well (menu_header_primary.items instead of menu.menu_header_primary.items. Otherwise, Timber will the take the first menu it can find.
<nav class="header__nav nav" role="navigation">
<ul class="nav__ul">
{% include "menu.twig" with {
menu: menu_header_primary,
prefix: 'nav'
} %}
</ul>
</nav>
Function.php
/**
* Regsiter Nav Menus
*/
public function theme_register_nav_menus() {
register_nav_menus( array(
'primary_menu' => esc_html__( 'Primary Menu', 'themenamehere' ),
'secondary_menu' => esc_html__( 'Secondary Menu', 'themenamehere' ),
) );
}
/** This is where you add some context
*
* #param string $context context['this'] Being the Twig's {{ this }}.
*/
public function add_to_context( $context ) {
$context['menu'] = new Timber\Menu();
$context['secondaryMenu'] = new Timber\Menu('secondary_menu');
$context['site'] = $this;
return $context;
}
Menu.twig
{% if menu %}
<ul>
{% for item in items %}
<li class="{{ item.classes | join(' ')}}">
{# <a target="{{ item.target}}" href="{{ item.link}}">{{ item.title}}</a> #}
<a target="_self" href="{{ item.link}}">{{ item.title}}</a>
{% include "menu.twig" with{'items':item.children} %}
</li>
{% endfor %}
</ul>
{% endif %}
After that include that file inside Twig file bascically in base.twig file:
For Primary Menu Code looks like:
{% include "menu.twig" with{'items':menu.get_items} %}
For Secondary Menu Code looks like:
{% include "menu.twig" with{'items':secondaryMenu.get_items} %}
it looks like you are missing <ul></ul> wrapper or you haven't posted the code in the entire:
{% if menu %}
<ul>
{% for item in menu %}
<li class="{{ prefix }}__li {{ item.classes | join(' ') }}">
<a class="{{ prefix }}__a" target="{{ item.target }}" href="{{ item.link }}">{{ item.title }}</a>
{% include "menu.twig" with {'items': item.children} %}
</li>
{% endfor %}
</ul>
{% endif %}

Customize symfony data-prototype for edit form

I know it's a common question but I cannot figure out how to achieve that.
I have a Course entity and a CourseDocument entity.
Course(id, documents, ...)
CourseDocument(id, file, course)
In my Course form:
class CourseType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('title', TextType::class, [
'label' => 'course.title',
])
->add('documents', CollectionType::class, array(
'entry_type' => MediaType::class,
'label' => 'course.documents_list',
'entry_options' => array(
'label' => false,
'data_class' => CourseDocument::class,
),
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
))
;
}
In the form I want to be able to add as many documents as I want. I set up everything, javascript included. My only issue is the data-prototype that is not what I want.
{% import "macros/prototype.html.twig" as prototype %}
{{ form_start(form) }}
<div class="row">
<div class="col-md-6">
<fieldset class="form-group">
{{ form_label(form.documents) }}
<div id="course_documents" class="collection_holder" data-prototype="{{ prototype.tagCollectionItem(form.documents.vars.prototype)|e }}">
{% for widget in form.documents %}
{{ prototype.tagCollectionItem(widget) }}
{% endfor %}
</div>
<button type="button" id="add-document-btn" data-target-collection="#{{ form.documents.vars.id }}" class="btn btn-sm btn-info"><i class="la la-plus"></i> {{ 'course.buttons.add_document' | trans({}, 'labels') }}</button>
</fieldset>
</div>
</div>
{{ form_end(form) }}
I'm generating my prototype with a macro:
{% macro tagCollectionItem(item) %}
<fieldset class="form-group">
<div id="{{ item.vars.id }}">
<div class="custom-file">
{{ form_widget(item.uploadedFile) }}
{{ form_label(item.uploadedFile, item.uploadedFile.vars.label, {'label_attr': {'class': 'custom-file-label'}}) }}
</div>
</div>
</fieldset>
{% endmacro %}
It's working pretty well except for editing. I don't want the input if I already selected a file. But I want the name of the file.
I finally used a macro.
{% macro tagCollectionItem(item) %}
<fieldset class="form-group">
<div id="{{ item.vars.id }}">
{% if item.uploadedFile.vars.file_url or item.uploadedFile.vars.image_url %}
{{ form_errors(item.uploadedFile) }}
{{ form_widget(item.uploadedFile, {'attr': {'hidden': true}}) }}
{% else %}
<div class="custom-file">
{{ form_widget(item.uploadedFile) }}
{{ form_label(item.uploadedFile, item.uploadedFile.vars.label, {'label_attr': {'class': 'custom-file-label'}}) }}
</div>
{% endif %}
</div>
</fieldset>
{% endmacro %}
The twig file for the form
<fieldset class="form-group">
{{ form_label(form.documents) }}
<div id="course_documents" class="collection_holder" data-prototype="{{ prototype.tagCollectionItem(form.documents.vars.prototype)|e }}">
{% for widget in form.documents.children %}
{{ prototype.tagCollectionItem(widget) }}
{% endfor %}
</div>
<button type="button" id="add-document-btn" data-target-collection="#{{ form.documents.vars.id }}" class="btn btn-sm btn-info"><i class="la la-plus"></i> {{ 'course.buttons.add_document' | trans({}, 'labels') }}</button>
</fieldset>

How can I make it work clockpicker in Symfony forms?

I'm trying to make a friendly interface to select TIME and save it to database, so I decided to use bootstrap clockpicker.
This is my layout.html.twig:
{% extends 'base.html.twig' %}
{% block stylesheets %}
{{ parent() }}
<link href="{{ asset('public/css/bootstrap.css') }}" type="text/css" rel="stylesheet" />
<link href="{{ asset('public/css/estilo.css') }}" type="text/css" rel="stylesheet" />
<link href="{{ asset('public/css/dataTables.bootstrap.min.css') }}" type="text/css" rel="stylesheet" />
<link href="{{ asset('public/css/bootstrap-select.min.css') }}" type="text/css" rel="stylesheet" />
<link href="{{ asset('public/css/datepicker.css') }}" type="text/css" rel="stylesheet" />
<link href="{{ asset('public/css/dist/bootstrap-clockpicker.css') }}" type="text/css" rel="stylesheet" />
{% endblock %}
{% block header %}
{% set empresa = app.session.get('empresa') %}
<div class="container header">
<div class="row">
<div class="col-md-2">
{% set env = app.environment %}
{% if env == 'dev' %}
{% set app_link = 'app_dev.php' %}
{% else %}
{% set app_link = 'app.php' %}
{% endif %}
<a href="{{ asset(app_link) }}">
<img src="{{ asset('public/images/logo.png') }}" class="img-responsive"/>
</a>
</div>
<div class="col-md-10">
<h3 class="text-gray">Sistema de planificación de recursos empresariales</h3>
<p class="text-gray" style="font-size: 1.1em;">Centuria version 1.1<br>
{% if empresa|length %}
{{ empresa }}
{% else %}
Potenciado por Symfony 3.4
{% endif %}
</p>
</div>
</div>
<div class="top_div_line"> </div>
</div>
{% endblock %}
{% block body %}
<div id="layout" class="container">
<div class="row">
<div class="col-md-12">
{% block content %}
{% endblock %}
</div>
</div>
</div>
<div class="container footer navbar-default navbar-fixed-bottom">
<p class="text-center text-gray">Derechos de copia reservados &copy MG Datos</p>
</div>
{% endblock %}
{% block javascripts %}
<script src="{{ asset('public/js/jquery-3.2.1.min.js') }}" type="text/javascript"></script>
<script src="{{ asset('public/js/bootstrap.min.js') }}" type="text/javascript"></script>
<script src="{{ asset('public/js/jquery.dataTables.min.js') }}" type="text/javascript"></script>
<script src="{{ asset('public/js/dataTables.bootstrap.min.js') }}" type="text/javascript"></script>
<script src="{{ asset('public/js/bootstrap-select.min.js') }}" type="text/javascript"></script>
<script src="{{ asset('public/js/bootstrap-datepicker.js') }}" type="text/javascript"></script>
<script src="{{ asset('public/js/dist/bootstrap-clockpicker.js') }}" type="text/javascript"></script>
<script>
$(document).ready(function() {
$('#ftable').DataTable({
stateSave: true,
language: {
"emptyTable": "No hay datos disponibles en la tabla",
"info": "Mostrando _START_ hasta _END_ de _TOTAL_ registros",
"infoEmpty": "Mostrando 0 hasta 0 de 0 registros",
"lengthMenu": "Mostrar _MENU_ registros",
"search": "Buscar:",
"loadingRecords": "Cargando...",
"processing": "Procesando...",
"paginate": {
"first": "Primero",
"last": "Ultimo",
"next": "Siguiente",
"previous": "Anterior"
},
"infoFiltered": "(filtrados de _MAX_ registros)"
}
});
$('#ftable2').DataTable({
stateSave: true,
language: {
"emptyTable": "No hay datos disponibles en la tabla",
"info": "Mostrando _START_ hasta _END_ de _TOTAL_ registros",
"infoEmpty": "Mostrando 0 hasta 0 de 0 registros",
"lengthMenu": "Mostrar _MENU_ registros",
"search": "Buscar:",
"loadingRecords": "Cargando...",
"processing": "Procesando...",
"paginate": {
"first": "Primero",
"last": "Ultimo",
"next": "Siguiente",
"previous": "Anterior"
},
"infoFiltered": "(filtrados de _MAX_ registros)"
}
});
$('.selectpicker').selectpicker({
size: 8
});
$('.datepicker').datepicker({
format: 'dd-mm-yyyy',
autoclose: true
});
$.datepicker.regional['es'] = {
closeText: 'Cerrar',
prevText: '<Ant',
nextText: 'Sig>',
currentText: 'Hoy',
monthNames: ['Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre'],
monthNamesShort: ['Ene', 'Feb', 'Mar', 'Abr', 'May', 'Jun', 'Jul', 'Ago', 'Sep', 'Oct', 'Nov', 'Dic'],
dayNames: ['Domingo', 'Lunes', 'Martes', 'Miércoles', 'Jueves', 'Viernes', 'Sábado'],
dayNamesShort: ['Dom', 'Lun', 'Mar', 'Mié', 'Juv', 'Vie', 'Sáb'],
dayNamesMin: ['Do', 'Lu', 'Ma', 'Mi', 'Ju', 'Vi', 'Sá'],
weekHeader: 'Sm',
dateFormat: 'dd/mm/yy',
firstDay: 1,
isRTL: false,
showMonthAfterYear: false,
yearSuffix: ''
};
$.datepicker.setDefaults($.datepicker.regional['es']);
$('.clockpicker').clockpicker();
} );
</script>
{% endblock %}
And this is my new.html.twig:
{% extends '#CenturiaMain/Default/index.html.twig' %}
{% block content %}
{% block centuria_menu %}
{{ parent() }}
{% endblock %}
<div class = "main container">
<div class = "row well col-md-12" style = margin-bottom:55px;">
<h3>Nuevo registro</h3>
{{ form_start(form) }}
<div class = "form-group col-md-4">
{{ form_widget(form.idCambiosplanilla, { 'attr':{'hidden':'hidden'} }) }}
<div class = "form-group col-md-12">
<h5>Fecha</h5>{{ form_widget(form.fecha, { "attr" : {"class" : "form-control datepicker"}}) }}
<span class = "text-danger">{{ form_errors(form.fecha) }}</span>
</div>
<div class="input-group clockpicker " data-autoclose="true">
<span class="input-group-addon">
<span class="glyphicon glyphicon-time"></span>
</span>
<input type="text" class="form-control" value="09:30" />
</div>
<div class = "form-group col-md-6">
<h5>Horario: Desde</h5>{{ form_widget(form.horarioInicio, { "attr" : {"class" : "form-control clockpicker-control"}}) }}
<span class = "text-danger">{{ form_errors(form.horarioInicio) }}</span>
</div>
<div class = "form-group col-md-6">
<h5>Hasta</h5>{{ form_widget(form.horarioFinal) }}{{ form_widget(form.horarioFinal) }}
<span class = "text-danger">{{ form_errors(form.horarioFinal) }}</span>
</div>
<br>
<a href="{{ path('centuria_cambiosplanilladetalle', {'idCd' : 1}) }}"
class = "btn btn-warning glyphicon glyphicon-arrow-left"></a>
<input type="submit" class="btn btn-success glyphicon glyphicon-ok" value="" />
</div>
{{ form_end(form) }}
</div>
</div>
{% endblock %}
I have followed the instrucctions to use clockpicker, In fact I have created a simple HTML file and It works, But I can not make it work in my symfony application, do you have any ideas regarding my code, what I am possible doing wrong?
This is my form:
<?php
namespace Centuria\MainBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\TimeType;
class CambiosplanilladetalleType extends AbstractType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('idCambiosplanilla')->add('idEmpleado')->add('fecha', TextType::class)->add('horarioInicio', TimeType::class)->add('horarioFinal')->add('extraInicio')->add('extraFinal')->add('tiempoTotal')->add('idAsignacion')->add('fechaRegistro')->add('horaRegistro')->add('idUsuario');
}/**
* {#inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Centuria\MainBundle\Entity\Cambiosplanilladetalle'
));
}
/**
* {#inheritdoc}
*/
public function getBlockPrefix()
{
return 'centuria_mainbundle_cambiosplanilladetalle';
}
}
And It appears like this:
Thank you in advice.
In your form type class, you need to add the "clockpicker" css class to the field, like so
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('idCambiosplanilla')
->add('idEmpleado')
->add('fecha', TextType::class)
->add('horarioInicio', TimeType::class, [
'attr' => [
'class' => 'form-control clockpicker-control clockpicker',
]
])
->add('horarioFinal', )
->add('extraInicio')
->add('extraFinal')
->add('tiempoTotal')
->add('idAsignacion')
->add('fechaRegistro')
->add('horaRegistro')
->add('idUsuario');
}
alternatively, in your form twig, you could also set the "clockpicker" class, like so:
<div class = "form-group col-md-6">
<h5>Horario: Desde</h5>{{ form_widget(form.horarioInicio, { "attr" : {"class" : "form-control clockpicker-control clockpicker"}}) }}
<span class = "text-danger">{{ form_errors(form.horarioInicio) }}</span>
</div>

Form theme in Twig / Symfony

With Symfony and Twig, I have this form and this form theme :
TrainingVersionType (this is collection) :
class TrainingVersionType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name', null, [
'attr' => [
'autofocus' => true,
],
'label' => 'label.training_name',
'ico' => 'pencil',
'translation_domain' => 'administration',
])
->add('document', FineUploaderType::class, [
'label' => 'label.attachments',
'translation_domain' => 'administration',
'limit' => 1,
'acceptFiles' => [
'image/jpeg',
'image/png',
],
'allowedExtensions' => [
'jpg',
'jpeg',
'png',
],
])
;
}
Form theme in my Twig file :
{% block _training_versions_entry_name_widget %}
<strong style="background: red; padding: 20px">
Ok 1 :)
</strong>
{% endblock %}
{% block _training_versions_entry_document_widget %}
<strong style="background: blue; padding: 20px">
Ok 2 :)
</strong>
{% endblock %}
Why I have "OK 1" and I don't have "OK 2" ?
In my config.yml, I have this global form theme, But this is Widget for "FineUploaderType" :
{% block fineuploader_widget %}
<div class="qq-uploader-selector card mb-3 bg-light">
<div class="card-body">
<div class="qq-upload-button-selector">
<span class="btn btn-info">
{{ 'action.select'|trans({}, 'fineuploader') }}
</span>
</div>
</div>
</div>
....
{% endblock %}
I want to add div in "document" entry in the form, for add help after FineUploader widget.
Can you help me ? :) Thanks
Edit :
administration/training/new.html.twig :
{% form_theme form 'administration/training/_form_theme.html.twig' %}
{% block content %}
{{ include('default/page/_new.html.twig', {
form: form,
btn_ico: 'plus',
btn_value: 'action.training_new'|trans({}, 'administration'),
back_label: 'action.training_back'|trans({}, 'administration'),
back_path: path('administration_training')
},
with_context = false
) }}
{% endblock %}
administration/training/_form_theme.html.twig :
{% block _training_versions_entry_name_widget %}
<strong style="background: red; padding: 20px">
Ok 1 :)
</strong>
{% endblock %}
{% block _training_versions_entry_document_widget %}
<strong style="background: blue; padding: 20px">
Ok 2 :)
</strong>
{% endblock %}

Resources