Make twig plugins by dynamically horizontal reusing via "use" (not "include") - plugins

Need: A dynamic template loaded from a DB
I need to present a JSON object via "template plugins" that will come from a database (defined outside the application itself). The plugin will be applied only if a plugin exists.
For this example, let's assume I have this object of type "Reservation" that contains a sub-object of type "Flight":
{
"id": "ABC-XYZ",
"reservationDate": "2020-09-23",
"state": "paid",
"flight":
{
"origin": "BCN",
"destination": "MAD",
"airline": "VY"
}
}
Rules
The controller will call a page template passing multiple objects.
The page template will "use" or "include" (see later more info) an object.html.twig and will display it.
The object.html.twig will do this:
If there's not any known plugin able to handle this type of object, the template will display a <pre> with the object converted to YAML. This will be mainly act as a "default" plugin or "fallback" plugin.
If there's a known plugin able to handle the object, the object will be sent to the plugin (which is not anything else than another twig template).
In this case, the plugin should be able to separate "parts" of "interpretable" results to make them "nice" and leave the rest into an object that will be, in turn, displayed again with the original "default" plugin.
If there are parts of the object that are in turn interpretable, they will be in turn passed to other plugins.
Example "desired" outputs
Sample 1. No plugin available
Sample 2. Plugin 'reservation' available. Interprets the state in green. Also removes the redundant data of the ID
Sample 3. Same than 2 but also plugin 'flight' available, able to process the flight block. Formats the texts and makes a lookup of the airline full-name
Sample 4. Plugin 'flight' available, able to process the flight, that in turn knows that the origin and the destination are "airports" and passes them into the corresponding nested 'airport' plugin, because they are "reusable" objects not only in the "flight" plugin but also in many other places in the application, so they are defined appart
What I have already explored
I know that doing it via 'include' it could work. But let's take a look at the differences between 'use' and 'include':
The major difference between include and use in twig is that:
When you include a template, it's direct HTML 'inserted there' where you can use the {{ }} operator for printing and {% %} for control flow (set variables, ifs, fors, etc.). The renderer will process it straight forward. But defining a new block via {% block myNiceBlock %} is forbidden.
When you 'use' a template, it's pre-loaded, and blocks are permitted. There's no rendering of the included block. Then, from the caller, you use a {{ block( 'whatever' ) }} to tell the renderer to go and render that specific block.
The 'include' is more rudimentary. The use allows horizontal reusing and allows itself to auto-organize itself with other sub-blocks called by the parent block, all in one single file.
For example, in the airports example, if there are N images, in a include you should put the wrapper HTML directly in the file, do a loop and inside the loop write the inner HTML.
Instead in the use approach you'd do an airport block which in turn loops over the images and just calls the block airportImage which is defined in another block in the same file, thus facilitating clean-coding.
Requirement
The application should not be re-deployed when new plugins are created. They must be loaded from a DB or any other dynamic system, as the plugins will be written by "users of the application" as they need it. No deploy allowed.
The plugins should be written in terms of a "block-able" twig template, so need to be 'use'-able.
Question
Discovering "which" plugin to call is not a problem. Assume that whoever (the controller, the twig itself, whoever really) can discover for this example that a "reservation" plugin exists somewhere. How can I use it from the page? When the reservation is rendering it "asks if a flight plugin" is available. If not, all to the YAML. If it is, how can then dynamically tell the reservation to use the flight?
In short: How do I force a template to dynamically use (not include) templates that, in turn, comes from the database (not from fixed files)?
NOTE: If this info is useful: I'm using Symfony 5 with webpack.
Thanks!

Related

How to use selector

Let's say, I have a template A and sling:resourceType is /apps/myproject/components/basePage. In this component I've body.html and header.html and footer.html script included through slightly in body.html.
Now I am creating another template B, and sling:resourceType is /apps/myproject/components/compB and sling:resourceSuperType of compB is /apps/myproject/components/basePage.
In /apps/myproject/components/compB I have added content.html and selector.html
If I create a page (mytest.html) of type template B, then header and footer script is included correctly but when I hit this mytest.selector.html then header and footer script is not included. I want template B will have two different view based on selector.
Please let me know where I am missing.
I believe you are trying to include multiple scripts within same template to achieve different views. This is correct approach todo in AEM. But missing part is the moment you create the second script (selector.html in this case), it becomes another template and you need to code to include your entire page scripts into this script as well.
When you override scripts from /libs/wcm/foundation/components/page component, they ll work fine when your custom script names matches to parent component. For example your body.html will override /libs/wcm/foundation/components/page/body.html and page will render how it is coded. When you create selector.html it becomes independent script as there is no /libs/wcm/foundation/components/page/selector.html.
You need to define all behavior (to include header, footer script etc) explicitly against your custom script. In this case you need include header/footer scripts explicitly into your selector.html
Using a selector means that you're using some special implementation of your component. For instance, your component may have several charts and you want to encapsulate those in your selectors and use it through AJAX from browser, and reuse those selectors in your main component as well.
Currently, you're trying to achieve is to use your header and footer to another component which breaks the encapsulation rule. Rather do this, take out your header.html and footer.html and make those individual components and you it in your basePage as well as your child pages.
See the snippet below:
<div data-sly-resource="${'header' # resourceType='/apps/myproject/components/header'}">
<p>Your body and anything you want to put here</p>
<div data-sly-resource="${'footer' # resourceType='/apps/myproject/components/footer'}">
This way, you can reuse your headers whereever you want even in your selectors.

Drupal 8 form and view in block

I am very new Drupal, In a block I would like to have a form with select box and submit button. Each options in the link is link of content page. That is when the user select an option and click the submit button it will redirect to other node page.
For the above requirement client used Web Form for Drupal 7 and I would like to clone the requirement for Drupal 8. I tried EForm and I am able to create a form but I can not able to show the form in Block Layout and View.
I am not sure the module Eform is suitable for my requirement.
Can you guys please help me what modules do I need to install for the above requirement in Drupal 8.
Okay, I don't know if there are any modules out there which do exactly what you want to, but you can build your own custom solution. In my eyes there are two main possibilities:
create a custom block type with a HTML body field, put your HTML in there and you're done. Advantage: easy to do, Disadvantage: hardcoded
clean way: create a new node type and/or a new category, which you will use for your country nodes. Then you'll create a block programmatically and query for all nodes of that certain country type or for nodes with the "country page"-category, whatever you use to organize those nodes. Then you just create a form out of that data and render it.
Advantage: dynamic, the select list will update itself whenever you add or delete new nodes of that type / category. Disadvantage: takes more effort initially
I personally would recommend using option 2. Option 1 is better for really simple and "stupid" requirements like showing some hardcoded text/image on several places on your site, or if it's something temporary like some campaign teaser, which will be over in 1 week and you'll throw it away after that.
EDIT:
Entity Query: https://api.drupal.org/api/drupal/core!lib!Drupal.php/function/Drupal%3A%3AentityQuery/8
How to build Forms in Drupal 8:
https://www.drupal.org/node/2117411
For display only your block in your templates with preprocess the best way is:
$block = \Drupal\block\Entity\Block::load('my_block_id');
$variables['My_region'] = \Drupal::entityManager()
->getViewBuilder('block')
->view($block);
And in your page.html.twig or node.html.twig or xxx.html.twig use your variable My_region like this :
{% if page.My_region %}
{{ page.My_region }}
{% endif %}
For details check:
https://drupal.stackexchange.com/questions/171686/how-can-i-programmatically-display-a-block

GTM Reduce number of tags

GTM up and running, main UA tag in place along with a ClickListener tag.
To reduce the number of macros, i use dataLayer variable Macros for event category, action, label, value & interaction, so they can be used for many rules and tags.
So i want to collect data from one link/button (Add to Fav), i add a rule to listen for the click using {{event}} equals gtm.click and {{Event Label}} equals Add_to_Fav (the label i push to the DL via onclick.
All good so far, but i need to create another UA tag (Track Type - event) that fires on the rule made previously. And this is my question, using this method seems to create many tags. If i have another 20 links that i want to collect data from, do i need to keep creating tags like this. Surely, this will affect page load speed with many tags firing on all pages.
Hope thats all clear.
If you need to retrieve the link text to use it as an event label you do not need many many event tracking tags, that would be horribly verbose. Instead you can use a custom javascript macro - the cool thing about them being that you can use existing macros inside your custom function.
If you create a click listener or link click listener this will create a few macros - one of them is {{element}}, which is the DOM element that received a click.
Now you create a macro of the type "custom java script", which must contain an anonymous function with a return value.
The barebones version of a function that retrieves the text of a clicked link would be
function() {
var el = {{element}};
return el.innerText;
}
(actually you do not need the variable assigment, you could use {{element}}.innerText directly).
You name the macro e.g. Linktext and use the macro {{Linktext}} in your single event tracking tag where it will dynamically be set to the value of the text of the clicked link (although you might want to check cross browser support for innerText, or maybe use innerHTML instead which serves in you use case probably the same purpose).

Reusing the CQ5 Form into the mywebsite components is not showing up the End of the Form section

I am trying to reuse the Form into my project components.
I have copy pasted the entire form folder from "/libs/foundation/components/form" to my project "/apps/mywebsite/components/form".
But when i am trying to use the form from mywebsite in the parsys the from shows only Start of the from.
Where as when i tried to use the form from the foundation in the same page parsys it shows both Start and End of the form.
Observation:
From the content, when i am using the foundation form the in the page content i can see the start and end nodes. where as when i am using the mywebsite form start node alone is created.
The form end is added/deleted by the fixStructure() method of the FormParagraphPostProcessor class. This post processor listens for creation and deletion of form start and form end paragraph and creates/removes the other paragraphs accordingly.
if ( ResourceUtil.isA(res, FormsConstants.RT_FORM_BEGIN)
|| ResourceUtil.isA(res, FormsConstants.RT_FORM_END)) {
if ( FormsHelper.checkFormStructure(res) != null ) {
logger.debug("Fixed forms structure at {}", contentResource.getPath());
}
}else {
fixStructure(res);
}
This class depends on the FormConstants.java where the form start(RT_FORM_BEGIN) and the form end(RT_FORM_END) are defined as "foundation/components/form/start" and "foundation/components/form/end" respectively. Due to this the post processor doesn't process the form start / end that is present within your project.
To make your custom form component working you may consider one of the following possible options:
Add the sling:resourceSuperType property for your project form start as "foundation/components/form/start". This would create a form end, but it would be of type foundation/components/form/end and not your project form end.
In case you do not want the default form end but your custom form end, then you may need to create a custom post processor which listens to your form start and end and fix the structure accordingly. This requires modifying few other java classes like FormsHelper.java etc, as they are also dependent on the FormConstants.java. Also make sure that the imports in the start.jsp are changed accordingly.
Finally, if you do not want to create custom classes, you can copy the default form start to "/apps/foundation/components/form/start" and make your modifications on top of it. But you need to be careful while using this approach as this is a global change and would affect the other projects that are using the default foundation form start.

Parameter and view naming collisions in Play/Scala templates

I am new to Play Framework and still trying to wrap my head around some things with the new Scala template engine.
Let's say I have the following package structure:
app/
app/controllers/Items.scala
app/models/Item.scala
app/views/layouts/page.scala.html
app/views/item/show.scala.html
app/views/item/details.scala.html //partial
And this is my item/show template:
#(item: Item, form: Form[Item])(implicit flash: Flash)
#layout.page() {
#*want to include details partial, wont work due to item param*#
#item.details(item)
}
Since including another template (e.g. including item/details above) is the exact same syntax as accessing a template parameter (e.g. item above), obviously this existing naming convention won't work without something changing.
I know I can rename my "app.views.item" package to "app.views.items", and rely on singular/plural forms to differentiate the view from the param name, but this does not seem like a very straightforward solution. Also what if I really want the parameter name to be the same as the view package?
One idea I have is to prepend all my views with an extra top level package:
app/views/views/item/details.scala.html
So the include syntax would be #views.item.details(), but again this is obviously a hack.
What is a good way to avoid this issue? How can I better organize my code to avoid such naming collisions?
Most other template engines use operations like "include" or "render" to specify a partial include. I don't mean to offend anyone here, but is the Play Scala template engine syntax so terse that it actually dictates the organization of code?
3 solutions:
First
Typpicaly for partial templates you should use tags as described in the docs, where app/views/tags folder is a base:
file: app/views/tags/product.scala.html
in the templates (no initial import required in the parent view full syntax will allow you to avoid name-clash: #tags.packageName.tagName()):
<div id="container">
#tags.product(item)
</div>
Of course in your case you can also use packages in the base folder
file: app/views/tags/item/product.scala.html
<div id="container">
#tags.item.product(item)
</div>
I'm pretty sure that'll solve your problem.
Second
To avoid clash without changing package's name you can just rename the item in your view, also I recommend do not use a form name for the Form[T] as it can conflict with helpers:
#(existingItem: Item, existingItemForm: Form[Item])(implicit flash: Flash)
#layout.page() {
#item.details(existingItem)
}
Third
If you'll fill your Form[Item] before passing to the view with given Item object, you don't need to pass both, as most probably you can get data from the form:
#(itemForm: Form[Item])(implicit flash: Flash)
#layout.page() {
<div>Name of item is: #itemForm("name").value (this is a replacemnet for ##existingItem.name </div>
#item.details(itemForm)
}
Of course in you product.scala.html you'll need to change the #(item: Item) param to #(itemForm: Form[Item])