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.

  • Like 1
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 2
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!

  • Like 2
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

Hey @FireWire sorry to hear that you are having troubles.

It seems that you are doing some quite complex stuff. I've never experienced any of these problems - maybe because I keep things very simple!

12 hours ago, FireWire said:

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!

What is "better" is a very subjective thing, but there are two things I want to mention upfront:

12 hours ago, FireWire said:
{embed 'file_to_embed.latte',
  page: $page,
  wire: $wire,
  foo: 'foo',
  bar: 'bar
}

Instead of listing all API variables you can just pass the $wire variable and then access anything you need in your template from there:

// if you passed the wire variable you can do this:
{$wire->pages->find(...)}
{$wire->modules->get(...)}
{$wire->...}

// if you passed "page: $page" do this
{$page->wire->pages->find(...)}

// or this
{var $wire = $page->wire}
{$wire->pages->find(...)}

I guess I don't have these problems because I'm always working with custom page classes, so I most of the time add those things to the pageclass and then I can just use $page->myMethod(). That has the benefit that template files stay very simple and I have all the logic in PHP and not in Latte, which has a ton of benefits. For example you can do this for debugging:

// in site/ready.php
bd(wire()->pages->get(123)->myMethod());

Or you could access ->myMethod from anywhere else, like in a cronjob etc. If you have your business logic in latte then you have to refactor at some point or you are violating the DRY concept if you are lazy and just copy that part of your logic from the latte file to the cronjob.

10 hours ago, FireWire said:

image.png.7fe640147ca89a06279c1af865458174.png

I'm not sure I can follow... I put this in whatever.latte:

  {block foo}
    <h1>Hello World</h1>
  {/block}

  {define bar}
    <h1>Hello World</h1>
  {/define}

  {include foo}
  {include bar}

And I get 3 times "Hello World" as expected. {block} does immediately render while {define} waits for the final include

Also I don't know why you make things "complicated". When I need a reusable portion of code like for a button, I simply put that into /site/templates/partials/my-button.latte and then I use {include ../partials/my-button.latte} in my latte file.

I'm usually always either in /site/templates/sections or in /site/templates/partials. That's it.

Sections are full width elements, like hero, slider, breadcrumbs, main, footer, etc. and partials are things like buttons, cards, etc.

Having said that I see that there might be potential to improve the experience though. For example we might make the functions api available to all latte layout files or we might make API variables always available. But to make that happen I'd need some easy to follow real world examples and not complex code examples like your specific setup.

Also I'm hesitant as I didn't experience any of these "problems" for maybe two years or so. So we have to identify the real problem first. Maybe the problem is only lack of documentation for example.

  • Like 1
  • Thanks 1
Link to comment
Share on other sites

@bernhard I get that there are easier ways of doing things. My challenges arose when trying to use Latte according to the documentation because keeping things simple started causing problems.

I started going off what the Latte docs say about these tags:

{layout}
{include}
{import}
{define}
{embed}

The biggest part for me is identifying what role a component of the design performs so that the code maintainable similar to the Latte docs. My example of the button follows the example use case from Latte to output simple elements. In my case I currently have 3 different kinds of buttons, probably will be more, that line up with how {define} works. Using {define} lets me organize these in one buttons.latte file and then import that file so any one or more of them can be used.

{* buttons.latte *}

{define button-primary, label, color: 'cerulean'}
  <button class="very-long tailwing-classes to-style this-button {$color === 'cerulean' ? 'bg-cerulean text-white'}{$color === 'strikemaster' ? 'bg-strikemaster text-white'} many-more-classes">
    {label|noescape}
  </button>
{/define}

{define button-secondary, label, color: 'cerulean'}
  <button class="very-long tailwing-classes to-style this-button bg-white {$color === 'cerulean' ? 'text-cerulean'}{$color === 'strikemaster' ? 'text-strikemaster'} many-more-classes">
    {label|noescape}
  </button>
{/define}

{define button-secondary, label, theme: 'light'}
  <button class="very-long tailwing-classes to-style this-button {$theme === 'dark' ? 'child-svg:fill-neutral-600 child-svg:hover:fill-neutral-800}{$theme ==='light' ? 'child-svg:fill-white/90 child-svg:hover:fill-white'} many-many-more-classes">
    {site()->renderIcon(
      name: 'material-design:close',
      title: __('Close'),
      description: __('Close this modal'))
    }
  </button>
{/define}



{*
	view.latte (simplified for example)
	This is where importing from an external file fails.
*}
{import definitions('buttons.latte');

{include button-primary, __('View Tickets')}

Other definitions are input elements like toggles and selects, Field outputs (like reusable containers specific from TinyMCE), and Headlines that contains multiple reusable headline styles. I have been struggling to figure out why my code doesn't work like the Latte documentation states for including everything from a file using {import}.

Using {define} makes a lot of sense in this project because these are simple elements that just output markup and managing them in one file makes life a lot easier.

Then there are modals, lightbox galleries, product cards, etc. I already have 32 separate components for including and embedding, and that number is growing very quickly. Some components are quite complex since they're implemented using AlpineJS (lots of JS). So for me the complexity is coming from the project requirements rather than overengineering.

4 hours ago, bernhard said:

But to make that happen I'd need some easy to follow real world examples and not complex code examples like your specific setup.

I think the examples below are good examples that are real world.

For example, I have 3 different types of modals thus far and there will be more, these are all perfectly suited for the {embed} method that introduces the scoping considerations I mentioned. Here's an example of one of the modals that really need the usefulness that {embed} provides.

{parameters $formField, $content = null, $ariaTitle, $buttonLabel, $buttonStyle = 'primary'}
{import definitions('buttons.latte')}
<div x-data="{ open: false }"
     @keydown.escape.window="open = false"
     x-on:toggle-modal.window="open = !open"
     x-on:close-modal.window="open = false"
     x-id="['ariaTitle']"
  >
  {if $buttonStyle === 'primary'}
    {include button-primary, label: $buttonLabel, click: "open = true"}
  {/if}
  {if $buttonStyle === 'text'}
    <button @click="open = true">{$buttonLabel}</button>
  {/if}
  <template x-teleport="body">
      <div x-show="open"
           :aria-labelledby="$id('ariaTitle')"
           x-transition:enter="transition ease-out duration-300"
           x-transition:enter-start="opacity-0"
           x-transition:enter-end="opacity-1"
           x-transition:leave="transition ease-in duration-300"
           x-transition:leave-start="opacity-1"
           x-transition:leave-end="opacity-0"
           x-on:transitionend="event => {
             if (event.target !== $el) {
               return;
             }

             if (open) {
               $refs.formContainer.style.minHeight = `${ $refs.formContainer.offsetHeight }px`;

               {* Force focus on first field instead of close button by x-trap *}
               $refs.formContainer.querySelector('input').focus();
             }

             if (!open) {
               const container = $refs.formContainer;

               container.style.minHeight = 'auto';
               container.querySelector('form').reset();

               [...container.querySelectorAll('.FormBuilderErrors, .input-error')].forEach(el => el.remove());
             }
           }"
           class="flex fixed inset-0 z-[100] items-center justify-center w-screen h-screen bg-white/60 backdrop-blur-sm limit-max-width p-5 md:p-8"
      >
        <div class="relative w-full max-w-[40rem] bg-white shadow-xl flex flex-col overflow-y-scroll max-h-[95vh]"
            @click.outside="open = false"
            :aria-hidden="open ? 'false' : 'true'"
            x-trap.noscroll.inert.noautofocus="open"
            role="dialog"
            aria-modal="true"
        >
          {include button-modal-close}
          <div class="p-10 overflow-y-scroll after:block after:absolute after:z-10 after:bottom-0 after:left-0 after:h-10 after:w-full after:bg-gradient-to-t after:from-white after:to-transparen">
            {block content}
            	{* Headline, additional text, CTA message, etc. *}
            {/block}
            <div x-ref="formContainer" class="w-full [&>div]:w-full">
              {modules('FormBuilderHtmx')->render($formField, [], ".htmx-indicator-form-processing")|noescape}
            </div>
          </div>
          <div class="htmx-indicator htmx-indicator-form-processing absolute inset-0 h-full w-full bg-white/70 z-10 flex items-center justify-center">
            <div class="loader"></div>
          </div>
      </div>
    </div>
  </template>
</div>

There's not a better way to do this without {embed}.

The modal embed is where the scoping is introduced that blocks out the API, so I added site(), wire(), and modules() available, but that list will grow and manually managing each API function becomes cumbersome and doesn't really fit with how ProcessWire works. I know that my current project is more complicated than the usual site, but that's where templating systems really shine!

4 hours ago, bernhard said:

Instead of listing all API variables you can just pass the $wire variable and then access anything you need in your template from there:

In my examples, the parameter list can already get quite long and seeing $wire passed around a lot makes Latte feel detached rather than integrated. Having Latte files get only what they need makes code and purpose a lot easier to grok. It's also just a best practice similar to OOP. When using {include} Even if they're available, I also avoid using parent variables directly because I don't know where using them from one place to another may break something. The {embed} scope ends up acting as a "safety check" to make that component safe to use anywhere.

I think exposing the Functions API would be great. It makes a lot of sense to me when working with how scope is documented in Latte and also how it's documented as globally available in ProcessWire. Although my example above of modules()->get('FormBuilderHtmx')->render() is more verbose than $htmxForms->render(), the Functions API makes it clear that you are working with globally scoped modules(). I think this becomes even more useful if we consider how many non-standard global variables are made available via modules, $forms, $rockicons, $fluency, etc.

4 hours ago, bernhard said:

Also I don't know why you make things "complicated". When I need a reusable portion of code like for a button, I simply put that into /site/templates/partials/my-button.latte and then I use {include ../partials/my-button.latte} in my latte file.

I am definitely trying to keep things from getting complicated! The use cases are more complex than using {include}, but they're not overengineered. Using {include} for things like modals ended up making my code more complicated where I was able to come up with a solution, but it was very clear that I was using the wrong tool for the job.

This is where my project is at and I have a ways to go before it's done, so this is going to grow a lot more. Using {define} and {import} with shared elements like above would save 8 additional files alone just right now.

image.thumb.png.2a87c9fbe0142cbc8b56993e8eb743a1.png

In my file structure above, the 'includes' are big standalone elements, 'embeds' are complex functional units of behavior independent of context, and 'definitions' are repetitious generic UI elements. My 'layouts' and 'partials' directories live in /site/templates.

I was using the method you mentioned with {include} but it just got to a point where the Latte features really started making more sense. I'm fully using Page Classes so my access to $page and $pages hasn't presented any problems and that's data perfectly suited for parameters passed to a component from a template.

Link to comment
Share on other sites

Hey @FireWire thx for the detailed response. Unfortunately it does not help me understand your problem or request.

9 minutes ago, FireWire said:

My challenges arose when trying to use Latte according to the documentation because keeping things simple started causing problems.

What problems? I can't help if I can't understand the problem.

10 minutes ago, FireWire said:

Using {define} makes a lot of sense in this project because these are simple elements that just output markup and managing them in one file makes life a lot easier.

Personally I don't agree here. But that might be a matter of preference. If files are growing you can also just put them in a folder, so instead of /site/partials/button-primary.latte you could simply have /site/templates/buttons/primary.latte, or am I missing something?

12 minutes ago, FireWire said:

The modal embed is where the scoping is introduced that blocks out the API, so I added site(), wire(), and modules() available, but that list will grow and manually managing each API function becomes cumbersome and doesn't really fit with how ProcessWire works.

I understand that. But to be precise this is only how ProcessWire works if the functions API is enabled, which might not always be the case. Personally I'm always using wire()->modules instead of modules() in my modules, because wire() is always available and wire('modules') does break intellisense in my IDE whereas wire()->modules does work.

I'd be interested to see simple examples (like 5 lines, not 50 like the one example you pasted) how you are using those function calls and what exactly causes problems if they are not available.

17 minutes ago, FireWire said:

There's not a better way to do this without {embed}.

I'm sorry, but this example is too complex for me to grasp.

17 minutes ago, FireWire said:

I think exposing the Functions API would be great.

I agree, but I need to fully understand the situation/scope/whatever first.

19 minutes ago, FireWire said:

Using {include} for things like modals ended up making my code more complicated where I was able to come up with a solution, but it was very clear that I was using the wrong tool for the job.

I'd still appreciate to get an example.

20 minutes ago, FireWire said:

Buttons were a good example since they're the perfect use for storing in an external file as {define} then using {import} to get everything in that file. Another good example are form elements like stylized Alpine/TW toggles and selects that when sharing a file become very easy to manage.

I'm sorry but I don't understand. Maybe some simple code examples would help.

22 minutes ago, FireWire said:

I was using the method you mentioned with {include} but it just got to a point where the Latte features really started making more sense.

Same as above.

Link to comment
Share on other sites

@bernhard I think it's difficult to explain with actual code examples from my project, other than to say that in this case it is more complex than other projects. I would be simplifying my code to match the examples in the documentation. Here's an example of my workaround to use {include} vs. using Latte's {embed} tag.

{* Using capture to assign to a variable *}
{capture $modalContents}
   <h2>{=__('Welcome to my humble modal')}</h2>
  {$page->body|noescape}
  <hr>
  {include 'button-primary.latte'}
{/capture}

{include 'modal.latte', butonLabel: __('Random Details'), buttonType: 'link', size: 'lg', height: 'fit', content: $modalConents}

{* Using embed *}
{embed 'modal.latte', butonLabel: __('Random Details'), buttonType: 'link', size: 'lg', height: 'fit'}
  {block content}
    <h2>{=__('Welcome to my humble modal')}</h2>
    {$page->body|noescape}
    <hr>
    {include 'button-primary.latte'}
  {/block}
{/embed}

There are pages with multiple modals and using {embed} just makes use of the Latte features as documented. For me, it's easier to read, provides indentation, and it's useful to see the {block} feature of {embed} would come in handy- especially when there are multiple blocks. So, true- the same thing can be achieved with using {include}, but more complexity starts to feel like it's compounding. Beyond that, I can't really make a case for why embed is better or not, it's just a feature of Latte that has been helpful.

4 hours ago, bernhard said:

I understand that. But to be precise this is only how ProcessWire works if the functions API is enabled, which might not always be the case. Personally I'm always using wire()->modules instead of modules() in my modules, because wire() is always available and wire('modules') does break intellisense in my IDE whereas wire()->modules does work.

Providing specific examples of where the Functions API would be used is less useful than to say that none of the ProcessWire variables or functions work in Latte files included using {embed}.

image.png.2df20d4c0bba7246fd8bea1e2dae4020.png

That is why I mentioned the custom hook I wrote to overcome that error in my earlier post. I don't really have a strong preference whether the Function or object API (or anything else) is made available, it only matters if it's going to be implemented. I just need to know if there's any way that this would be available in RFE or if I should just keep managing it using the RockFrontend::loadLatte hook.

3 hours ago, bernhard said:

Personally I don't agree here. But that might be a matter of preference. If files are growing you can also just put them in a folder, so instead of /site/partials/button-primary.latte you could simply have /site/templates/buttons/primary.latte, or am I missing something?

I guess you're right about it being a preference. When I first talked about the {define} {import} style and the errors I was getting, I didn't think that had something to do with RFE. I didn't know where to start and figured I did something wrong so that's where I was troubleshooting from. I can't really make a case for this other than to say it's just a feature in Latte that works very well for me.

So there's only to things I'm struggling with at the moment-

Can some form of the ProcessWire API be made available through RPB to files using {embed}?
Can it be possible to use {define} and {import} using files as shown in the Latte docs?

I think I was too specific in my other response and didn't mean for it to get in the way of what I was trying to say. I posted before coffee this morning. I didn't mean to be poorly communicative or aggressive.

  • Like 1
Link to comment
Share on other sites

16 minutes ago, FireWire said:

Can some form of the ProcessWire API be made available through RPB to files using {embed}?
Can it be possible to use {define} and {import} using files as shown in the Latte docs?

That helps.

Ok I tried the following:

// _main.latte
{include 'test.latte'}

// test.latte
{bd($wire)}

// result
// $wire is a ProcessWire object

Next using {embed}

// _main.latte
{embed 'test.latte'}{/embed}

// test.latte
{bd($wire)}

// result
// $wire is NULL

This is exactly the behaviour that the docs state:

Quote

The {embed} tag is similar to the {include} tag but allows embedding blocks into the template. Unlike {include}, only explicitly declared variables are passed:

That's what I wished you provided, so I can easily try things out and can follow. But seems we are there now.

20 minutes ago, FireWire said:

Can some form of the ProcessWire API be made available through RPB to files using {embed}?

Yes. Always remember LATTE = PHP, so you can do this, for example:

{extract(\ProcessWire\wire('all')->getArray())}
{bd($wire)}
{bd($modules)}

As you can see when adding the namespace to any function api it will also work! Again, it's just PHP.

Or you could also do this:

// _main.latte
{embed 'test.latte', api: get_defined_vars()}{/embed}

 

38 minutes ago, FireWire said:

Can it be possible to use {define} and {import} using files as shown in the Latte docs?

I'm sorry, but I'm not going to try to guess what you mean. Actually I tried, but unless you provide a simple step by step example like I did above I can't help, as I don't know/understand what the problem is. Maybe you already mentioned it, but I can't remember everything you wrote and showed in one of the extensive examples. I know you are busy, but so am I, so it would be nice to make it easier for me to follow and help 😉

  • Like 1
  • Thanks 1
Link to comment
Share on other sites

On 9/29/2024 at 1:09 PM, bernhard said:

I'm sorry, but I'm not going to try to guess what you mean. Actually I tried, but unless you provide a simple step by step example like I did above I can't help, as I don't know/understand what the problem is.

I have been able to get it to work. I think it was sneaky syntax that wasn't really clear and easy for me to miss! I think the challenge in our conversation was that it was hard for me to describe more robustly because it ended up being difficult to understand myself.

I'll recap. Let's assume I have a file called 'buttons.latte' and inside of it I have a couple of reusable definitions, this goes along with what I was describing earlier- nothing new.

{define button-default, $label}
  <button class="bg-blue-200 hover:bg-blue-300 transition-colors">
    {$label|noescape}
  </button>
{/define}

{define button-danger, $label}
  <button class="bg-red-500 hover:bg-red-600 transition-colors">
    {$label|noescape}
  </button>
{/define}

 

I can import all of those definitions using {import} and the file name in my templates and other components/partials/files etc. Then I use {include} with a specific name rather than individually from different files. Using {define} and {import} is a little like named exports in JavaScript.

{import 'buttons.latte'}

<h1>Welcome to the internet cafe, have a Latte</h1>

{* ...A bunch of stuff on the page... *}

 <div class="modal">
  <h2>Do you really want to do that?</h2>
  {* Here I can use include with button-danger instead of including a file like 'button_danger.latte' *}
  {include button-danger, label: 'OK'}
  {include button-default, label: 'Cancel'}
</div>

{* ...The rest of the stuff on the page... *}

 

The problems all started when using an {embed} on the page and then attempting to use an element that was imported, in this case button-danger and button-default. Here's what happens if we create a reusable "modal" that can be embedded and populated using blocks.

{import 'buttons.latte'}

<h1>Welcome to the internet cafe, have a Latte</h1>

{* ...A bunch of stuff on the page... *}

{embed 'modal.latte'}
  {block content}
    {* The block inside the embed tag is a whole new scope and fails when you attempt to include the buttons imported outside that embed tag *}
    <h2>Do you really want to do that?</h2>
    {include button-danger, label: 'OK'}
    {include button-default, label: 'Cancel'}
  {/block}
{/embed}

{* ...The rest of the stuff on the page... *}

This was confusing because this {embed} and the content inside the {block} were mixed in with the rest of my template markup so it wasn't easy to catch where the issue was actually occurring. Even with the improved error reporting you added, the error that was shown by Latte by this did not make it clear it was a scope issue so at the time I thought it was because the 'buttons.latte' file was not being included whatsoever. The real story here is that Latte does not provide errors with specific messages for scoping issues. It will just show an error that looks like a file can't be found.

HOWEVER. Solution ahoy!

The following does work because the {import} is placed inside the {embed} tag so now the elements in 'buttons.latte' are now scoped within that embed tag so they can be included by name. In this example I've left the {import} at the top of the page because that would necessary should you want to use one of the button elements anywhere outside the scope of that {embed} tag.

{import 'buttons.latte'}

<h1>Welcome to the internet cafe, have a Latte</h1>

{* This required that import above for the parent scope *}
{include button-default, label: 'Here is a random button'}

{* ...A bunch of stuff on the page... *}

{embed 'modal.latte'}
  {import 'buttons.latte'}
  {block content}
    {* These work now that the buttons have been imported in the embed scope and can be included by name *}
    <h2>Do you really want to do that?</h2>
    {include button-danger, label: 'OK'}
    {include button-default, label: 'Cancel'}
  {/block}
{/embed}

{* ...The rest of the stuff on the page... *}

I thought that Latte's scoping rules were specific to files not tags- in this case the {embed} tag.

This was really easy for me to miss in a couple of larger Latte files where noticing the {embed}{/embed} scope while surrounded by a bunch of other markup was really not easy to catch, and combine that with the not-so-clear errors, it made it seem like {import} was broken, but it isn't.

This is no problem once you add that to your knowledge of Latte. That said, I think that the fact that Latte uses {include} interchangeably for files like "button.latte" and elements created using {define button}{/define} can be a point of confusion. Well, it was for me.

So, Latte scope is serious business but errors leave something to be desired 🤣

I've been able to use all of the features of Latte with RFE and it's great. Hopefully my mistake will help others. Thanks as always for your help @bernhard this was a tough one 🤷‍♂️

On 9/29/2024 at 1:09 PM, bernhard said:

As you can see when adding the namespace to any function api it will also work! Again, it's just PHP.

This is a pretty smart solution! I didn't think about that. I'm still wrapping my head around the Latte === PHP situation. My brain hasn't connected the two yet. Slowly but surely...

Since I already had it set up, I'm using the hook RFE provides to access the Latte instance and a custom extension. I posted this above, but I'll mention it here again in case it's helpful for someone to see this info all in one place. (I added a custom filter as well)

<?php

use Latte\Extension;

final class CustomLatteExtension extends Extension
{
    /**
     * Define functions available in all Latte files
     * These can be anything ProcessWire related or not, any functions defined here will be available everywhere
     */
    public function getFunctions(): array
    {
        return [
            'wire' => fn (?string $property = null) => wire($property),
        ];
    }

    public function getFilters(): array
    {
        return [
            'bit' => fn (mixed $value) => new Html(filter_var($value, FILTER_VALIDATE_BOOLEAN) ? 1 : 0),
            'bitInverse' => fn (mixed $value) => new Html(filter_var($value, FILTER_VALIDATE_BOOLEAN) ? 0 : 1),
        ];
    }
}

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

That hook in practice does this:

// Defining the wire() function above means that you can use it in any Latte file anywhere
{wire()->modules->get('SomeModule')}

// The $property parameter is a little syntactic sugar that for calling wire() with arguments for a native feel
{wire('modules')->get('SomeModule')}

// I have a need to output booleans to the rendered markup and it was getting laborious. My use case is outputting to AlpineJS that exists in
// Latte templates
{var $someVariableOrPageProperty = true}

function someJsInYourTemplate() {
    // This does nothing because you cant echo a boolean to the page and neither will Latte
    if ({$someVariableOrPageProperty}) {
        //...
    }

    // You can do this, but it gets ugly when you're working with a lot of booleans.
    if ({$someVariableOrPageProperty ? 1 : 0}) {
        //...
    }

    // With the custom filter. Clean.
    if ({$someVariableOrPageProperty|bit}) {
        //...
    }
}

Anyway, those are what I came up with and it's working out great for me.

  • Like 1
  • Thanks 1
Link to comment
Share on other sites

Hey @FireWire thank you very much for taking the time to explain everything. That's great and I understand now! I have an idea, but I tried adding your example to /site/ready.php and I get this:

l5rJeZe.png

This is my ready.php:

<?php

namespace ProcessWire;

if (!defined("PROCESSWIRE")) die();

/** @var ProcessWire $wire */

$rockforms->setErrors('de');



use Latte\Extension;

final class CustomLatteExtension extends Extension
{
  /**
   * Define functions available in all Latte files
   * These can be anything ProcessWire related or not, any functions defined here will be available everywhere
   */
  public function getFunctions(): array
  {
    return [
      'wire' => fn(?string $property = null) => wire($property),
    ];
  }

  public function getFilters(): array
  {
    return [
      'bit' => fn(mixed $value) => new Html(filter_var($value, FILTER_VALIDATE_BOOLEAN) ? 1 : 0),
      'bitInverse' => fn(mixed $value) => new Html(filter_var($value, FILTER_VALIDATE_BOOLEAN) ? 0 : 1),
    ];
  }
}

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

Do I have to require_once something? I have never added a custom extension yet.

Link to comment
Share on other sites

11 hours ago, FireWire said:

This is a pretty smart solution! I didn't think about that. I'm still wrapping my head around the Latte === PHP situation. My brain hasn't connected the two yet. Slowly but surely...

Thx, but I don't think it's very smart 😄 It's really just PHP, that's one of the main reasons why I like latte so much. They don't reinvent PHP or invent another syntax, they just use PHP and add some helpers here and there.

It's really easy to understand, for example create the file php-demo.latte:

{file_put_contents('php-demo.txt', 'latte-is-cool')}

This will do two things:

  • It will create the file php-demo.txt in the folder of the .latte file
  • It will output "13", which is the strlen of "latte-is-cool"

You can think of { ... } being similar to <?= ... ?>. That's why tracy will instantly work inside latte via {bd(...)} as bd() is a globally available function without any namespace and it does not return/output anything.

To prevent our previous example from outputting anything you can simply add "do":

{do file_put_contents('php-demo.txt', 'latte-is-cool')}

Also when using {var $foo = 'whatever'} there is no output.

Now consider the following file "php-demo.latte":

{wire()->page->id}

This will cause the following error:

j510Dvw.png

You can click on "PHP" and then you see which file it actually renders and why wire() is not found:

4hZme7E.png

Once you click on that file link, it will open up in your IDE:

10nlWLG.png

So you can see that this is the compiled PHP file that latte uses to render your .latte file. And your IDE shows, that wire() is not available!

dJOUwn4.png

So using our IDE we can fix the issue:

0nqCnxs.png

Which will add this to the PHP file:

KT3e2GJ.png

Now since we can't add the use statement to our latte file like this:

{use function ProcessWire\wire}
{wire()->page->id}

AX4HTyF.png

We just use the inline syntax for namespaced functions:

{ProcessWire\wire()->page->id}

And that's all you need to understand 🙂 

But I'll probably add the functions api without namespaces to all .latte files once you help me get the mentioned issue sorted 😉 

  • Thanks 1
Link to comment
Share on other sites

5 hours ago, bernhard said:

Do I have to require_once something? I have never added a custom extension yet.

I completely forgot to mention that part. I ran into that myself and resolved it by just installing Latte via Composer. It doesn't conflict with RFE and provides the library to use via namespacing as normal. I'm not sure if that's the best way, but it got me up and running fast and with no problems. Let me know if you have any ideas for an alternate approach!

Link to comment
Share on other sites

This is what RockFrontend has in its composer.json:

  "require": {
    "latte/latte": "^3",
    "sabberworm/php-css-parser": "^8.4",
    "matthiasmullie/minify": "^1.3",
    "wa72/htmlpagedom": "^3.0",
    "baumrock/humandates": "^1.0"
  },

How does your composer.json look like?

Link to comment
Share on other sites

@bernhard I had to install the package at the root level in my main composer.json file. So it's in addition to the Composer file in RockFrontend since that won't register as a namespace available globally when it's located in the RockFrontend subdirectory.

 "require": {
    "php": ">=8.2",
    "ext-gd": "*",
    "vlucas/phpdotenv": "^5.6",
    "roach-php/core": "^3.2",
    "spatie/simple-excel": "^3.6",
    "nesbot/carbon": "^3.7",
    "ramsey/uuid": "^4.7",
    "meyfa/php-svg": "^0.16.0",
    "srwiez/thumbhash": "^1.2",
    "latte/latte": "^3.0"
  }

Those are all the packages I have installed for my entire project.

Link to comment
Share on other sites

Hey @FireWire thank you for the composer file, that helped!

10 hours ago, FireWire said:

So it's in addition to the Composer file in RockFrontend since that won't register as a namespace available globally when it's located in the RockFrontend subdirectory

Sorry to say that, but this is nonsense 😉 

I didn't know for certain, but from my understanding if I do "composer require latte/latte" it should not matter WHERE I do that as long as I use the correct path in my require_once /path/to/autoload.php

So I tried doing "require_once /path/to/rockfrontend/autoload.php" above the CustomLatteExtension class and that fixed my error. The reason is simple: In ready.php the latte autoloader was not available, because I have added the "require_once /path/to/composer/autoload.php" in the "loadLatte" method of RockFrontend, since I thought it would be more efficient to only load it if needed.

I think that was wrong and I moved it into init(), because that's the whole purpose of the autoloader... to load things only if necessary, right?!

So with the latest version on the DEV branch you can add your extension without installing anything in your root folder and without adding any require_once anywhere.

21 hours ago, bernhard said:

I have an idea, but I tried adding your example to /site/ready.php and I get this:

I experimented with this:

<?php

namespace ProcessWire;

use Latte\Extension;

final class CustomLatteExtension extends Extension
{
  /**
   * Define functions available in all Latte files
   * These can be anything ProcessWire related or not, any functions defined here will be available everywhere
   */
  public function getFunctions(): array
  {
    $functions = [];
    foreach (wire('all')->getArray() as $key => $value) {
      if (!is_object($value)) continue;
      $functions[$key] = fn() => $value;
    }
    return $functions;
  }
}

But that is not a reliable solution. I'll ask for help in the nette forum. I think the only reliable solution would be to add the ProcessWire namespace to all compiled latte files. That sounds like side effects though. But we'll see...

Link to comment
Share on other sites

8 hours ago, bernhard said:

Sorry to say that, but this is nonsense 😉 

I didn't know for certain, but from my understanding if I do "composer require latte/latte" it should not matter WHERE I do that as long as I use the correct path in my require_once /path/to/autoload.php

So I tried doing "require_once /path/to/rockfrontend/autoload.php" above the CustomLatteExtension class and that fixed my error. The reason is simple: In ready.php the latte autoloader was not available, because I have added the "require_once /path/to/composer/autoload.php" in the "loadLatte" method of RockFrontend, since I thought it would be more efficient to only load it if needed.

I think we're talking about the same thing haha, I just didn't go the route of including /path/to/rockfrontend/autoload.php anywhere in my code manually. I just installed it in my root composer.json and didn't really give any further thought to it.

8 hours ago, bernhard said:

I experimented with this:

I tinkered with something like that but didn't spend much time on it. Interested in what you find out!

Link to comment
Share on other sites

 Share

  • Recently Browsing   0 members

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