Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Shared Layouts

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

Open up base.html.twig and move the {% block layout %} to be around everything. So, put the start just inside the body tag... and the end just before the closing body tag:

<!DOCTYPE html>
<html>
... lines 3 - 16
<body>
{% block layout %}
<nav class="navbar navbar-expand-lg navbar-light bg-light">
... lines 20 - 45
</nav>
{% block body %}{% endblock %}
<div class="container mt-5">
... lines 51 - 60
</div>
{% endblock %}
</body>
</html>

If we refresh the homepage now... it's destroyed! The top nav and footer are gone. Why did I do this? Because I love chaos! Kidding - I did it because it gives us the power, inside layouts, to design totally custom pages: even pages without the traditional navigation and footer, maybe like a temporary landing page for a promotion.

But let's be honest, 99% of the time, we will want the nav and footer. No problem, head back over to base.html.twig. Remember: adding blocks give us more flexibility. So, above the navigation, add a new block called navigation, with {% endblock %} after. Then, down here, another called footer... and {% endblock %}:

<!DOCTYPE html>
<html>
... lines 3 - 16
<body>
{% block layout %}
{% block navigation %}
<nav class="navbar navbar-expand-lg navbar-light bg-light">
... lines 21 - 46
</nav>
{% endblock %}
{% block body %}{% endblock %}
{% block footer %}
<div class="container mt-5">
... lines 54 - 63
</div>
{% endblock %}
{% endblock %}
</body>
</html>

I bet you know what I'll do next. In the layout admin, we can now add a Twig block to the top that renders navigation... then one down here on the bottom. It doesn't need to be in this last zone... but it makes sense there. Render footer.

Let's try it! Hit "Publish and continue editing" and... refresh. We are back!

Creating a Second Layout

Let's create a second layout, this time for the /recipes page. If you look at RecipeController, you'll see that I already did all the work to query for the recipes, and pass them into this template:

... lines 1 - 12
class RecipeController extends AbstractController
{
#[Route('/recipes/{page<\d+>}', name: 'app_recipes')]
public function recipes(RecipeRepository $recipeRepository, int $page = 1): Response
{
$queryBuilder = $recipeRepository->createQueryBuilderOrderedByNewest();
$adapter = new QueryAdapter($queryBuilder);
/** @var Recipe[]|Pagerfanta $pagerfanta */
$pagerfanta = Pagerfanta::createForCurrentPageWithMaxPerPage($adapter, $page, 4);
return $this->render('recipes/list.html.twig', [
'pager' => $pagerfanta,
]);
}
... lines 27 - 34
}

And in that template, we loop over and render each one, with pagination:

... lines 1 - 4
{% block body %}
<div class="hero-wrapper">
<h1>Doggone Good Recipes</h1>
<p>Recipes your pup will love!</p>
</div>
... lines 10 - 31
{% endblock %}

And so, I definitely want to include all of this custom work in the new layout.

Back in the admin area, I'll hit "Publish layout" as an easy way to get back to the layout list. Then hit new layout, I'll choose my favorite layout 2 and call it "Recipes List Layout". To start, add a new block called "Full View"... and drag it anywhere onto the page, whoops! There we go.

What is this "Full View". It's nothing special, in fact, it's kind of redundant! It's nothing more than a "Twig block" that renders the block called body. So, yes, we could have just as easily done this by using the normal Twig block and typing in body.

Publish this layout... then go to "Layout Mappings". Add a new one... and this time I'll link it first... to "Recipes List Layout". Then click "Details". Like last time, we could map this via the route name. But to see something different, use "Path Info", which, again, is just a fancy word for the URL, but without any query parameters. Make it match /recipes... "Save Changes" and... sweet!

When we try the page... it looks awesome! Except, whoops, I forgot the nav and footer! Adding those two blocks to "Recipe List Layout" is easy. But what if, later we decide that every page should render both the navigation block on top as well as a dynamic banner, maybe for a sale that we're having. If that happened, we would need to edit every layout to manually add that new banner.

Shared Layouts

Fortunately, there's a better way to handle repeated layout elements like this.

Hit "Discard" to get back to the layouts list, then click "Shared layouts" and "New shared layout". As usual, the layout type doesn't matter much, so I'll use my normal one... and call it "Nav & Footer Layout".

This is not going to be a real layout that's linked to any pages. Nope, it's just going to be a layout that we steal pieces from. Up in the top zone, create a Twig Block that renders navigation... and I'll even label it "Top Nav" to make it more clear. Then in any other zone - you can put it at the bottom, but you don't have to, add another twig block that renders footer and is labeled Footer.

Cool! Hit "Publish layout". Now we have one shared layout. Again, these are not meant to be mapped to pages: they're meant for us to use in other real layouts.

Check it out: edit "Recipe List Layout". On the bottom left of the screen, hiding behind the web debug toolbar - I'll close that temporarily - there's a button to link a zone with a shared layout zone. Click that, then select the top zone... called the "Header" zone, though that name isn't important.

Now, we can find a shared zone from a shared layout... and we only have one. Hit "Select Zone" and... that's it! The top zone in our layout will now equal whatever block or blocks are in the top zone of that shared layout. If we added more stuff to that zone in the shared layout, it would automatically show up here.

Do that one more time: select the last zone so that the footer definitely shows up at the bottom, select the shared zone and... done!

Publish that, move over, refresh and... the full page is back! Let's quickly repeat that for the "Homepage Layout". Oh, but this is tricky because I put all of my blocks inside this top zone. Mostly, these zones don't matter, but in this case, to avoid overwriting all of this, I'll drag everything except for the navigation twig block down here. We can fix the order later.

And now, set the top zone to use the one from the shared layout. Yup! It replaced what we had there before. Below, also link the bottom zone with the shared layout.

Perfect! Let's check the order of our blocks... which is kind of the beauty of layouts. If I don't like the order of what's on my page, I can always change it! That's better. Publish the layout, head back to the homepage on the frontend and... beautiful!

Next: let's make our recipe list page more flexible by allowing this top h1 area to be built and customized from inside layouts... instead of it being hardcoded in the template.

Leave a comment!

4
Login or Register to join the conversation

Not sure if you made a mistake or not, but you are referring to a body twig block when using the Full View block, but this is not correct, it should be content. In the text:

What is this "Full View". It's nothing special, in fact, it's kind of redundant! It's nothing more than a "Twig block" that renders the block called body. So, yes, we could have just as easily done this by using the normal Twig block and typing in body.

The full view twig template (in my version at least, file: @NetgenLayoutsStandard/nglayouts/themes/standard/block/full_view.html.twig) shows:

{% extends '@nglayouts/block/block.html.twig' %}

{% block content %}
   {{ twig_content|raw }}
{% endblock %}
Reply
weaverryan Avatar weaverryan | SFCASTS | edin | posted 8 months ago | HIGHLIGHTED

Hey edin!

This is tricky stuff! When I saw your well-researched comment, I was thinking "he's right! How did I mess that up?".

But actually, the text is correct. But... you are ALSO correct :D. Here's what happens internally:

1) You're right that the template that is rendered for the full_view.html.twig looks like this:

{% block content %}
    {{ twig_content|raw }}
{% endblock %}

But this doesn't mean that this reads from your block called content. It means that it prints the twig_content variable (we'll talk about where this comes from in a minute) into a block called content. Then, because this extends block.html.twig - that template. - https://github.com/netgen-layouts/layouts-core/blob/master/bundles/LayoutsBundle/Resources/views/nglayouts/themes/standard/block/block.html.twig#L9-L19 - just takes the content block and renders it out.

So this is all a really fancy way of basically just saying {{ twig_content }} outside of any block.

2) So where does twig_content come from? First, each block has a "handler" class. The one for full view is this - https://github.com/netgen-layouts/layouts-standard/blob/master/lib/Block/BlockDefinition/Handler/Twig/FullViewHandler.php - nothing too important there, except that it implements TwigBlockDefinitionHandlerInterface.

3) When the blocks are rendering an event is fired. One of the listeners is this class: https://github.com/netgen-layouts/layouts-core/blob/master/bundles/LayoutsBundle/EventListener/BlockView/GetTwigBlockContentListener.php - notice this checks to see if the block handler implements TwigBlockDefinitionHandlerInterface. If it does, it takes whatever the current template is that's rendering (it's an object, stored in that twig_template parameter), loops over $blockDefinition->getTwigBlockNames($block) and returns the contents of the first matching block. These contents become the twig_content variable rendered in the template.

4) So then, which block does $blockDefinition->getTwigBlockNames($block) return? Well, if you look back at FullViewHandler:

class FullViewHandler ... {
    public function __construct(array $twigBlockNames)
    {
        $this->twigBlockNames = $twigBlockNames;
    }

    public function getTwigBlockNames(Block $block): array
    {
        return $this->twigBlockNames;
    }
}

That doesn't tell us. It just says that the block names are injected via the constructor. Where is this service configured? Well, the argument is configured here: https://github.com/netgen-layouts/layouts-standard/blob/14af051a3f0bdc1f2be89014169a8b84148f1225/bundle/Resources/config/services/block_definitions.yaml#L21

So in the end, EITHER the content block or body block would be used (though the fact that you see a block called content inside ful_view.html.twig is coincidental and not part of this process). If you had both a content and body block, content would win, so I suppose your answer is slightly more correct than mine, though I think I'll keep the text as it is :).

Cheers!

2 Reply

Oh wow, thank you for the detailed explanation. I feel a bit guilty not doing this myself.
I am going to dig into this matter more myself when the full tutorial gets released.

For now, I am not able to render anything besides content Twig block in my template.
I probably missed something as I am using my own playground project for this, but as I said, I will investigate this further with the tutorial project.

Thank you.

Reply

Hey edin!

No worries - I love to dig in, and your conclusion seemed logical to me too originally! Weird that only content Twig block is being used - let me know what you find out when you have a chance to investigate further.

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=8.1.0",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "babdev/pagerfanta-bundle": "^3.7", // v3.7.0
        "doctrine/doctrine-bundle": "^2.7", // 2.7.0
        "doctrine/doctrine-migrations-bundle": "^3.2", // 3.2.2
        "doctrine/orm": "^2.13", // 2.13.3
        "easycorp/easyadmin-bundle": "^4.4", // v4.4.1
        "netgen/layouts-contentful": "^1.3", // 1.3.2
        "netgen/layouts-standard": "^1.3", // 1.3.1
        "pagerfanta/doctrine-orm-adapter": "^3.6",
        "sensio/framework-extra-bundle": "^6.2", // v6.2.8
        "stof/doctrine-extensions-bundle": "^1.7", // v1.7.0
        "symfony/console": "5.4.*", // v5.4.14
        "symfony/dotenv": "5.4.*", // v5.4.5
        "symfony/flex": "^1.17|^2", // v2.2.3
        "symfony/framework-bundle": "5.4.*", // v5.4.14
        "symfony/monolog-bundle": "^3.0", // v3.8.0
        "symfony/proxy-manager-bridge": "5.4.*", // v5.4.6
        "symfony/runtime": "5.4.*", // v5.4.11
        "symfony/security-bundle": "5.4.*", // v5.4.11
        "symfony/twig-bundle": "5.4.*", // v5.4.8
        "symfony/ux-live-component": "^2.x-dev", // 2.x-dev
        "symfony/ux-twig-component": "^2.x-dev", // 2.x-dev
        "symfony/validator": "5.4.*", // v5.4.14
        "symfony/webpack-encore-bundle": "^1.15", // v1.16.0
        "symfony/yaml": "5.4.*", // v5.4.14
        "twig/extra-bundle": "^2.12|^3.0", // v3.4.0
        "twig/twig": "^2.12|^3.0" // v3.4.3
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.2
        "symfony/debug-bundle": "5.4.*", // v5.4.11
        "symfony/maker-bundle": "^1.47", // v1.47.0
        "symfony/stopwatch": "5.4.*", // v5.4.13
        "symfony/web-profiler-bundle": "5.4.*", // v5.4.14
        "zenstruck/foundry": "^1.22" // v1.22.1
    }
}
userVoice