Jump to content

Support for HTML-over-the-wire through the render of template file fragments


Pixrael
 Share

Recommended Posts

I have been using HTMX recently and it really is a very good option. Of course all of its functionality can be achieved using the Processwire API, and including partial files in the template file. But it would be wonderful to have some kind of facilities in our framework.

Maybe with a new method in the PageRender class, that allows to select specific elements of the template file markup. The following is a simple idea based on the Markup Regions syntax:

<pw-region id="content">
	<h1><?= $page->title ?>
	<?= $page->summary ?>
	
	<h2>Contact Details</h2>

	<div id="contact_info" pw-fragment>
		<p><strong>$page->contact_name</strong></p>
		<p>$page->contact_address</p>
	</div>

	<h2>Social Channels</h2>

	<div id="total_likes" hx-swap-oob="true" pw-fragment>
		<a href="https://www.facebook.com/">$page->likes</a>
	</div>

	<p>Please, Call Us!</p>

</pw-region>

Since I'm not a programmer and I don't really understand very well how PW works inside, perhaps one of the following options can work to execute the rendering of the fragments (It can use the Find method that Markup Regions has for HTML tags)

$output = $page->render("#contact_info#total_likes");

$output = $page->render("basic-page.php#contact_info,total_likes");

$output = $page->render("fragment=contact_info|total_likes");

$output = $page->fragment("contact_info|total_likes")->render();

$output = $page->render()->fragment("contact_info|total_likes");

$output = $page->renderFragment(["contact_info","total_likes"]);

wireFragment()

Of course this method does not use the append file of the "_main.php", perhaps doing a:

$this->halt();

Laravel Blade has this already implemented for precisely the same purpose. Check this video: https://youtu.be/3dPUsXsDZsA

https://htmx.org/essays/template-fragments/

@ryan what do you think about this? Everyone, would this be a good idea? ..or something similar?

 

 

 

  • Like 7
Link to comment
Share on other sites

  • Pixrael changed the title to Support for HTML-over-the-wire through the render of template file fragments

I've only little experience with HTMX. Is your request for making something possible that currently is not. Or is it about making something easier? Or is it about making something more performant?

I thought you can simply request the whole page and HTMX will select the correct elements to replace?

If it's about performance I'm not sure if your suggestion is really a good idea. The whole page would in my case almost always be pro-cached and therefore requests would be blazing fast. If the request returned only a portion that was not cached I guess it would be a lot slower. But it's just a guess 🙂 

12 hours ago, Pixrael said:

But it would be wonderful to have some kind of facilities in our framework.

I'm missing the WHY - maybe you want to explain that a little more detailed so that others that are not familiar to HTMX can better understand? Maybe @Jonathan Lahijani can also explain the gymnastics that are necessary at the moment?

  • Like 1
Link to comment
Share on other sites

1 hour ago, bernhard said:

I thought you can simply request the whole page and HTMX will select the correct elements to replace?

@bernhard This works pretty well for static content, but unfortunately not for content filtered by URL parameters, for example.

I really like to use HTMX to dynamically load and replace content on pages. Also asynchronous loading of partial parts of the page becomes very easy.

Here is a simple example of the output of a product tile with URL hook

  <?php foreach ($items as $key => $item) {
  	$pos = $start + $key + 1; ?>
  	<div :hx-get="'/getproduct/'+<?= $item->id ?>" hx-trigger="revealed" hx-indicator=".loader" class="product-tile">
      <div class"loader">
      	Some fancy loader :-)
      </div>
  	</div>
  <?php } ?>

The hook: (getproduct.php)

<?php 

namespace ProcessWire;

$wire->addHook('/getproduct/{prodid}', function ($event) {
    $id = $event->arguments('prodid');
    $item = $event->pages->get("template=product, id=$id"); ?>

<div>
  <p>Producttile</p>  
</div>

<?php exit(); ?>

And of course in ready.php

include 'templates/functions/getproduct.php';

if (array_key_exists('HTTP_HX_REQUEST', $_SERVER)) {
    $config->appendTemplateFile = '';
    $config->prependTemplateFile = '';
    $config->htmxRequest = true;
}

Maybe that's what @Jonathan Lahijani means by gymnastics.

For me personally, this is not a special effort and is actually already part of my workflow.

 

Disclaimer: I know this is not Markup Regions 😉

  • Like 3
Link to comment
Share on other sites

I'm developing a site using HTMX to swap images in a gallery (it uses 'picture' element and 'srcset' so I didn't want to load all the markup at once) alongside PW regions and it works great.

I haven't really encountered any problems or gymnastics -- but maybe my use-case is simple enough?

<?php

namespace ProcessWire;

$imgMarkup = '';
$thumbMarkup = '';
$imgNum = 0;

/** Build the gallery fragment (the bit that changes via HTMX) before output */
if ($page->gallery && $page->gallery->count() > 0) {
    $imgNum = $input->get('img', 'int', 0); // get image index number from GET var (AJAX and no-JS)
    $galleryImg = $page->gallery->eq($imgNum); // get the image from index

    // The image markup (actually rendered from a custom page class)
	$imgMarkup = '<picture>
        <source type="image/webp" srcset="...">
        <img src="..." width=".." height="..." alt="..." srcset="..." sizes="...">
     </picture>';

  	// If it is a ajax (HTMX) request, just echo the image markup and stop PW processing tyhe rest of the template.
    if ($config->ajax) {
        echo $imgMarkup;
        return $this->halt();
    }

  	/* When doing HTMX ajax swap, the rest of this template won't be rendered */
  
    // Build the clickable thumbnails that do the swapping (simplified)
    foreach ($page->gallery as $item) {
        $thumbMarkup .= '<li><a href="..." hx-get="..."><img src="..." width="..." height="..." alt="..."></a></li>';
    }
}
?>
<!-- The markup regions -->
<pw-region pw-id="imageViewer">
    <div class="image-wrapper">
        <figure class="image-main-wrapper" id="mainImg">
            <?=$imgMarkup?>
        </figure>

        <div class="image-thumbs-wrapper">
          <!-- where the HTMX magic happens -- note the additional hx-headers, which allows PW's $config->ajax to work -->
            <ul class="image-thumbs"
                hx-trigger="click" hx-target="#mainImg"
                hx-swap="innerHTML"
                hx-headers='{"X-Requested-With": "XMLHttpRequest"}'
            >
                <?=$thumbMarkup?>
            </ul>
        </div>
    </div>
</pw-region>

<pw-region pw-id="pageContent" class="content-left">....</pw-region>

<pw-region id="sidebarContent">...</pw-region>

It just works... no need to faff with cancelling prepend/append templates etc, just rember to add the extra hx-header (also, you can do it once on a parent element), you don't need to add it to every call.  I haven't ever tried, so I'm not 100%, but there might be a way to automatically add the header to every HTMX call with a bit of javascript in the header/footer.

I feel all the tools are probably already there, but if there is an even easier way, though, that would be great.

Edited by LMD
Typos / added additional code clarification
  • Like 3
Link to comment
Share on other sites

3 minutes ago, LMD said:

I haven't ever tried, so I'm not 100%, but there might be a way to automatically add the header to every HTMX call with a bit of javascript in the header/footer.

Looks like htmx has you covered 🙂 I read the docs like you can add "hx-headers" to your <body> and you're done.

Quote

Notes

  • hx-headers is inherited and can be placed on a parent element.
  • A child declaration of a header overrides a parent declaration.
  • Like 1
  • Thanks 1
Link to comment
Share on other sites

5 minutes ago, bernhard said:

Looks like htmx has you covered 🙂 I read the docs like you can add "hx-headers" to your <body> and you're done.

Ahh, yes.... I've only been putting it on the local parent element where required, because the <body> is controlled by a different template.  But if your entire site is HTMX powered that is absolutely the way to go.

  • Like 1
Link to comment
Share on other sites

All that you describe is very good for simple examples like the ones you put here, but when it's a much more complex page, or a web application, with several different elements that you want to update, it starts to become complex. You need to start dissecting the template file into small partial files (or even worse build the markup with PHP variables) to be able to render different types of requests to the same template, and it's a mess! ex. complex forms with dependencies between the fields, updating graphs or tables, panels with details of the selected element, couple of counters in different location in the page, etc. The idea is to be able to use the same original template file that initially rendered the page, and that can be used with Markup Regions if you wish.

Response with a whole page as @bernhard proposes (although it's already cached, you need to update it with the new query) and send the complete page each time you need to update something, ex. only one <select> element, small text or a counter, these kind of pages regularly weigh MBs ..It's very unreasonable ..is better to reload the page. The ideal is to send the exact piece of html that you need to update on your initial page render.

@bernhard Latte already support this kind of rendering. Using the 3rd parameter to only render 1 block from the template

$Latte_Engine->render('path/to/template.latte', [ 'foo' => 'bar' ], 'content');

 

  • Like 2
Link to comment
Share on other sites

Ok thx I think I get the point 🙂 

Maybe I was not seeing the problem because RockFrontend let's you split your markup in as many files as you want, which is also helpful with HTMX.

In your template you could have this

<div uk-grid>
  <div n:foreach="$page->cards() as $card">
    {$rockfrontend->render('partials/card.latte', $card)}
  </div>
</div>

And in your HTMX Endpoint you could have this:

<?php
$wire->addHook("/cards/{id}", function($event) {
  $rockfrontend = $event->wire->rockfrontend;
  $post = $event->wire->pages->get($event->id);
  if($post->template != 'blogpost') throw new Wire404Exception("Invalid Page");
  return $rockfrontend->render("partials/card.latte", $post);
});

 

  • Like 1
Link to comment
Share on other sites

I get it, in PW it can be done too with $files->render() ..now do the example without partials 😉 

I mean when a page has many moving parts, you have to dissection too much the "view" in files, and it becomes complex (not impossible) to handle... I want to make clear my first post says that it can be done now, in fact I have done it... but it would be much more productive and simple something like what I propose .. but it's only an idea based on my opinion

  • Like 1
Link to comment
Share on other sites

in fact I have a method in the custom page class for that:

if (HTMX Blah Blah) $page->renderFragment("todo", [array], "200 OK");


function renderFragment($filename, $bag, $code) 
{ 
	header("HTTP/1.1 $code"); 
	wire("files")->include("services/partials/{$filename}.php", array('view' => $bag));
	exit(); 
}

.. but I love to use Markup regions in my projects, and I hate using tens of partial files, that's the reason for the proposal. Get the "partials" from the same template file that I render in the page request.

It's a simple helper in tune with Processwire Markup Regions

  • Like 2
Link to comment
Share on other sites

On 4/7/2023 at 12:49 PM, LMD said:

I haven't ever tried, so I'm not 100%, but there might be a way to automatically add the header to every HTMX call with a bit of javascript in the header/footer.

HTMX already sends headers to identify itself with every request it initiates, so you could just put something like this into your config.php:

$config->htmx = isset($_SERVER['HTTP_HX-Request']);

The other headers seem useful, too: https://htmx.org/docs/#request-headers

  • Like 3
Link to comment
Share on other sites

On 4/7/2023 at 11:49 AM, LMD said:

HTMX call with a bit of javascript in the header

Others have mentioned how to do it in the Markup or in $config. If you wanted to do it using JS, this is how:

document.body.addEventListener("htmx:configRequest", (event) => {
	// add XMLHttpRequest to header to work with $config->ajax
	event.detail.headers["X-Requested-With"] = "XMLHttpRequest"
})

https://htmx.org/events/#htmx:configRequest

Not meaning to hijack this thread and I haven't read through everything and I don't know how Markup Regions work...just want to mention that I have set up a GitHub repo for htmx + Alpine.js + Tailwind CSS + ProcessWire demos. It is early days but I am hoping to add all sorts of demos there, from basic to advanced ones, including backend and frontend examples. I'll  create a thread for this work at a later time. Meanwhile (and I have nothing against the OP wish), I am happy to take on a challenge  like 'how would you build this complex page using htmx' . OK, maybe not as complex as this app: From React to htmx on a real-world SaaS product 😁: (too much todo, etc.).

Edit

First two demos are discussed in this thread:

 

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

On 4/7/2023 at 2:53 AM, Pixrael said:

Everyone, would this be a good idea? ..or something similar?

I think this is a great idea and would love to see it implemented. Of course you could achieve this with partials, if statements etc. But your proposal would make things much cleaner imo.

3 hours ago, kongondo said:

I have set up a GitHub repo for htmx + Alpine.js + Tailwind CSS + ProcessWire demos It is early days but I am hoping to add all sorts of demos there, from basic to advanced ones, including backend and frontend examples.

Great resource. Thank you for putting this together.

  • Like 2
Link to comment
Share on other sites

I'm not yet sure what ProcessWire could do here since it's the template file that controls all the logic of what gets output. But I may not yet fully understand the request, so I'll use an example or what I do understand below. Markup Regions don't have control over what your template file spends time rendering, just what gets output at the end. So there wouldn't be much benefit to having output of partials when it still has to spend the time to render everything, whether used in the output or not. Instead, you would need some logic in your template file in order to selectively render partials, and gain a performance benefit from it:

<?php namespace ProcessWire; 

// render just $part if requested, otherwise render all parts
$part = $input->get('part'); // i.e. header, content, footer

?>

if($part == 'header' || !$part): ?>
  <div id='header'>
     ...header markup...
  </div>
<?php endif; ?>


if($part == 'content' || !$part): ?>
  <div id='content'>
     ...content markup...
  </div>
<?php endif; ?>


if($part == 'footer' || !$part): ?>
  <div id='footer'>
     ...footer markup...
  </div>
<?php endif; ?>

<?php if($part) return $this->halt(); ?>

In the above example, if the page is requested without a "?part=" query string in the URL, then it renders everything (header, content and footer). But if rendered with a "?part=content" query string in the request URL (for example), then it would render and output just the <div id='content'>...</div>. 

  • Like 7
Link to comment
Share on other sites

On 4/19/2023 at 10:22 AM, ryan said:

Markup Regions don't have control over what your template file spends time rendering, just what gets output at the end.

This part doesn't seem like an easy thing to refactor. How would a template file be split accorrding to template regions? It'd require some sort of cached template files created from the "template regions split" itself? Throwing this questions in the open, not specifically to Ryan.

I think fragments in Blade "suffers" from the same issue in the sense that it takes the performance hit of rendering the whole template. https://github.com/laravel/framework/pull/44774 at least that's what I get from from skimming the issue. 

Link to comment
Share on other sites

The method proposed by @ryan here isn't bad, but it doesn't allow nested fragments. In the case of complex views (web apps, calculators, etc.) with different user actions where several calls are needed for different sections of the page, and we (almost always) need fragments that are inside larger fragments.

I'm currently working on my first Processwire module, which can help to use these fragment-based systems. I've been implementing several methods, and only one of them prevents the entire content of the template file from being rendered. It works but requires some conditions, ex. in the case of a single template file with markup regions, you should organize your code to put the creation of all content variables at the beginning of the file and completely separate from the output HTML markup.

The idea is that during rendering it collects the requested fragments from the template file and saves them to a temporary PHP file, then includes it in the template file at the point before full HTML output occurs, and then stop the rendering the template file. It's basically like a dynamic partial. The advantage is that you don't need to split the template files into several parts (nested fragments will be a nightmare) and avoid to put a lot of conditionals in the code. It also helps that if you have a new type of request, you only need to declare in the request which parts of the page to fetch. @elabx if you're interested, I have a Hook version of this that I used during testing.

Note: It would be great if the Render method, in addition to the current template file, would also accept an HTML string. This avoid the use of temporal files.

  • Like 1
Link to comment
Share on other sites

I'd also wonder if any other templating language aside from Blade already works with fragments in a way that it's possible to load only the data in the scope of the fragment. 

Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
 Share

  • Recently Browsing   0 members

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