Append content to block from multiple subtemplates - symfony

there are several questions on how to append a block with twig. The answer is always using inheritance and use and then call the parent(). Somehow I don't know how this works in my specific case:
base.html.twig
{% block content %}{% endblock %}
{% block appendable %}
{% endblock %}
{% block another_appendable %}
{% endblock %}
site.html.twig
{% extends base.html.twig %}
{% block content %}
{# Here use/include/embed, i dont know #}
{% use sub1.html.twig %}
{% use sub2.html.twig %}
{% endblock content %}
sub1.html.twig
Some content that should be directly rendered
{% block appendable %}
some stuff that should be added to appendable
{% endblock %}
{% block another_appendable %}
This content should be added to "another appendable"
{% endblock %}
sub2.html.twig
{% block appendable %}
additional stuff that should be appended
{% endblock %}
I would like that both contents from sub1 and sub2 are rendered within appendable. How could I achieve that?

Let's go. I had this same problem and this solution works for me:
base.html.twig
{% block content %}{% endblock %}
site.html.twig
{% extends base.html.twig %}
{% use sub1.html.twig with appendable as appendableContent, another_appendable as another_appendableContent %}
{% block content %}
{% block appendable -%}
{{ block('appendableContent') }}
{% endblock %}
{% block another_appendable -%}
{{ block('another_appendableContent') }}
{% endblock %}
{% endblock %}
sub1.html.twig
{% use sub2.html.twig with appendable as appendableContentAlternative %}
{% block appendable %}
some stuff that should be added to appendable<br/><br/>
{{ block('appendableContentAlternative') }}
{% endblock %}
{% block another_appendable %}
This content should be added to "another appendable"<br/><br/>
{% endblock %}
sub2.html.twig
{% block appendable %}
additional stuff that should be appended<br/><br/>
{% endblock %}
According my research this technique is called "horizontal reuse", and here's the doc:
http://twig.sensiolabs.org/doc/tags/use.html

To include a template, you need to use the include keyword, not the use keyword:
{% block appendable %}
{# Assuming your sub1 template is in AcmeDemoBundle/Resources/views/MySub/sub1.html.twig #}
{% include "AcmeDemoBundle:MySub:sub1.html.twig" %}
{% endblock appendable %}
AcmeDemoBundle:MySub:sub1.html.twig could look like this:
<b>Put directly your code there, no need to use the blocks.</b>
Using Inheritance
If you wish, you can use the {{ parent() }} keyword to use inheritance. For example, if you want to include sub1.html.twig by default but append sub2.html.twig in your child template, you can do the following:
base.html.twig
{% block content %}
{% include "AcmeDemoBundle:MySub:sub1.html.twig" %}
{% endblock %}
site.html.twig
{% extends base.html.twig %}
{% block content %}
{# render what happens in the parent content block #}
{{ parent() }}
{# and append sub2.html.twig as well #}
{% include "AcmeDemoBundle:MySub:sub2.html.twig" %}
{% endblock content %}

I'd like to share my own solution for this problem. I implemented my own Twig extension, which implements custom tag widget (I used Twig tag embed as a source).
Extension
WidgetNode.php:
namespace Artprima\Bundle\TwigWidgetTagBundle\Twig\Extension;
/**
* Class WidgetNode
*
* #author Denis V
*
* #package Artprima\Bundle\TwigWidgetTagBundle\Twig\Extension
*/
class WidgetNode extends \Twig_Node_Include
{
// we don't inject the module to avoid node visitors to traverse it twice (as it will be already visited in the main module)
public function __construct($filename, $index, \Twig_Node_Expression $variables = null, $only = false, $ignoreMissing = false, $lineno, $tag = null)
{
parent::__construct(new \Twig_Node_Expression_Constant('not_used', $lineno), $variables, $only, $ignoreMissing, $lineno, $tag);
$this->setAttribute('filename', $filename);
$this->setAttribute('index', $index);
}
/**
* Compiles the node to PHP.
*
* #param $compiler \Twig_Compiler A Twig_Compiler instance
*/
public function compile(\Twig_Compiler $compiler)
{
$compiler->addDebugInfo($this);
if ($this->getAttribute('ignore_missing')) {
$compiler
->write("try {\n")
->indent()
;
}
$this->addGetTemplate($compiler);
$compiler->raw('->displayBlock(');
$compiler->string('widget');
$compiler->raw(', ');
$this->addTemplateArguments($compiler);
$compiler->raw(");\n");
if ($this->getAttribute('ignore_missing')) {
$compiler
->outdent()
->write("} catch (Twig_Error_Loader \$e) {\n")
->indent()
->write("// ignore missing template\n")
->outdent()
->write("}\n\n")
;
}
}
protected function addGetTemplate(\Twig_Compiler $compiler)
{
$compiler
->write("\$this->env->loadTemplate(")
->string($this->getAttribute('filename'))
->raw(', ')
->string($this->getAttribute('index'))
->raw(")")
;
}
}
WidgetTokenParser.php:
namespace Artprima\Bundle\TwigWidgetTagBundle\Twig\Extension;
/**
* Class WidgetTokenParser
*
* #author Denis V
*
* #package Artprima\Bundle\TwigWidgetTagBundle\Twig\Extension
*/
class WidgetTokenParser extends \Twig_TokenParser_Include
{
/**
* Parses a token and returns a node.
*
* #param \Twig_Token $token A Twig_Token instance
*
* #return \Twig_NodeInterface A Twig_NodeInterface instance
*/
public function parse(\Twig_Token $token)
{
$stream = $this->parser->getStream();
$parent = $this->parser->getExpressionParser()->parseExpression();
list($variables, $only, $ignoreMissing) = $this->parseArguments();
// inject a fake parent to make the parent() function work
$stream->injectTokens(array(
new \Twig_Token(\Twig_Token::BLOCK_START_TYPE, '', $token->getLine()),
new \Twig_Token(\Twig_Token::NAME_TYPE, 'extends', $token->getLine()),
new \Twig_Token(\Twig_Token::STRING_TYPE, '__parent__', $token->getLine()),
new \Twig_Token(\Twig_Token::BLOCK_END_TYPE, '', $token->getLine()),
));
$module = $this->parser->parse($stream, array($this, 'decideBlockEnd'), true);
// override the parent with the correct one
$module->setNode('parent', $parent);
$this->parser->embedTemplate($module);
$stream->expect(\Twig_Token::BLOCK_END_TYPE);
return new WidgetNode($module->getAttribute('filename'), $module->getAttribute('index'), $variables, $only, $ignoreMissing, $token->getLine(), $this->getTag());
}
public function decideBlockEnd(\Twig_Token $token)
{
return $token->test('endwidget');
}
/**
* Gets the tag name associated with this token parser.
*
* #return string The tag name
*/
public function getTag()
{
return 'widget';
}
}
TemplateTagsExtension.php:
namespace Artprima\Bundle\TwigWidgetTagBundle\Twig\Extension;
/**
* Class TemplateTagsExtension
*
* #author Denis V
*
* #package Artprima\Bundle\TwigWidgetTagBundle\Twig\Extension
*/
class TemplateTagsExtension extends \Twig_Extension
{
/**
* #inheritdoc
*/
public function getTokenParsers()
{
return array(
new WidgetTokenParser(),
);
}
/**
* Returns the name of the extension.
*
* #return string The extension name
*/
public function getName()
{
return 'template_tags';
}
}
services.yml:
parameters:
artprima.twig.extension.template_tags.class: Artprima\Bundle\TwigWidgetTagBundle\Twig\Extension\TemplateTagsExtension
services:
artprima.twig.extension.template_tags:
class: %artprima.twig.extension.template_tags.class%
tags:
- { name: twig.extension }
Usage example
views/Blocks/widget.html.twig:
{# please note, that only "widget" block is rendered, all other blocks can be used inside the "widget" block #}
{# if you don't define the "widget" block, nothing will be rendered #}
{% block widget %}
<div class="{{ block('widget_box_class') }}">
{{ block('widget_header') }}
{{ block('widget_body') }}
</div>
{% endblock %}
{% block widget_header %}
<div class="{{ block('widget_header_class') }}">
{{ block('widget_title') }}
{% if display_toolbar is defined and display_toolbar %}{{ block('widget_toolbar') }}{% endif %}
</div>
{% endblock %}
{% block widget_body %}
<div class="{{ block('widget_main_class') }}">
{{ block('widget_main') }}
</div>
{% endblock %}
{% block widget_title %}
<h5 class="widget-title">{{ block('widget_title_text') }}</h5>
{% endblock %}
{% block widget_title_text %}(undefined){% endblock %}
{% block widget_toolbar %}
<div class="widget-toolbar">
{{ block('widget_toolbar_inner') }}
</div>
{% endblock %}
{% block widget_toolbar_inner %}{% endblock %}
{% block widget_box_class %}{% spaceless %}widget-box{% endspaceless %}{% endblock %}
{% block widget_main_class %}{% spaceless %}widget-main{% endspaceless %}{% endblock %}
{% block widget_main %}{% endblock %}
{% block widget_header_class %}{% spaceless %}widget-header{% endspaceless %}{% endblock %}
views/Dashboard/widgets/sample.html.twig
{% widget "ArtprimaSampleBundle:Blocks:widgets.html.twig" %}
{% block widget_title_text %}{{ "widget.records_creation_history"|trans }}{% endblock %}
{% block widget_main_class %}{% spaceless %}no-padding {{ parent() }}{% endspaceless %}{% endblock %}
{% block widget_main %}
<table class="table table-striped table-bordered table-hover no-margin-bottom">
<thead>
<tr>
<th>Description</th>
<th>Amount</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{ "widget.number_of_countries.created"|trans }}</td>
<td>{{ dashboard.countries.created }}</td>
</tr>
<tr>
<td>{{ "widget.number_of_users.created"|trans }}</td>
<td>{{ dashboard.users.created }}</td>
</tr>
</tbody>
</table>
{% endblock %}
{% endwidget %}
Summary
So, as you can see, with my extension, it is possible to include a template an re-use the blocks within it. If you need multiple widgets, you can have multiple widget tags in your template using the same source template and the block content will not overlap. Essentially, it works like embedding the template using Twig's embed (and I used this tag as a source for my extension), but with the only (and major) difference - it renders ONLY the block called "widget". All other blocks are ignored but can be used inside the "widget" block.

If you only define blocks in the sub templates, you may utilize the block function and substitute explicitly:
base.html.twig
{% block content %}{% endblock %}
{% block appendable %}{% endblock %}
{% block another_appendable %}{% endblock %}
site.html.twig
{% extends base.html.twig %}
{% block appendable %}
{{ block('appendable', 'sub1.html.twig') }}
{{ block('appendable', 'sub2.html.twig') }}
{% endblock %}
{% block another_appendable %}
This content should be added to "another appendable"
{{ block('another_appendable', 'sub1.html.twig') }}
{% endblock %}
sub1.html.twig
{% block appendable %}
some stuff that should be added to appendable
{% endblock %}
{% block another_appendable %}
This content should be added to "another appendable"
{% endblock %}
sub2.html.twig
{% block appendable %}
additional stuff that should be appended
{% endblock %}

Related

Influence the twig included file on parent

I have a problem like this
But i can not use {% use %}
for example:
{# file1.twig #}
{% extends "main.twig" %}
{% block content %}content text{% endblock %}
{# main.twig #}
{% block content %}{% endblock %}
{% for widget in widgets %}
{% embed widget %}{% endembed %}
{% endfor %}
{% block js %}{% endblock %}
{# widget1.twig #}
{% block js %}
script1
{% endblock %}
{# widget2.twig #}
{% block js %}
script1
{% endblock %}
I want to be the result:
content text
script1
script2
I have a lot of files like file1.twig and widget1.twig
I can not use all the widgets in the all files.
Also, {{ parent() }} does not work for me
Is there another way?

Twig javascript in included child

I have a question regarding adding javascript to child elements. How do you do that?
I have this setup:
base.html.twig:
{% block content %}
{% endblock content %}
{% block script %}
{% endblock script %}
index.html.twig:
{% extends base.html.twig %}
{% include intro.html.twig %}
{% block content %}
<html></html>
{% endblock content %}
{% block script %}
<script></script>
{% endblock script %}
intro.html.twig:
{% block script %}
<script></script>
{% endblock script %}
I want to add more javascript files into the intro.html.twig file, but it doesn't append it to the script block in index.html.twig.
All help appreciated!
UPDATE
I want to send some parameters with intro.html.twig:
{% extends 'intro.html.twig' with {
'title': 'title',
'type': 'test',
} %}
is this possible using extends, or can I only use with with include?
index.html.twig
{% extends intro.html.twig %}
{% block content %}
<html></html>
{% endblock content %}
{% block script %}
{{ parent() }}
<script></script>
{% endblock script %}
intro.html.twig
{% extends base.html.twig %}
{% block script %}
<script></script>
{% endblock script %}

Template does not always see its variables

I will first present to you the incriminated templates.
To begin with, I have my layout template with the JQuery. To avoid multiple "$(document).ready()" I placed a block after the general JavaScript.
{# layout.html.twig #}
<script type="text/javascript">
$(document).ready(function(){
//Some JS
{% block jquery %}
{% endblock %}
});
</script>
{# Some HTML & JS #}
<body>
<!-- Some HTML -->
<div>
{% block content %}
{% endblock %}
</div>
<!-- Some more HTML -->
</body>
It is extended by the template layout_cart.html.twig.
{# layout_cart.html.twig #}
{% extends 'AppBundle::layout.html.twig' %}
{% block content %}
<h1>{% block titre %}{% endblock %}</h1>
<div id="content-container">
{% block subcontent %}
{% endblock %}
</div>
{% endblock %}
Which is extended itself by panier.html.twig
{# panier.html.twig #}
{% extends AppBundle:layout_cart.html.twig #}
{# Some code #}
{% block jquery %}
// Some JavaScript
{% for produit in produits %}
// Generating some JavaScript
{% endfor %}
{{ dump(produits) }}
{% endblock %}
{% block subcontent %}
{% if produits is defined and produits|length > 0 %}
{{ dump(produits) }}
{# Interacting with the variables #}
{% endif %}
{% endblock %}
The dump in the jquery renders this :
<pre class='xdebug-var-dump' dir='ltr'>
<b>array</b> <i>(size=0)</i>
<i><font color='#888a85'>empty</font></i>
</pre>
While in the subcontent it renders my objects collection.
Am I missing something ?

Twig combine multiple blocks using USE

I have a problem and I'm trying to see if anyone has a solution for it.
I have a twig template that extends base:
{% extends "base.html" %}
{% use "element1.html" %}
{% use "element2.html" %}
{% block css %}
{{ parent() }}
{% endblock %}
{% block body %}{% endblock %}
{% block javascript %}
{{ parent() }}
{% endblock %}
element1.html and element2.html are almost the same
{# element1.html #}
{% block css %}
some css...
{% endblock %}
{% block body %}
some body html
{% endblock %}
{% block javascript %}
some javascript...
{% endblock %}
When the code runs, element2 overwrites element1, Is there a way to combine blocks, just like parent() combines base blocked with the main template?
Please name the blocks of element1 and element2 appropriately so that it does not override the other.
{% use "element1.html" with css as element1_css, body as element1_body, javascript as element1_js %}
{% use "element2.html" with css as element2_css, body as element2_body, javascript as element2_js %}
And now use appropriate blocks from element1 or element2 such as
{% block element1_css %}{% endblock element1_css %}

Custom form field template with twig

I'd like to create a custom template in twig to render a form field.
Example:
{{ form_row(form.field) }}
This can be overriden by form theming
{% block form_row %}
... custom code
{% endblock form_row %}
What I would like to do is this:
{% block custom_row %}
... custom code
{% endblock custom_row %}
and use it like this:
{{ custom_row(form.field }}
however, this throws an exception that method custom_row is not found.
My understanding is that this can be done with Twig extension, but I don't know how to register a block to be a function.
Update
what I actually want:
I use twitter bootstrap and a bundle which overrides all the form themes. And it renders a div around a radio, so it can't be inlined. So I wanted to do something like this:
copy their template and get rid of the div:
{% block inline_radio_row %}
{% spaceless %}
{% set col_size = col_size|default(bootstrap_get_col_size()) %}
{% if attr.label_col is defined and attr.label_col is not empty %}
{% set label_col = attr.label_col %}
{% endif %}
{% if attr.widget_col is defined and attr.widget_col is not empty %}
{% set widget_col = attr.widget_col %}
{% endif %}
{% if attr.col_size is defined and attr.col_size is not empty %}
{% set col_size = attr.col_size %}
{% endif %}
{% if label is not sameas(false) %}
{% if not compound %}
{% set label_attr = label_attr|merge({'for': id}) %}
{% endif %}
{% if required %}
{% set label_attr = label_attr|merge({'class': (label_attr.class|default('') ~ ' required')|trim}) %}
{% endif %}
{% if label is empty %}
{% set label = name|humanize %}
{% endif %}
{% set label_attr = label_attr|merge({'class': (label_attr.class|default('') ~ ' radio-inline')|trim}) %}
<label{% for attrname, attrvalue in label_attr %} {{ attrname }}="{{ attrvalue }}"{% endfor %}>
{{ block('radio_widget') }}
{{ label|trans({}, translation_domain) }}
</label>
{% else %}
{{ block('radio_widget') }}
{% endif %}
{{ form_errors(form) }}
{% endspaceless %}
{% endblock inline_radio_row %}
and then
{{ inline_radio_row(form.field) }}
I ended up just overriding the whole theme, and added ifs around the div in question, a the class (radio-inline). But I'm still wondering if there's a way to make this work. Seems like it makes you work so hard for something so simple.
Update 2
I found the functionality:
class FormExtension extends \Twig_Extension
{
public function getFunctions()
{
return array(
'inline_radio_row' => new \Twig_Function_Node(
'Symfony\Bridge\Twig\Node\SearchAndRenderBlockNode',
array('is_safe' => array('html'))
),
);
}
}
This does exactly what I want, but it says it's deprecated. Anyone knows an updated version of how to use this?
Update 3
Similar functionality can be also achieved with http://twig.sensiolabs.org/doc/tags/include.html
You can use the twig functions for each part of a form row:
form_label(form.field)
form_widget(form.field)
form_errors(form.field)
For example:
<div class="form_row">
{{ form_label(form.field) }} {# the name of the field #}
{{ form_errors(form.field) }} {# the field #}
{{ form_widget(form.field) }} {# the errors associated to the field #}
</div>
You can use form theming.
Step by step:
1. Form Type Class
Check the name in your class
public function getName() {
return 'hrQuestionResponse';
}
2. Include a custom theme in your template
{% form_theme form 'InterlatedCamsBundle:Form:fields.html.twig' %}
3. Find the block
Can be quite difficult. For the bootstrap bundle, as you seem to have found it is in ./vendor/braincrafted/bootstrap-bundle/Braincrafted/Bundle/BootstrapBundle/Resources/views/Form/bootstrap.html.twig and you have found the block radio_row. I have been finding the block by putting output in the source template and overriding more blocks than I need. In 2.7 there is a theme 'rendering call graph'.
4. Override the block
Copy the block from the master template and call it replace the standard term with the name of in the FormType found in step 1.
Don't forget to also change the endblock name.
e.g.
{% block hrQuestionResponse_widget %}
hrQuestionResponse_row
{% spaceless %}
{% set class = '' %}
...
{% endspaceless %}
{% endblock hrQuestionResponse_widget %}
In your case because you can only call form_widget() you will need to override _widget. You could extract only the content that you need, or you can override the block chain to radio_row.

Resources