Jump to content

Why I love the Latte Template Engine


bernhard
 Share

Recommended Posts

I don't know when I last found a tool for my daily work that had such a huge impact as Latte. The last one was ProcessWire I guess (which had an even greater impact of course, but still).

Latte uses PHP syntax so you don't have to learn a new language! It plays extremely well with the PW API and it makes my markup files so much cleaner and easier to read and maintain I can't tell you.

From time to time I stumble over situations where I'm once more impressed by latte, so this thread is intended to collect small latte snippets to show how one can use it.

If you want use Latte for your ProcessWire projects you can either install it via composer or you wait some more days until I release RockFrontend - a frontend module that can render latte files and comes with several other nice frontend helpers 🙂 

-----

Breadcrumbs the Latte+PW way:

<ul>
  <li n:foreach="$page->parents()->add($page) as $item">
    <a href="{$item->url}" n:tag-if="$item->id != $page->id">
      {$item->title}
    </a>
  </li>
</ul>

The cool thing here is that the last item (the page we are currently viewing) is NOT linked. The <a> tag is only rendered for all other items. Still the label of the link is rendered, because we are using n:tag-if and not n:if

-----

Output an image, but only if it exists:

<img n:if="{$page->yourimage}" src="{$page->yourimage->maxSize(400,300)->url}" alt="...">

Note that the ->maxSize() call will NOT throw an error even if you didn't upload an image! This is one of my favourite features of latte: Write the markup once and then add simple instructions directly within that tag. No more if/else/foreach chaos 🙂 

  • Like 13
  • Thanks 2
Link to comment
Share on other sites

That template engine is great, in my beginnings with Processwire I was testing with this excellent module  developed by @tpr it's very complete, with many additions and filters to extend it. But I finally ended up using Markup Regions when it came out around the same time. I wasn't that ready yet for that "advanced" PHP. I'll keep an eye on your module, maybe I'll try again with Latte 🙂

  • Like 1
Link to comment
Share on other sites

I personally enjoy Twig as it's similar to Nunjucks / Liquid which is my goto choice in 11ty. 

Yet it somehow doesn't feel right for me in ProcessWire for whatever reason and therefore Twig never made it into any project so far. Maybe some day.

3 hours ago, bernhard said:

when I last found a tool for my daily work that had such a huge impact

Had this recently in some kind with TailwindCSS and AlpineJS.
Both made work way faster for me. Not only in prototypes and playgrounds.

  • Like 1
Link to comment
Share on other sites

Quote

I don't know when I last found a tool for my daily work that had such a huge impact as Latte ...

On the latte website I am reading some interesting things:

- You don't have to look in the documentation. You don't have to learn another language. You just write like in PHP.
- Latte is a next generation templating system – it understands HTML. Where other systems see only a bunch of characters, Latte sees HTML elements.
- Latte is based on PHP, whereas Twig is based on Python. A designer in Latte doesn't have to constantly switch between two different conventions.

I am using placeholders in my html using php and I must say it works really good and fast
like this:

<div class="header_navigation block">
    <div class="header_language block"><?php include $header_language; ?></div>
    <div class="header_navbuttons block"><?php include $header_navbuttons; ?></div>
</div>

Here is my question:

Could you say from your own experience if using latte shortcodes make more productive and is the overhead worth it ?

 

Link to comment
Share on other sites

install it via composer and than how init latte?  tried breadcrumbs code not working?
 

<ul>
  <li n:foreach="$page->parents()->add($page) as $item">
    <a href="{$item->url}" n:tag-if="$item->id != $page->id">
      {$item->title}
    </a>
  </li>
</ul>

 

Link to comment
Share on other sites

I have also been using Latte for a few weeks. Thanks to Bernhard 😉 

It's just so awesome to use latte.

But you also learn new tricks every day.

I'm looking forward to the public version of RockFrontend (by the way a brilliant module 😉 thanks again Bernhard!) 

  • Like 2
Link to comment
Share on other sites

4 hours ago, pwired said:
<div class="header_navigation block">
    <div class="header_language block"><?php include $header_language; ?></div>
    <div class="header_navbuttons block"><?php include $header_navbuttons; ?></div>
</div>

Using RockFrontend your code would be like this:

<div class="header_navigation block">
    <div class="header_language block"><?= $rf->render("partials/header_language") ?></div>
    <div class="header_navbuttons block"><?= $rf->render("partials/header_navbuttons") ?></div>
</div>

Which is the short version of this:

<div class="header_navigation block">
    <div class="header_language block"><?= $rockfrontend->render("/site/templates/partials/header_language.latte") ?></div>
    <div class="header_navbuttons block"><?= $rockfrontend->render("/site/templates/partials/header_navbuttons.latte") ?></div>
</div>

But you can also use latte's include: https://latte.nette.org/en/tags#toc-include

4 hours ago, pwired said:

Could you say from your own experience if using latte shortcodes make more productive and is the overhead worth it ?

What more could I say than I love it? I'll share more examples here in the thread when they pop up in my work. And I'll release RockFrontend very soon (just need to do the showcase video...)

3 hours ago, fliwire said:

install it via composer and than how init latte?  tried breadcrumbs code not working?

Just wait for RockFrontend 🙂 Or see here: https://latte.nette.org/en/develop

  • Like 1
Link to comment
Share on other sites

Another nice one from today: Imagine you are on a blog overview page and you want to provide a link for your clients to directly add a new blog post (if they are allowed to):

<a
   n:if="$page->addable()"
   href="{$pages->get(2)->url}page/add/?parent_id={$page}"
>
  Add new blog-item
</a>

 

  • Like 1
Link to comment
Share on other sites

To make all the html layout better readable will make the workflow more productive
I assume latte will allow me to rewrite php includes like this:

<div class="header_navigation block">
    <div class="header_language block">{language}</div>
    <div class="header_navbuttons block">{navbuttons}</div>
</div>

 

 

 

 

Link to comment
Share on other sites

Cross-posting in case any Wireframe user wants to give Latte a try: there's a renderer for it now. Somewhat experimental, since I have a rather miniscule understanding of Latte myself 😅

Slightly annoying marketing shenanigans aside (I didn't know how to do a single thing in Latte without reading the docs, "Latte is the only system with an effective defense, thanks to context-sensitive escaping" is a silly thing to claim, etc.) Latte does look quite nice. I dig the n:attributes in particular, they remind me a lot of the AngularJS (v1) syntax. Apart from that, Latte seems largely the same as other engines/languages I've used (e.g. Twig, Blade, and Dust).

Admittedly I've just scratched the surface, so there's likely a lot more there 🙂

I'm not sure yet whether it's a good thing or not that Latte syntax is so close to plain PHP, just with auto-escaping enabled and <?php ?> replaced with curly brackets. Among other things a) the PHP syntax isn't necessarily the easiest to grasp or nicest to look at, especially for non-developers (though this is admittedly highly opinionated), and b) there's a slight fear in the back of my head that this actually makes it extra tempting for developers to put unnecessarily complex code within their views.

So far among the templating languages I've used Blade has been my personal favourite. It's easy to grasp, familiar to PHP users but also for everyone who's ever used another templating language (like Twig), has some very handy shortcuts available (particularly when it comes to loops), the syntax looks nice and clean (in my opinion), and components in particular are just brilliant in terms of both implementation and use. But enough advertising 😉

  • Like 4
Link to comment
Share on other sites

Need a language switcher?

<div class="...">
  <a
    n:foreach="$languages as $lang"
    href="{$page->localUrl($lang)}"
    class="..."
  >
    {$lang->title}
  </a>
</div>

 

  • Like 1
Link to comment
Share on other sites

<div class="...">
  <a
    n:foreach="$languages as $lang"
    href="{$page->localUrl($lang)}"
    class="..."
  >
    {$lang->title}
  </a>
</div>

Useful example and good jump start for me to try latte
thanks for posting

  • Like 1
Link to comment
Share on other sites

1 hour ago, Pixrael said:

@bernhard check this custom filters for PW, maybe you wan to add this to your module: https://github.com/rolandtoth/TemplateLatteReplace/wiki/Additional-filters

Thx @Pixrael I've seen those filters already. IMHO many of them are overkill:

// why should I use this?
{$page->title|bd}
// if I can simply use this:
{bd($page->title)}

I don't want to add too much stuff that might be more confusing than helping. For example I also see no reason for this:

// why this?
<div {$page->featured_image->size(1180,320)|bgimage|noescape}></div>
// and not simply that?
<div style="background-image: url('{$page->featured_image->size(1180,320)->url}')"></div>

The latter is much easier to control.

Though there are some examples that could be a great timesaver, like this one: https://github.com/rolandtoth/TemplateLatteReplace/wiki/Additional-filters#srcset

Do you have any specific wishes or experience with the other latte module?

Link to comment
Share on other sites

8 minutes ago, bernhard said:

Do you have any specific wishes or experience with the other latte module?

Not really, but what I wanted is to help you to have material, to review, because it's difficult to have an idea of future needs ahead of time. You could discard the ones that don't make sense of course

Link to comment
Share on other sites

6 minutes ago, Pixrael said:

Not really, but what I wanted is to help you to have material, to review, because it's difficult to have an idea of future needs ahead of time. You could discard the ones that don't make sense of course

Yeah I was just hoping you have some experience with it and have done the review before so I don't have to do it 😄😉 

Proper image handling is something on my list and it's definitely something that I'd want to have in RockFrontend. The goals is to have a module that makes all the tedious frontend tasks easier and more efficient. srcset would be a perfect example and image handling is on my list for quite some time now, but I don't even have experience with webp at the moment so I might not be the best person to implement such a feature 😄 

Let's see what you guys think of RockFrontend once it is published and then maybe someone wants to contribute...

Thx for the suggestions 🙂 

  • Like 1
Link to comment
Share on other sites

Gotta be honest, I'm really excited about this! Cant' wait for RockFrontend. I just want to jump in!  Are there any "Hello Worlds" or should I just be patient and wait for the module. 😉 

Have a great weekend. 

  • Like 1
Link to comment
Share on other sites

Hey @Greg Lumley I wanted to finish my video about RockFrontend last week or weekend, but it turned out to be a lot more time consuming that I thought 😅 Yesterday I even realised that some parts of RockFrontend could be improved even more, so I decided to do some important updates before finalising the video. I love those new additions 😍

To get live reloading for example is now really just installing the module and adding "$config->livereload = 1" to your PW site 😎 No injecting of scripts whatsoever...

Back to topic: Today I had to build an accordion content-element:

  <ul>
    <li n:foreach="$block->items() as $item">
      <a href="#">{$item->title}</a>
      <div>{$item->content()|noescape}</div>
    </li>
  </ul>

IMHO it can't get any cleaner 🤩

7IAQ6X7.png

  • Like 2
Link to comment
Share on other sites

@bernhard This is going to be so powerful.

I've done a few videos (not for dev) over the years, it sure takes a lot more time than people think it does. Good luck with that! 

Really looking to RockFrontend! Thank you for your work. 

 

 

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

Latte is great indeed! I've been using it for all projects since discovering it. Everything is so much more concise and elegant, especially once your start building your own filters and macros.

Quick note that you can also use Latte with Wanze's TemplateEngineFactory. I've found that the easiest way to get started; one composer install and you're ready to go 🙂

  • Like 4
Link to comment
Share on other sites

On 7/29/2022 at 7:02 PM, bernhard said:

Want to share some examples with us? 🙂 

Sure! Here's a few that might be of use to ProcessWire sites:

<?php

use Latte\Engine;

/**
 * Latte filter provider
 *
 */
final class Filters
{
    /**
     * Install available filters.
     */
    public static function install(Engine $latte)
    {
        foreach ((new static())->provide() as $name => $callback) {
            $latte->addFilter($name, $callback);
        }
    }

    /**
     * @return array<string, callable>
     */
    public function provide(): array
    {
        return [

            // Sanitize values using ProcessWire's sanitizer API variable
            'sanitize' => function ($value, $sanitizer, $options = null) {
                if (!$options) {
                    return sanitizer()->$sanitizer($value);
                } else {
                    $args = func_get_args();
                    unset($args[1]); // remove $sanitizer arg
                    return call_user_func_array([sanitizer(), $sanitizer], array_values($args));
                }
            },

            // Render FormBuilder form, but allow prepending and appending fields
            'form' => function ($form, $options = []) {
                if (!$form) {
                    return '';
                }
        
                $output = $form->render();
        
                if ($options['append'] ?? false) {
                    $output = str_ireplace('</form>', $options['append'], $output);
                }
        
                if ($options['prepend'] ?? false) {
                    if (stripos($output, '<form') !== false) {
                        $output = preg_replace('#(<form[^>]*>)#i', '\\1' . $options['prepend'], $output);
                    } else {
                        $output = $output . $options['prepend'];
                    }
                }
        
                return $output;
            },

            // URL slug / page name
            'slug' => function ($str, $length = 128) {
                $str = sanitizer()->unentities($str);
                $str = sanitizer()->pageNameTranslate($str, $length);
                return $str;
            },

            // Truncate string
            'truncate' => function ($str, $length = 200) {
                return sanitizer()->truncate($str, $length, ['visible' => true]);
            },

            // Render Markdown
            'markdown' => function ($str) {
                $str = "{$str}";
                modules()->get("TextformatterMarkdownExtra")->formatValue(new Page(), new Field(), $str);
                return $str;
            },

            // Unwrap ProcessWire value objects
            'value' => function ($object) {
                if (is_object($object)) {
                    if ($object instanceof SelectableOptionArray) {
                        return $object->implode(' ', 'value');
                    } else {
                        return $object->value ?? '';
                    }
                } elseif (is_array($object)) {
                    return implode(' ', $object);
                } else {
                    return (string) $object;
                }
            },

            // Join a string with a custom separator at the end
            'join' => function ($list, $separator = ', ', $lastSeparator = ' & ') {
                if (count($list) > 1) {
                    $last = array_pop($list);
                    return implode($separator, $list) . $lastSeparator . $last;
                } else {
                    return implode($separator, $list);
                }
            },

            // Prettify URL by removing protocol and www
            'prettyUrl' => function ($url, $options) {
                if (null === $url) {
                    return false;
                }
        
                $url = trim($url);
        
                if ($options['www'] ?? true) {
                    $url = str_replace('www.', '', $url);
                }
                if ($options['http'] ?? true) {
                    $url = str_replace(array('https://', 'http://'), '', $url);
                }
                if ($options['slash'] ?? true) {
                    $url = rtrim($url, '/');
                }
        
                return $url;
            },
        ];
    }
}

 

  • Like 3
Link to comment
Share on other sites

And a few macros as well:

<?php

use Latte\Compiler;
use Latte\MacroNode;
use Latte\Macros\MacroSet;
use Latte\PhpWriter;

/**
 * Latte macro provider
 *
 */
final class Macros extends MacroSet
{
    /**
     * Install available macros.
     */
    public static function install(Compiler $compiler): void
    {
        $me = new static($compiler);
        $me->addMacro('ifispage', [$me, 'macroIsPage'], '}');
        $me->addMacro('icon', [$me, 'macroSvgIcon']);
        $me->addMacro('trim', '', [$me, 'macroTrimEnd'], null);
        $me->addMacro('minify', [$me, 'macroMinifyHtml'], [$me, 'macroMinifyHtmlEnd'], null, self::ALLOWED_IN_HEAD);
    }

    /**
     * {ifispage $page}
     */
    public function macroIsPage(MacroNode $node, PhpWriter $writer)
    {
        return $writer->write('
            $isObj = %node.word && is_object(%node.word);
            $isCustomPage = $isObj && is_a(%node.word, "ProcessWire\DefaultPage");
            $isCorePage = $isObj && get_class(%node.word) === "ProcessWire\Page";
            $isPage = ($isCustomPage || $isCorePage) && %node.word->id;

            if ($isPage) {
        ');
    }

    /**
     * {icon 'search'}
     */
    public function macroSvgIcon(MacroNode $node, PhpWriter $writer)
    {
        return $writer->write('
            $icon = %node.word;
            $ratio = "xMinYMid";
            $ratioAttr = $ratio ? "preserveAspectRatio=\"{$ratio}\"" : "";
            $html = "<svg class=\'icon icon-{$icon}\' {$ratioAttr} aria-hidden=\'true\'><use xlink:href=\'#icon-{$icon}\' /></svg>";
            echo $html;
        ');
    }

    /**
     * n:trim
     */
    public function macroTrimEnd(MacroNode $node, PhpWriter $writer)
    {
        $node->validate(false);
        $node->openingCode = "<?php ob_start(); ?>";
        $node->closingCode = '<?php $__output = ob_get_clean(); ?>';
        if ($node->prefix === MacroNode::PREFIX_INNER) {
            // Simple case: n:inner-trim -> just trim content
            $node->closingCode .= '<?php $__output = trim($__output); ?>';
        } else {
            // Trickier case: n:trim
            // remove whitespace *inside* outer tags
            // while preserving whitespace *outside* of outer tags
            // Before: "   <h1>   Title   </h1>   "
            // After:  "   <h1>Title</h1>   "
            $node->closingCode = $node->closingCode .
                // Remove whitespace after opening tag
                '<?php $__output = preg_replace(\'~^(\s*<[^>]+>)\s+~s\', "\$1", $__output); ?>' .
                // Remove whitespace before closing tag
                '<?php $__output = preg_replace(\'~\s+(</[^>]+>\s*)$~s\', "\$1", $__output); ?>';
        }
        $node->closingCode .= '<?php echo $__output; ?>';
    }

    /**
     * {minify}
     */
    public function macroMinifyHtml(MacroNode $node, PhpWriter $writer)
    {
        return $writer->write('
            ob_start(function ($s, $phase) {

                // 0: replace newlines
                //$html = preg_replace("/\r\n|\r|\n/", "", $s);

                // 1: remove whitespace from between tags that are not on the same line.
                $html=preg_replace(\'~>\s*\n\s*<~\', \'><\', $s);

                // 2: replace all repeated whitespace with a single space.
                static $strip = true;
                $html=LR\Filters::spacelessHtml($html, $phase, $strip);

                return $html;

            }, 4096);
        ');
    }

    /**
     * {/minify}
     */
    public function macroMinifyHtmlEnd(MacroNode $node, PhpWriter $writer)
    {
        return $writer->write('
            ob_end_flush();
        ');
    }
}

 

  • Like 2
Link to comment
Share on other sites

On 7/21/2022 at 5:39 PM, bernhard said:
// if I can simply use this:
{bd($page->title)}

Or just use

{dump $page->title}

 

On 7/27/2022 at 2:21 PM, bernhard said:
 <ul>
    <li n:foreach="$block->items() as $item">
      <a href="#">{$item->title}</a>
      <div>{$item->content()|noescape}</div>
    </li>
  </ul>

I think you can drop the parentheses after items or content. Also you might want to use a n:if clause on the ul, so the markup is discarded if there are no items.

 <ul  n:if="$block->items">
    <li n:foreach="$block->items as $item">
      <a href="#">{$item->title}</a>
      <div>{$item->content|noescape}</div>
    </li>
  </ul>

I am also using Latte in an actual project and love it so far. For the integration into ProcessWire I am using  TemplateEngineFactory. It also has a Latte renderer (but with Latte 2 atm, actual version is 3).

Link to comment
Share on other sites

 Share

  • Recently Browsing   0 members

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