Jump to content

Questions and syntax LATTE (Template Engine by Nette)


sebibu
 Share

Recommended Posts

I really love LATTE (Template Engine by Nette) but here and there I still have problems with the syntax.

I'm using RockPageBuilder by @bernhard and try to output the html for this module:
https://processwire.com/modules/fieldtype-leaflet-map-marker/

In PHP this would be:

<?php echo $map->render($page, 'YOUR MARKER FIELD'); ?>

And in LATTE? 😆 I tried this variants and more:

{$map->render($page, 'map')}
{$map->render($page, 'block->map')}
{$map->render($page, $block->map)}

Getting: Call to a member function render() on null in block #1158 (rockpagebuilderblock-map)

Maybe this topic can be a collection point for smaller Latte syntax questions.

Link to comment
Share on other sites

This

<?php echo $map->render($page, 'YOUR MARKER FIELD'); ?>

would be this

{$map->render($page, 'YOUR MARKER FIELD')}

Your error message means, that $map is NULL. That means $map is not available in your latte file for whatever reason. That means you are not struggling with latte syntax, you are struggling with PHP 😉 😛 

You can easily debug this via TracyDebugger:

{bd($map)}
{bd($page)}

To help you with your specific problem I'd need more info in where you are trying to use this markup...

  • Like 1
Link to comment
Share on other sites

It looks like you need to make $map available in your Latte file first. Then, as Bernhad has said, you could write in your Latte file

{$map->render($page, 'YOUR MARKER FIELD')}

With https://processwire.com/modules/template-engine-latte/ you would write something like the following in your PHP template file (or ready.php):

$view->set('map', $map);

It might vary depending on which modules you use exactly in your project.

Edited by MrSnoozles
Link to comment
Share on other sites

6 hours ago, MrSnoozles said:

$views->set('map', $map);

RockFrontend / Latte doesn't provide this syntax as far as I know. What are you referring to?

6 hours ago, sebibu said:

Ah sorry, now I see $map should be a map marker object. The docs there state this:

<?php $map = wire('modules')->get('MarkupLeafletMap'); ?>

Which you could do in latte like this:

{var $map = $modules->get('MarkupLeafletMap')}

 

Link to comment
Share on other sites

Thanks for your help!

Trying..

{var $map = wire('modules')->get('MarkupLeafletMap')}
{$map->render($page, 'map')}

..in RockPageBuilder block LATTE-file I get: Call to undefined function wire() in block

Trying..

{var $map = $this->$wire('modules')->get('MarkupLeafletMap')}
{$map->render($page, 'map')}

.. I get: Method name must be a string in block

Now I'm confused. 😆 What I'm doing wrong?

Link to comment
Share on other sites

Sorry, wire() is not available in a latte file, because wire() is actually \ProcessWire\wire with the namespace. In PHP files where you have "namespace ProcessWire" at the top just adding wire() will work.

In latte it is a different story, because the latte file has no namespace and it will be compiled. Therefore "wire()" is not available.

But all ProcessWire API variables are, so please just use $wire instead of wire().

Pro-Tip: From any API variable you can access the wire object via ->wire, so this would also work: $block->wire->... or $config->wire->... etc.; So in hooks you might see $event->wire->... and that's the reason for this.

PS: $block is not an API variable but is available in RockPageBuilder blocks! And as it is also extending "wire" it will also have its methods.

Edit: I've just updated the example above and realised that it was using wire('modules') syntax that I'm never using. In that case you need to use $wire->modules->... or simply use $modules->... as $modules is also an API variable and therefore directly available.

Link to comment
Share on other sites

I came back to the thread to share some experiences and they are very similar to what @sebibu is running into. Latte introduces a lot of challenges with scoping and augments how you interact with ProcessWire. This is most pronounced for me when working with different inheritance types provided by Latte.

When working with `include` there is no scope so anything that is added using {include 'file_name.latte'} will have access to the parent variables. This includes $page and $wire. Unfortunately the limitations with Latte is that you can't define blocks inside them, they only take parameters in the {include} statement. This is fine for most applications, but if you have a reusable component, such as a modal where the markup for the modal stays the same but complex contents should be contained in that markup, it would best be served by using {embed} because you can define something like {block content}{/block}.

So while variables are available in primary template files, using all available features in Latte starts to lock out ProcessWire.

Files included using {embed} are isolated in scope, so anything you want/need available in that file must be passed as parameters or added as rendered content within a {block}stuff here{/block}. This can get laborious for developers that are used to having ProcessWire's global variables and functions available anywhere. From my experience, there are two options.

{embed 'file_to_embed.latte',
  page: $page,
  wire: $wire,
  foo: 'foo',
  bar: 'bar
}
{block content}waddap{/block}
{/embed}

You can choose to add parameters to an {embed} file that provide ProcessWire's API. I'm not entirely a fan of this because it feels laborious and if we're just going to have to pass the ProcessWire API into Latte components to overcome the limited scope, then it's just an extra step that ends up adding additional parameters passed to components all over your templates very repetitiously. I'm also used to working around the concept of only providing objects the specific data they are supposed to work with like best practices for dependency injection containers.

The second option is to add custom functions to Latte using a hook at runtime. This method allows you to declare functions that will be available globally within all types of Latte reusability methods. Here's an example of a hook file on my current project. I have a dedicated latte_functions_extension.php file where I can declare these in an organized way.

<?php

declare(strict_types=1);

namespace ProcessWire;

use Latte\Extension;

final class CustomLatteFunctions extends Extension
{
    public function getFunctions(): array
    {
        return [
            // Latte templating paths
            'definitions' => fn (string $file) => $this->createComponentPath('definitions', $file),
            'embeds' => fn (string $file) => $this->createComponentPath('embeds', $file),
            'imports' => fn (string $file) => $this->createComponentPath('imports', $file),
            'layout' => fn (string $file) => $this->createPath('layouts', $file),
            'partial' => fn (string $file) => $this->createPath('partials', $file),
            // Expose ModernismWeekSite.module.php as site()
            'site' => fn () => wire('modules')->get('ModernismWeekSite'),
            // Ensure that wire() is available in all components
            'wire' => fn (?string $property = null) => wire($property),
        ];
    }

    /**
     * Creates a path for including a component
     * @param  string $file Dot notation subdir and filename,
     * @return string
     */
    private function createComponentPath(string $componentSubdir, string $file): string
    {
        return $this->createPath("components/{$componentSubdir}", $file);
    }

    /**
     * Creates a component file path for a given filename does not require .latte suffix
     * @param  string $templatesSubdir Name of directory in /site/templates
     * @param  string $file            Name of .latte file that exists in the directory
     */
    private function createPath(string $templatesSubdir, string $file): string
    {
        !str_ends_with($file, '.latte') && $file = "{$file}.latte";

        return wire('config')->paths->templates . "{$templatesSubdir}/{$file}";
    }
}

$wire->addHookAfter(
    "RockFrontend::loadLatte",
    fn (HookEvent $e) => $e->return->addExtension(new CustomLatteFunctions),
);

I've defined a 'wire' function that will be available in every component with a parameter that allows you to use it like you would expect to such as 'wire('modules')'. I have a custom site module so I've exposed that as 'site()'. If you wanted to make it easier to work with modules in your templates and included files you could define a more terse 'modules' function:

<?php

final class CustomLatteFunctions extends Extension
{
    public function getFunctions(): array
    {
        return [
            // ...
            'modules' => fn (string $moduleName) => wire('modules')->get($moduleName),
        ];
    }
}

I feel that there is a tradeoff when using Latte in ProcessWire. There are some great features in Latte, but it requires introducing abstractions and feature management to make Latte act like ProcessWire, like manually defining functions. This just means that you'll have to keep a balance of complexity/abstraction vs. using as minimal enough of a approach to keep it sane.

The other challenge here is that now there can be a deviation between where the native ProcessWire API is used in Latte and other places where it isn't. So some files will use $modules, and other files will use modules(), and it's not clear whether that's referencing the ProcessWire functions API, or whether it's leveraging Latte's custom functions extension feature. Something to keep in mind when determining how other files will be included/rendered in other files.

In my case I have two examples that brought this challenge out for me today. Here's one

// Native behavior provided by the module
// Does not work everywhere due to scoping in Latte. This caused an issue when trying to embed forms
// in a modal within a {block}
{$htmxForms->render($page->field_name)}

// With one level of abstraction using a custom function
// Because this replicates how ProcessWire provides the wire() function natively, the usage feels
// natural and predictable, especially for core behavior, but this introduces a lot of verbosity
// that starts to make files pretty messy
{wire('modules')->get('FormBuilderHtmx')->render($page->field_name)}

// With two levels of abstraction in Latte via a custom function
// This looks better and still adheres to the syntax of the ProcessWire functions API
// The issue is that every native ProcessWire function has to be manually replicated in our custom
// functions hook class. Managing this in the long term requires extra work and cognitive load
{modules('FormBuilderHtmx')->render($page->field_name)}

// With 3 levels of abstraction
// This has restored the feel of the variables provided by the module, but again we have to track
// And implement them on an as-needed basis to manage them within the context of usage in Latte
{htmxForms()->render($page->fieldName)}

The level of abstraction you choose depends on how much customization you want vs. how much extra work it will take to maintain simplicity by hiding complexity.

The other functions, 'embeds', 'definitions', 'imports', etc. are to overcome the relative paths all over the place in Latte.

// In my home.latte file
{layout './layouts/main.latte')}
{var $collapseNavOnScroll = true}
{import './components/definitions/headlines.latte'}
{import './components/definitions/event_activity_card.latte')}
{block subnav}
  {embed './components/embeds/event_subnav.latte', eventPage: $page->eventPage()}{/embed}
{/block}
// ...etc

// Becomes
{layout layout('main')}
{var $collapseNavOnScroll = true}
{import definitions('headlines')}
{import definitions('event_activity_card')}
{block subnav}
  {embed embeds('event_subnav'), eventPage: $page->eventPage()}{/embed}
{/block}
// etc.

// In RPB blocks
{embed '../../../components/embeds/example.latte', content: $block->body()}{/embed}

{embed embeds('example'), content: $block->body()}{/embed}

This really helps when working with Latte templates that embed components that have nested embeds and imports because the functions are generating absolute paths that Latte can handle. With these functions, I don't have to think about relative paths anywhere.

As for the directory structure that I chose that requires the different paths, here's what it looks like:

/templates
  ...etc
  /components
    /definitions
    /embeds
    /imports
    ...etc

I chose that method because Latte has many different ways of including, embedding, and importing code from other files. It made more sense to organize my code by how Latte treats it. It wasn't my first choice, but this overcomes confusion that I was experiencing when working with all of the files sharing the same components directory.

Without this type of organization it can be challenging to because of scoping and how {embed}, {include}, {define}, and {import} behave differently. Some accept parameters, export values, or use blocks, but not all of them do. So having a "modal.latte" component file that provides a {block content}{/block} next to 'button.latte' that doesn't render anything and only exports items created using {define}, next to a file that is only added to a template using {include} that doesn't take parameters or provide blocks had me jumping between files a lot and slows down development checking to see that the file is being used correctly.

Just sharing some of my experiences in case it helps anyone else out.

If anyone sees anything here that can be done better or if I'm missing out on features that Latte has that don't require some extra steps, let me know!

Link to comment
Share on other sites

Importing definitions are also not working for me. I have this defined:

1114565542_Screenshotfrom2024-09-2816-12-09.thumb.png.53aa98160a49e8e8855b547bb35720d8.png

When attempting to use it:

53151557_Screenshotfrom2024-09-2816-16-32.png.e1918ba9aa258465637d13a9b8545c0a.png
 

image.png.7fe640147ca89a06279c1af865458174.png

I've confirmed that the path is correct, so it's seeing the file, but attempting to create simple elements using {define} and then using them with {import} fails.

What am I doing wrong?

 

Link to comment
Share on other sites

 Share

  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...