Goal
Let's assume that we have a json array having a nested structure of objects. Typically something that you manage with a JsonMenuEditorFieldType field in elasticms. Those object can be of any kind with their specifics fields.
The first goal is to recursively generate a HTML DOM out of this structure with a Twig template.
After that, how can we apply a specific Twig template in function of the type of the object.
Think object
You have to consider a Twig template as an object in an object-oriented way. Indeed blocks
can be seen as class's functions and with the extends
you can inherit from another. In this approach the Twig context is you class's members. In this post I'll define one Twig's class by type of object in my JSON structure. All those classes inherits from an 'abstract' block.html.twig
Twig template. That abstract class will implement those functions:
- render: generates the DOM from a structure
- row: generates the DOM for the current object
- title: generates the object title's DOM
- widget: generates the object content's DOM
- children: generates the object children's DOM
To call a specific Twig template's block with a specific Twig context we need to combine with
and block
.
{% with {
'structure': source.blocks|default("{}")|ems_json_decode,
'trans_default_domain': trans_default_domain,
} %}
{{ block("children", "@EMSCH/template/blocks/abstract-block.html.twig") }}
{% endwith %}
Like that, Twig's blocks can be recursively called in a more elegant way that using macros.
Notice that that I also provide the trans_default_domain variable.
Recursively generate DOM
This block receives the array of objects and have to initialize the recursive rendering for each of them. With the right Twig context:
- item: the object
- childrenHaveBeenGenerated: a variable set to true if the children block has been called
- currentBlockLevel: a variable with the object level in the structure (from 0)
- headerBlockLevel: the header tag (H2, H3, ...) to generate if a label is defined in the current object (starting at 2)
{% block render %}
{% spaceless %}
{% set childrenHaveBeenGenerated = false %}
{% set currentBlockLevel = 1 %}
{% set headerBlockLevel = 2 %}
{% for item in structure|default([]) %}
{% with {
'item': item,
'currentBlockLevel': currentBlockLevel,
'headerBlockLevel': headerBlockLevel,
'headerBlockLevel': headerBlockLevel,
'trans_default_domain': trans_default_domain,
} %}
{{ block("row") }}
{% endwith %}
{% endfor %}
{% endspaceless %}
{% endblock render %}
The trans_default_domain
variable is still passed to the block sub-templates.
The row block will be called for each object:
{% block row %}
{% spaceless %}
<div class="{{ item.object.class|default('pt-4 px-3 px-lg-4') }}" id="row-{{ item.id }}">
{{ block('title') }}
{{ block('widget') }}
{% if item.children|default([])|length > 0 %}
{{ block('children') }}
{% endif %}
</div>
{% endspaceless %}
{% endblock row %}
The title
block is generating a header tag if a label is defined:
{% block title %}
{% spaceless %}
{% if item.object.label is defined %}
<h{{ headerBlockLevel }} class="h{{ headerBlockLevel+1 }} mb-4 mt-2">{{ item.object.label|default('') }}</h{{ headerBlockLevel }}>
{% endif %}
{% endspaceless %}
{% endblock title %}
The widget
is the abstract method (it has to be overridden):
{% block widget %}
<p>{{ ('Block `widget` must be overridden for. `' ~ item.type ~ '`')|ems_markdown }}</p>
{% endblock widget %}
And finally the children
block wich initiate the context for all object's children:
{% block children %}
{% spaceless %}
{% if not childrenHaveBeenGenerated %}
{% set currentBlockLevel = currentBlockLevel + 1 %}
{% set headerBlockLevel = headerBlockLevel + (item.object.label is defined ? 1 : 0) %}
{% for item in item.children|default([]) %}
{{ block("row") }}
{% endfor %}
{% set childrenHaveBeenGenerated = true %}
{% endif %}
{% endspaceless %}
{% endblock %}
The variable childrenHaveBeenGenerated
is used in order to avoid that the children are generated twice.
Customize by type
Let's update the children
block in order to call a template based on the object's type:
{% block children %}
{% spaceless %}
{% if not childrenHaveBeenGenerated %}
{% set currentBlockLevel = currentBlockLevel + 1 %}
{% set headerBlockLevel = headerBlockLevel + (item.object.label is defined ? 1 : 0) %}
{% for item in item.children|default([]) %}
{{ block("row", "@EMSCH/template/blocks/"~item.type~".html.twig") }}
{% endfor %}
{% set childrenHaveBeenGenerated = true %}
{% endif %}
{% endspaceless %}
{% endblock %}
And the render and the row block:
{% block render %}
{% spaceless %}
{% set childrenHaveBeenGenerated = false %}
{% set currentBlockLevel = 1 %}
{% set headerBlockLevel = 2 %}
{% for item in structure|default([]) %}
{% with {
'item': item,
'currentBlockLevel': currentBlockLevel,
'headerBlockLevel': headerBlockLevel,
'headerBlockLevel': headerBlockLevel,
'trans_default_domain': trans_default_domain,
} %}
{{ block("row", "@EMSCH/template/blocks/"~item.type~".html.twig") }}
{% endwith %}
{% endfor %}
{% endspaceless %}
{% endblock render %}
{% block row %}
{% spaceless %}
<div class="{{ item.object.class|default('pt-4 px-3 px-lg-4') }}" id="row-{{ item.id }}">
{{ block('title', "@EMSCH/template/blocks/"~item.type~".html.twig") }}
{{ block('widget', "@EMSCH/template/blocks/"~item.type~".html.twig") }}
{% if item.children|default([])|length > 0 %}
{{ block('children', "@EMSCH/template/blocks/"~item.type~".html.twig") }}
{% endif %}
</div>
{% endspaceless %}
{% endblock row %}
If our object item
has a markdown field, it's Twig may looks like this:
{% extends '@EMSCH/template/blocks/abstract-block.html.twig' %}
{% block widget %}
{{ item.object.body|default('')|ems_markdown|emsch_routing }}
{% endblock %}
So you can also call the parent function:
{% block row %}
{% spaceless %}
{% if item.object.hr|default(false) %}
<hr class="d-print-none">
{% endif %}
{{ parent() }}
{% endspaceless %}
{% endblock row %}
{% block widget %}{% endblock widget %}
Reuse a block
Hopefully you can reuse a block outside a structure context:
{% with {
'item': {
id: 'blog-template',
type: 'last_updates',
object: {
title: 'blog.last_update',
limit: 10,
}
}
} %}
{{ block("row", "@EMSCH/template/blocks/last_updates.html.twig") }}
{% endwith %}
Define a translation domain
If you want to use a skeleton translation inside your blocks you have to specify, inside your blocks, the skeleton translation domain (which is in fact your environment alias name):
{% extends '@EMSCH/template/blocks/abstract-block.html.twig' %}
{% trans_default_domain trans_default_domain %}
{% block widget %}
{% set result = emsch_search('page', {
"query": {
"term": {
"template": "blog"
}
},
"sort": {
"published_date": {
"order": "desc",
"missing": "_last",
"unmapped_type": "long"
}
}
}, 0 , item.object.limit|default(10)) %}
<ul>
{% for item in result.hits.hits %}
<li><a lang="{{ item._source.locale }}" href="{{ path('match_all', {
path:item._source.path,
_locale:item._source.locale,
}) }}">{{ item._source.title }}</a>
{% if app.request.locale != item._source.locale %}
[{{ ('website.content_in.'~item._source.locale)|trans }}]
{% endif %}
</li>
{% endfor %}
</ul>
{% endblock %}
As those blocks are loaded outside of the request; the trans_default_domain
variable is unknowed outside of the twig blocks. So some block template, like this one, will trigged an error:
{% extends '@EMSCH/template/blocks/abstract-block.html.twig' %}
{% trans_default_domain trans_default_domain %}
{% block widget %}
{{ 'foobar'| trans }}
{% endblock %}
Last posts
- Draw.io on a website
- Deploy elasticms on AWS
- Intégrer BOSA Accessibility Check dans un site web [Content in French]
- PHP - Convert Human Readable Size to Bytes
- Composer: How to use a specific branch acting like a specific revision in composer dependencies
- Stream a CSV from a JSON array
- Comment utiliser les commandes "locales" du skeleton [Content in French]
- How to extract data from a JsonMenuNestedEditorField
- Backup on AWS glacier
- Refer to environment variables in twig