Jump to content

PW 3.0.189 – Core updates


ryan

Recommended Posts

Now that we're in the last couple months of this year, I've been trying to implement a lot of plans that we discussed early in the year. That meant a lot of updates to repeaters in the last couple of weeks (and likely more on the way). But this week, I've been working on another thing we discussed earlier, which is bringing automatic save and live preview capabilities to ProcessWire, independently of ProDrafts, and with full repeater support. 

In ProcessWire, it's your own code (in template files) that renders the site’s output. ProcessWire delivers a $page to your template file(s), and your template file(s) render it in whatever manner they see fit. In this environment, we need an automatic save capability before we can have a live preview capability. Working on that automatic save capability is what I've been doing this week. I figure that once that is finished, then we'll be able to start developing the live preview capability. 

I'm glad to report that we now have automatic save fully functional and have tested pretty thoroughly with every Fieldtype/Inputfield I can think of. Unlike in ProDrafts, it also fully supports repeater, repeater matrix, and even nested ones. Actually, it looks like we'll be able to support everything you would want it to. But it's early yet it'll need a lot of testing before its production ready. 

I've built it into a module called PageAutosave. It requires the latest version of ProcessWire on the dev branch (3.0.189). I'm not yet positive whether this is going to end up as a core module or not. I thought I would gauge interest and see. For now, I'm posting a limited release test version of it in the ProDrafts support board and ProDevTools support board, if anyone is interested in trying it out. 

The PageAutosave module lets you configure whether you want it enabled for all pages or just for unpublished pages. When needed, you can also optionally choose to limit it to certain templates, certain fields, or certain user roles. Autosave isn't always desirable as it literally means your changes get saved to the $page as you type, so I imagine it's something people might use more for unpublished pages.

Autosave is one side of the coin and live preview is the other. The current goal for live preview is to not depend on any particular view or admin interface and instead to just simply have the option of having a window open to a page in the website that detects changes and updates them automatically. So if you had one window open to the page edit screen, and another window viewing the page being edited, you could observe your updates as you made them. Once that is working reliably, we can decide on how best to put an interface around it, and maybe some will also like the option of just having two browser windows open wherever they want them. 

This won't be the kind of live preview where you'll see every character as you type it, but more likely you'll see updates a second or so after you make them. That's because your edits have to be auto-saved in the page editor, ProcessWire has to call your template file(s) to render the output, and the live preview has to update whatever has changed since the last update. It's that last part of it that I'll need the most help on. Some of you have mentioned htmx as a potential way to accomplish that and I think it looks compelling. And if you know of any other [likely JS-based] tools or technologies that we should also look at, please reply and let us know. 

I've bumped the core version to 3.0.189 primarily because the PageAutosave module requires updates that are in this version. This version also continues some longer term build out of a couple Fieldtype interface methods across multiple modules, and adds a new 'sorted' JS event that is triggered during sort actions (also used by PageAutosave). But if you aren't going to be installing the PageAutosave module, there's no urgency to update to 3.0.189 if you are on the dev branch and already using 3.0.188. Thanks for reading and have a great weekend!

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

This capability would directly impact how many of my projects I do in Processwire and how many I do in Oxygen/Wordpress/ACF.  I am currently only using Processwire for things that will make heavy use of the API for very custom projects, and using Oxygen for all projects that are design/marketing/brochure heavy.  Its just so much faster to see changes as I make them.

This is what I would like to see, working backward from the user side to the tech side.

When I edit a page, as I am changing fields, if I change a text box or a drop down or a range slider to change a heading font size, my page preview is updated as fast as possible, so I can see what that new heading size looks like on the page.  As soon as the field is changed, it adds the change to a change history list that I can use to roll back edits I don't like without needing to remember what changed.

Next, when I am ready for the user to see my changes I have made to my draft version of the page, I can hit a publish changes button to make my draft version replace the live version of the page.

I am assuming editing the page in this live preview mode would need to make a new copy of the page that would replace the live page if you save it.

Right now, I must save after every font size change to see if it works, and it doesnt remember how far down the admin page I was, so I must scroll all the way back down to that field and change the size again and hit save again and switch tabs again.  Its not a great experience.

These changes would be great!

  • Like 3
Link to comment
Share on other sites

Hi Ryan,

I just gave the module a quick test with my super advanced out of this world builder setup and wow, this really works well!  It even auto-saves Mystique-based fields which is absolutely perfect.  Also, replacing a single image-field autosaves correctly too.  So sweet.

  • Like 5
Link to comment
Share on other sites

49 minutes ago, ryan said:

It's that last part of it that I'll need the most help on. Some of you have mentioned htmx as a potential way to accomplish that and I think it looks compelling.

I am happy to help in this regard in any way I can.

  • Like 2
Link to comment
Share on other sites

Dear Ryan,

2 hours ago, ryan said:

I've been trying to implement a lot of plans that we discussed early in the year.

I think you have not been "trying" to...,  instead, you have been working hard on them (not just big features you mentioned today, but smaller yet still useful ones, as always). Thank you so much!

2 hours ago, ryan said:

Some of you have mentioned htmx as a potential way to accomplish that and I think it looks compelling. And if you know of any other [likely JS-based] tools or technologies that we should also look at, please reply and let us know. 

Maybe it is worth taking a look at Unpoly as well:
https://unpoly.com/

Various opinions:

Demo site created by the author:
https://demo.unpoly.com/

  • Like 3
Link to comment
Share on other sites

I'd like to add another vote to the notion of using something like htmx or Unpoly - I like them, a lot. Having used them on various projects, both have pros and cons (like most things) and satisfy different use cases.

I know PW is quick, and the template caching improves on things, but I recently launched a site that made use of Unpoly's up-preload and up-instant functionality - the perceived speed of page loads makes it feel like a static site. The client's response on seeing the dev preview was "I was not expecting a like for like copy! Especially one that is that fast." (their emphasis; it was a redevelopment of a bad WordPress build).

With Unpoly, I find the modal, popup and drawer UI are great. Having those integrated with the dynamic functionality is a plus, and means I don't have to worry about whichever underlying CSS framework is being used or pulling in a separate dependency to provide that. It's not perfect; some interactions, particularly nested ones, need a bit more care to make sure the server responses and front-end attributes are all present and correct. I also make heavy use of the "compiler" functionality. This is essentially your "$(document).ready()" equivalent which lets you initialise your client-side components (like maps or colour pickers, for example) on both initial page load as well as any time they happen to be added to the page dynamically via AJAX as the result of an Unpoly request.

htmx sits at a bit of a lower level, and provides events and a plugin system to extend it and hook into different interactions. When you need to do client-side things, you will still need some javascript to get the job done; or you could use htmx's sister project _hyperscript.

Either way, I'd be happy to see any of these become part of the PW admin. 🙂 

  • Like 4
Link to comment
Share on other sites

8 hours ago, Craig said:

htmx sits at a bit of a lower level, and provides events and a plugin system to extend it and hook into different interactions. When you need to do client-side things, you will still need some javascript to get the job done;

One of the many reasons I like htmx. It reminds me of ProcessWire a lot. Out of the box all it does is give you the necessary tools then stays out of your way. Even the author, for some reason, reminds me of Ryan 🙂

  • Like 5
Link to comment
Share on other sites

Thanks, @ryan! This year's end seems to be even more interesting than its beginning)

12 hours ago, ryan said:

I've built it into a module called PageAutosave. It requires the latest version of ProcessWire on the dev branch (3.0.189). I'm not yet positive whether this is going to end up as a core module or not. I thought I would gauge interest and see. For now, I'm posting a limited release test version of it in the ProDrafts support board and ProDevTools support board, if anyone is interested in trying it out. 

If that is required for the live preview it should be in the core, IMHO)

12 hours ago, ryan said:

And if you know of any other [likely JS-based] tools or technologies that we should also look at, please reply and let us know. 

If I understand it right, the core of what we need to build is something listening to a page save event and refreshing the preview page when it happens. Now we have an autosave to generate the events. The other part is reacting to that autosave. I can see 2 ways of doing that: ajax polling and sse (we probably do not need WebSockets as the preview doesn't need to send anything to the server... yet?). The latter (sse) seems to be a better fit, as it should use less server resources, but might be harder to implement (maybe not). Anyway, htmx supports sse (and even ws to an extent), which makes it a better fit than unpoly that doesn't (at least it didn't not so long ago when I checked). Actually, we could go without htmx, just taking inspiration from the principle it is based on. Though taking the ready-made library could be easier.

The other part where htmx (or unpoly, or turbo or...) could help, is refreshing not the whole preview page, but only a part of it. Regenerating the whole page markup could be a long process (those 2 seconds in the OP are way too optimistic for many of the sites I have seen). For example, we could regenerate only one PM block markup and sent it to the preview for htmx to swap. But that would require either some standardization of the frontend or some hookable architecture for a developer to implement. The former would break the core PW principle of leaving frontent to the developer. The latter should be possible and would work nicely when rendering RM based content builders the standard way or with a Wireframe.

9 hours ago, Craig said:

I'd like to add another vote to the notion of using something like htmx or Unpoly - I like them, a lot... Either way, I'd be happy to see any of these become part of the PW admin

Unpoly is complete framework, which could be used to upgrade PW admin as a whole, but would probably require to do everything its way. For a one place thing or for a more-work-more-customization htmx is a better suit, as it is lower level, as both of you @Craigand @kongondoagreed. If we bring in unpoly, we need to be ready to slowly redo all the admin area with it (which might be a nice thing in the long term). But for one task htmx is lighter solution. And we could even go without it only getting inspiration from it.

  • Like 9
Link to comment
Share on other sites

@ryan As a possible alternative to having something polling for changes, have you considered some kind of server side push technology for signalling changes to the connected browsers as mentioned above by @Ivan Gretsky?  I've been playing with using a thin ReactPHP layer that sends DOM mutation messages to clients using SSE (which has really good support in browsers) and have about 20 lines of vanilla JS in the browser that does the element replace in the DOM when it gets a message over SSE (though htmx could be used for this too).  None of the connections in the video are polling.

  • Like 6
Link to comment
Share on other sites

Thanks for all of the replies, this is great information. It sounds like SSE is the way to go here. I don't have any experience with that yet so am looking forward to learning more. I'm basically repeating what's been said above, but it sounds like htmx has this built in and may be a simple way to get started with it. Since this is a front-end modification (where PW doesn't usually venture into), the more lightweight we can be with it, the better. So we might eventually consider putting together just the functionality that we need for this part, as I know htmx does quite a bit more. But since I know almost nothing about SSE, htmx sounds like a great place to start. 

Since the front-end comes exclusively from user's template files, I don't think there's a way that ProcessWire can selectively render one thing or another in the page. It has to render the entire page. But it does sound like the actual JS-update part of it could certainly be more selective. For users that want selective server-side rendering of elements, it's certainly possible that we could provide some kind of suggestion that the developer should follow, as a means to increase performance of live preview. For instance, if the URL is /path/to/page/?change=body then the developer might detect $input->get('change') === 'body' and render the markup appropriately. But I think most will just want a drop-in solution that they don't have to code around. And that means how fast it works would have more to do with how efficiently the site is coded and how fast the environment is that it's running is. I don't think this will ever be a "see each character as you type" type of live preview, but I think most of the value of live preview will still be present even with a small delay. 

  • Like 4
Link to comment
Share on other sites

Just to make it clear. htmx has SSE client built in, but the SSE server part is still to be implemented. @netcarversuggested ReactPHP for that purpose. There are other options to choose from. Or we could invent our own)

I didn't quite grasp the "/path/to/page/?change=body" thing. Who is to request that? When doing the SSE thing we send something to the preview without it requesting anything. So it is the SSE server function to generate partial markup depending on the changed fields and pass it to the view. And htmx can handle not the full markup, but parts of it and swap just the piece it receives (with something like this). But it is a on step ahead - we need to have it working with full page swap first? as @ryan said.

Link to comment
Share on other sites

Unfortunately, we cannot get a way from a full page refresh no matter how desirable a partial refresh is. htmx deals with selectors, to find the element(s) to swap. It would be very onerous for ProcessWire to read through a template's code to find the element to swap. 

1 hour ago, Ivan Gretsky said:

I didn't quite grasp the "/path/to/page/?change=body" thing.

This means this:

<div 
    hx-get="/some-url"
    hx-trigger="some_event"
    hx-target="#body"
>
</div>

OR this..

<div 
    hx-get="/some-url"
    hx-trigger="some_event"
    hx-target=".body"
>
</div>

What @ryanis suggesting is that the target for htmx can be read from the GET variable. 

Whilst this might work for simpler cases, and it is a nice idea, the fact is there are myriad ways for building templates. What about fields that are split into various fragments on the page, how do we update those? What about the concatenated ones? This also means developers need to code for the live preview and in a certain way. Having said that, htmx has hx-vals

 <div hx-get="/example" hx-vals='{"myVal": "My Value"}'>Get Some HTML, Including A Value in the Request</div>

OK, that might work. This would tell ProcessWire if this changes, give me the changed values for 'myVal'. All these lead nicely to the next headache. What happens when your site goes live? It means you have to delete all those hx-attributes as you only needed them for live preview.

Just playing devil's advocate here 🙂.

If live preview was about rendering an Inputfield's content, that's easy. We would let individual Inputfields' __render()s handle it. I am using this quite a lot with partial updates (i.e., only those bits that have changed). However, if it is about previewing the site, how it would appear when rendered on the frontend, then the many difficulties of adapting to a developer's code in their template files make partial updates quite difficult, if not impossible.

  • Like 3
Link to comment
Share on other sites

On 11/5/2021 at 9:55 PM, ryan said:

That's because your edits have to be auto-saved in the page editor, ProcessWire has to call your template file(s) to render the output, and the live preview has to update whatever has changed since the last update. It's that last part of it that I'll need the most help on.

Is it really necessary to introduce a third-party dependency to do a comparison and updates the parts that were updated? Since the whole process won't be instant, couldn't it just be a matter of reloading an iframe showing the page? (which is what you're doing already with ProDrafts I assume)

I feel you could (should?) come up with your own minimal implementation of SSE to signal an autosave to a "preview" tab and have the js simply reload the iframe. Reading the Mozilla page I guess you could have two EventSource : one signaling a global autosave for PW's "preview" tab to automatically reload and another one signaling specific changes (in JSON format?) that developers could eventually listen to on their own using htmx or what, which is what you're actually suggesting with ?change=body, my bad.

I hope I'm not saying obvious things or misunderstanding what's been said, sorry if I did.

A question though re the autosave feature: since this is partly meant as a way to preview changes on a page, what happens if one wants to roll back to the initial state of the page ? Could you provide a way of cancelling all of the auto-saved changes ? Actually now that I think of it and re-reading your post, how would you do if you have a published page that you want to edit but don't want the edits to be seen by a guest ?

  • Like 4
Link to comment
Share on other sites

Quote

Just to make it clear. htmx has SSE client built in, but the SSE server part is still to be implemented.

@Ivan Gretsky That part I know I can handle. It's the front-end JS side of it that I don't know a lot about, yet. 

Quote

I didn't quite grasp the "/path/to/page/?change=body" thing. Who is to request that? 

I mean this just to suggest the possibility of establishing some sort of guidelines for how a developer might implement selective rendering. I don't know what those guidelines would be, and just use that GET var as an example of how a request might indicate a change for a particular field (named "body"). Trying to communicate that in the simplest example possible. The developer would decide what that meant in terms of output that should be produced. But perhaps this example is too far from the context, I'm still learning how this SSE stuff works. 

Quote

When doing the SSE thing we send something to the preview without it requesting anything. So it is the SSE server function to generate partial markup depending on the changed fields and pass it to the view.

PW can't generate partial markup for anything (outside of the admin at least), so it would be up to the site developer to implement whatever methodology we suggest for this, at least if they want higher performance live updates. 

Quote

And htmx can handle not the full markup, but parts of it and swap just the piece it receives (with something like this). But it is a on step ahead - we need to have it working with full page swap first? as @ryan said.

From what I can tell, htmx can handle both cases... replacing partial markup, or selecting a part (hx-select) from a larger portion of markup, like the full document. 

Quote

 It would be very onerous for ProcessWire to read through a template's code to find the element to swap. 

@kongondo Yes you are right, it wouldn't be very practical. But the benefits of selective markup rendering may be enough that it's worthwhile for us to establish some guidelines for supporting selective rendering. Like that ?change=body example earlier, that's what I was trying to communicate there even if it's likely not a good example. 

The guidelines could be as simple as just letting the site developer know when they can skip over expensive things to render, like primary navigation or footer, etc. Something like

if($config->livePreview) {
  // skip rendering unnecessary primary navigation
} else {
  // render primary navigation
}
Quote

What @ryanis suggesting is that the target for htmx can be read from the GET variable. 

I was actually thinking of "body" as a PW field name, and the developer would decide what that means to the output in terms of what should be replaced. But you guys have a lot more experience with htmx than me so I think my example is likely flying a little blind, as my focus so far as only been on the auto-save part that identifies field changes. I need to start playing with htmx. 🙂 

Quote

However, if it is about previewing the site, how it would appear when rendered on the frontend, then the many difficulties of adapting to a developer's code in their template files make partial updates quite difficult, if not impossible.

The way that ProDrafts live preview works is by having the editor window communicate to the preview window and notify it when a field changes (JS based window-to-window communication). The preview window does pull a fresh/updated copy of the rendered page, but it attempts to only replace the markup that changed. It does this by having formatted values for fields like "body" include an extra <div> around them that identifies what it is, and then ProDrafts can replace just that element with JS. You can see things as you type in the editor, with a short delay, unless your page is very slow to render. This works well for formatted text fields (which is usually most of what one wants to see in live preview), but it does have to refresh the full document for other things where the site developer may be responsible for more runtime markup related to the field output. 

Quote

Is it really necessary to introduce a third-party dependency to do a comparison and updates the parts that were updated? Since the whole process won't be instant, couldn't it just be a matter of reloading an iframe showing the page? (which is what you're doing already with ProDrafts I assume)

@monollonom ProDrafts only reloads the whole frame as a last resort. For most cases it can update just what changed. This is helpful for avoiding a very visible and distracting page refresh. Such page refreshes kind of kill the live-preview effect and can more easily draw your attention away from the content you are working on. 

I don't know if it'll be necessary to introduce a 3rd party dependency or not (htmx), but it does seem like a good place to start, even if we end up rolling our own later. 

Quote

 

A question though re the autosave feature: since this is partly meant as a way to preview changes on a page, what happens if one wants to roll back to the initial state of the page ? Could you provide a way of cancelling all of the auto-saved changes ? Actually now that I think of it and re-reading your post, how would you do if you have a published page that you want to edit but don't want the edits to be seen by a guest ?

 

That's more the domain of ProDrafts, and PagesSnapshots (another module in development). I think any kind of core autosave and live preview will focus on just those features (autosave and live updated view) and making them as good as they can be. It might be that one only wants to use these features for unpublished pages, but I think that's where it is most useful in the first place. If you wanted autosave/live preview on a published page, you could always clone it and then replace the original (or not) when you were done. This is essentially how PagesSnapshots manages drafts.

  • Like 6
Link to comment
Share on other sites

I have had a play and some of the implementation is literally, trivial 😄.

SSE

SSE is so simple it is unbelievable at first. You only need two things: in our case PHP and a modern browser...batteries included!

Example adapted from w3schools. More examples at MDN.

server-side

<?php namespace ProcessWire;
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');

$time = date('r');
echo "data: The server time is: {$time}\n\n";
flush();

client

if(typeof(EventSource) !== "undefined") {
  var source = new EventSource("/some-processwire/url/");
  source.onmessage = function(event) {
    document.getElementById("result").innerHTML += event.data + "<br>";
  };
} else {
  document.getElementById("result").innerHTML = "Sorry, your browser does not support server-sent events...";
}

I tested it and it works fine but was getting errors about wrong mime type if I set the headers later, but that is to be expected. 

Live Preview Refresh - Partial and Full - ajax - htmx

I have this working nicely (will try post the demo later today). The premise is:

  1. A change happens. The editor client sends the data to the server to save the change (autosave - was just mimicking this part). 
  2. Server saves and sends back a response including the name of the field that has been saved and the page ID back to the editor client.
  3. The editor client communicates with the preview client (a separate window). It sets the page ID and the name of the changed field name in its message to the preview window.
  4. The preview window client (htmx) receives the page id and the name of the field that has changed and sends a request to the server to fetch the rendered contents of that field for that page. The server sends back the html for htmx to swap in the DOM.

In this simple communication, we have the field name match the html id of the element we are swapping. E.g. 

<h2 id='headline'>My Headline</h2>

This is simple and straightforward and easily achieves partial refresh. However, it will be difficult to handle the more complex cases I mentioned in my previous post. It will also be somewhat dictating how the developer should name their html attributes. In addition, it still leaves the developer's templates with htmx attributes that will not be needed in production. This got me thinking.

Live Preview Wrapper - idea #1

In this case, the live preview window would add a wrapper html element around the rendered template file markup. The wrapper html element would have the htmx attributes instead of the template file markup. This idea didn't go very far. It would inevitably lead to broken / incorrect markup in the preview window as the outermost markup of the template file would be <html> tags. In addition, while it solves the problem of unrequired htmx attributes in production, it doesn't solve the problem of selective rendering. It still wouldn't' know what parts of the markup in the preview window to swap.

Live Preview Config  - idea #2

What if instead of live preview / ProcessWire suggesting things like naming of elements, standard markup, etc. that are needed in a template file in order to get selective markup rendering, we implemented it the other way round? What if the developer instead, via config file(s), passed properties to live preview to implement the htmx attributes needed for a template file / page? The default would be full page refreshes. However, if live preview detected that a partial refresh config was available, it would implement those. This would offer the granularity needed for partial refreshes. If live preview did not receive / detect any configs, the default would kick in. Live preview would remain as agnostic as possible. It could be something like this (assumes use of htmx):

  1. A developer places their live preview partial refresh configs in /site/templates/live-previews/basic-page-live-preview.php (or basic-page-live-preview.json).
  2. ProcessWire detects that the template file basic-page has a partial refresh config in .../live-previews/basic-page-live-preview.php
  3. When ProcessWire loads the preview window, it injects the htmx attributes for each named element in the live preview configs in the corresponding elements in the preview window DOM. Injection is done by a ProcessWire live preview near-agnostic JavaScript script that only runs in the preview window.
  4. When the autosave happens in the editor window and the preview window gets informed, htmx kicks in, responding to an event and fires htmx.trigger(elem, event).
  5. The element in the markup with the corresponding event listener (hx-trigger='this-element-event-fired') will pick up the event fired by htmx.trigger() and query the server for the changed markup. The attributes set in #3 will ensure only the markup that has a change will fire.
  6. Server sends the response back and the element's hx-swap (or even hx-oob) picks it up and swaps the DOM.

#6 is still not straightforward though. What will live preview / ProcessWire send back? Here too the config at #1 can help. E.g. it can tell ProcessWire to send <p>My Field Value</p> or just send the value of the field back, i.e. 'My Field Value'. The former is still complicated. Instead of telling ProcessWire I want <p>My Field Value</p>, how about tell it, via the configs at #1 that if requested field is field A, send me back the raw value (e.g. a plain text) but if the request is for field B, use this defined function in basic-page-live-preview.php to send me the rendered partial markup for field B? All ProcessWire would do is pass the value of field B to the said function. This value could be a Pageimages object. The function would iterate the images and send back the 'gallery' markup back to htmx to swap. 

example live preview config

<?php

namespace ProcessWire;

$livePreviewConfigs = [
  [
    'field' => 'headline', 'element_id' => 'my_headline_element_id', 'htmx-attributes' => ['hx-swap' => 'outerHTML', 'hx-trigger' => 'headlineevent']
  ],
  [
    'field' => 'images', 'element_id' => 'gallery', 'handler' => 'renderGallery','htmx-attributes' => ['hx-swap' => 'innerHTML', 'hx-trigger' => 'galleryevent']
  ],
];

function renderGallery(Pageimages $images) {
    // we only need the <li> since we are using innerHTML swap
    // the target element is <ul id='gallery'></ul>
    // --------------
    $out = "";
    foreach ($images as $image) {
        $thumb = $image->size(200, 200);
        $out .= "<li><img src='{$thumb->url}' alt='{$image->url}'></li>";
    }

    // ---------
    // sent back to htmx to swap
    return $out;
}

 

When the preview window loads, ProcessWire sends $livePreviewConfigs to the browser, e.g. ProcessWire.config.LivePreview. The preview window JavaScript would consume it like this:

client set htmx attributes

@note: JavaScript written in the browser, not tested, could have errors.

// get live preview configs
const elementsConfigs = ProcessWire.config.LivePreview
// loop through configs, find the corresponding element and set their htmx attributes
for (const elementConfig of elementsConfigs) {
    const element = document.getElementById(elementConfig.element_id)
    if (element) {
        const htmxAttributes = elementConfig['htmx-attributes']
        for (const [prop, value] of Object.entries(htmxAttributes)) {
            element.setAttribute(prop,value);
          }
    }
}

htmx-ready DOM changes via live preview injection (runtime only in live preview window):

<h2 id="my_headline_element_id" hx-swap='outerHTML' hx-trigger='headlineevent' hx-vals='{"field":"headline"}'>My Headline</h2>
<!-- gallery -->
<ul id="gallery" hx-swap='innerHTML' hx-trigger='galleryevent' hx-vals='{"field":"images"}'>    
<li><img src='image-1-src' alt='image-1-alt'></li>
<li><img src='image-2-src' alt='image-2-alt'></li>
</ul>

I have partly implement the above (just hard-coded configs for now) and it works nicely. 

Takeaways

  1. ProcessWire's live preview remains as agnostic as possible. 
  2. Developer give instructions to live preview (via configs) and not the other way round.
  3. Live preview ships with minimal defaults, including full page refresh.
  4. It seems like a lot of work on the developer's part if they want partial refresh. However, this partly matches the complexity of their template files. Besides, some of the functionality (e.g. renderGallery()) could be something they already use in their template, hence reusable.
  5. Developer does not need to have additional markup in their template files in order to see live previews when editing. This means no cleaning up afterwards. Their templates remain clean. Live preview only alters the DOM (of the rendered template) at runtime.
  6. Developer can name their markup attributes as they wish.
  7. Developer can decide what needs live preview and what doesn't, even on the same page (e.g. hx-select).
  8. ProcessWire stays out of the developer's way, as usual.

 

Edited by kongondo
typos
  • Like 10
Link to comment
Share on other sites

@kongondo 

Thanks, great ideas! Your SSE examples are eye opening. I think I understand a lot of this but not yet all of it, since I'm not deep in htmx yet, so I'll have to return to this once I do. But part of what you covered was the external interface to it that the web developer implements, and I think that's a good place to start, so I'll focus just on that. 

I understand the benefits of a separate config file, but am not sure how many would go to the effort of maintaining it for live preview. Maybe if PW auto-generates it somehow with functional defaults, it might help. 

Pros/cons aside, when we need something outside of the defaults, personally I'd rather declare this with markup classes or attributes in a manner similar to how we do for Markup Regions. Maybe I'm lazy, but it's already easy and familiar, and not much  new to remember. Like with Markup Regions, ProcessWire can remove the attributes when they aren't needed, so no need for any output downsides.

But if I had to declare it separately from the markup, I'd at least like to be able to do it in Setup > Fields > field rather than having to edit a .json file (and maybe this is a good fallback either way). 

I agree that it's good if the default behavior is to swap the entire <body> / refresh the window. But when we want to declare what gets updated with a live preview, here's an example of one way we could do it. If I wanted a particular section of markup to update automatically when the field "images" changed, I'd like if I could just do something simple like this, where div#gallery is an existing bit of markup a already there, like this:

<div id='gallery'>
  // markup for an image gallery
</div>

…and I just add a class to that existing markup tag...

<div id='gallery' class='pw-field-images'>
  // markup for an image gallery
</div>

...or maybe a custom attribute instead:

<div id='gallery' pw-field='images'>
  <!-- markup for images gallery -->
</div>

I'm not suggesting to add additional markup, but just an additional class or attribute to existing markup. If that same images field was also used elsewhere in the page, then I could just use that class (or attribute) again, and it would update too: 

<div id='sidebar-photos' class='pw-field-images'>
  <!-- markup for the sidebar photos -->
</div>

One thing I noticed in coding ProDrafts live preview is that field-level granularity often makes little difference in the end result. Replacing larger blocks of markup often is just as effective and visually identical. So I'd want to be able to say "if any of these fields change, update this…": 

<div id='content' pw-field='title,body,images,sidebar'>
  ... 
</div>

For fields that already contain markup (like CKEditor), or fields that get rendered from their own /site/templates/fields/* template file, ProcessWire could automatically add the appropriate class by wrapping the value with it (this is similar to what ProDrafts and PageFrontEdit do): 

<div class='pw-field-body'>
  <!-- value of $page->body, div wrapper added automatically when live preview -->
</div>

This enables it to work automatically, without the developer doing anything at all. The only minor downside is that occasionally that can interfere with a CSS/JS selector, like if you are trying to target h1 + p:first-child, where the <p> is part of the CKEditor output but the <h1> isn't. Live preview still works, but the CSS/JS selector no longer matches when live preview is on. It's simple to work around though, and of course only affects output when someone is live previewing.

Whether using a separate config file or not, if we use htmx, I think it should be a silent player and the developer wouldn't even need to know anything about how to use it. I'd like to avoid an external config file where I'm having to add 'htmx-attributes' arrays. I don't think we need the level of granularity or features in live preview that would demand the user know this kind of stuff. (Though fine for advanced users if/when needed). The more automatic it is, the more likely one is to use it. I think we'd also like the flexibility of not being dependent upon any particular library in case we ever decide to replace it with something custom. 

Side note, but one issue I also noticed in coding ProDrafts live preview is that when you update an element that has JS events on it that were added in document.ready, then they no longer work. For instance, an images gallery might have events that make thumbnails open in a lightbox or something. When the images gallery live updates, the lightbox no longer works unless the events were added to the whole document. Does htmx have some magic for this kind of situation?

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

4 hours ago, ryan said:

Whether using a separate config file or not, if we use htmx, I think it should be a silent player and the developer wouldn't even need to know anything about how to use it. I'd like to avoid an external config file where I'm having to add 'htmx-attributes' arrays. I don't think we need the level of granularity or features in live preview that would demand the user know this kind of stuff.

Thanks for your thoughts @ryan. You raise some good points here. Setting the entry level as low as possible is desirable.

4 hours ago, ryan said:

when you update an element that has JS events on it that were added in document.ready, then they no longer work. For instance, an images gallery might have events that make thumbnails open in a lightbox or something. When the images gallery live updates, the lightbox no longer works unless the events were added to the whole document. Does htmx have some magic for this kind of situation?

Do you mean when coding in htmx or vanilla JavaScript/jQuery? Maybe have a look at htmx onLoad method? There is an example here for third-party integration, although I am not sure if it is applicable. Other than that, many of htmx attributes are inherited and can be placed on the parent element. htmx aside, with JavaScript event bubbling/delegation, even with vanilla JS, if you placed the events on the parent element, future events should be picked up. Another option is MutationObserver. It is a beast though! Or maybe I misunderstood the question?

Edited by kongondo
typos
  • Like 2
Link to comment
Share on other sites

Quote

htmx aside, with JavaScript event bubbling/delegation, even with vanilla JS, if you placed the events on the parent element, future events should be picked up. 

The thing I'm thinking of is that a lot of people don't do this (often me included). Like if we've got a button with id="my-button" that we want to add a click event to, then we add it to $("#my-button").on('click', ...); and that no longer works if the button gets swapped with something from live preview. Of course, it's just a matter of coding the JS in a different way. But before telling people to code things a certain way I like to see if I can find a way to make things work no matter how they've put together their front-end code. For this case (live preview) it's probably not that important though. 

  • Like 1
Link to comment
Share on other sites

4 hours ago, ryan said:

Like if we've got a button with id="my-button" that we want to add a click event to, then we add it to $("#my-button").on('click', ...); and that no longer works if the button gets swapped with something from live preview. Of course, it's just a matter of coding the JS in a different way. But before telling people to code things a certain way I like to see if I can find a way to make things work no matter how they've put together their front-end code. For this case (live preview) it's probably not that important though. 

I get you now. I've been dealing with this a lot, where custom Inputfields have been updated and InputfieldPageAutocomplete, CKEditor, Selectize, etc stop working. I have been using ProcessWire's Inputfields API to reload them 😀, i.e. initFromInputfield, Inputfields.reload, .trigger('reloaded'), etc. htmx has this event htmx:afterSettle that is triggered after the DOM has settled, after a swap. I use that like this:

htmx.on("htmx:afterSettle", function (event) {
  // RUN POST SETTLE OPS
  // this function will get the details of the trigger and target event
  // it will then call ProcessWire's Inputfield's methods or an Inputfield's own methods (e.g. InputfieldPageAutocomplete)
  // to reload the Inputfield that has been updated, e.g. adding a new autocomplete to the DOM via htmx
  runAfterSettleOperations(event)  
})

The only challenge I've had with this is Selectize (I reported the error in another post) but I managed to resolve that. If you are not currently using htmx, I imagine you could emit an event after your preview has been updated and reload your elements and their attached events.

4 hours ago, ryan said:

But before telling people to code things a certain way I like to see if I can find a way to make things work no matter how they've put together their front-end code.

I'd probably use MutationObserver in that case, and have it watch a top-level wrapper element, e.g. something that wraps all the markup in the preview window. It can detect changes anywhere in the DOM. You might run into performance issues though, if you cast your net that wide, as opposed to watching, say, a number of ul lists. 

  • Like 1
Link to comment
Share on other sites

On 11/9/2021 at 3:20 PM, ryan said:

Side note, but one issue I also noticed in coding ProDrafts live preview is that when you update an element that has JS events on it that were added in document.ready, then they no longer work. For instance, an images gallery might have events that make thumbnails open in a lightbox or something. When the images gallery live updates, the lightbox no longer works unless the events were added to the whole document. Does htmx have some magic for this kind of situation?

I think there's no way around this issue except for the developers to adapt their code a bit, have their page working without javascript, or a full page reload. I tried to check if there was a way to somehow "reset" the javascript but it seems there's none.

On 11/9/2021 at 3:20 PM, ryan said:

...or maybe a custom attribute instead:

<div id='gallery' pw-field='images'>
  <!-- markup for images gallery -->
</div>

What I like about this is how it's somehow reminiscent of front-end editing's method D. Maybe it could be something to rely on for the live preview as well ?

  • Like 4
Link to comment
Share on other sites

18 hours ago, monollonom said:

What I like about this is how it's somehow reminiscent of front-end editing's method D. Maybe it could be something to rely on for the live preview as well ?

I quite agree. I think that, stylistically, the front-end editing approach is a good model to follow (assuming it works) and has the added benefit of already being familiar to many developers. 

  • Like 1
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
  • Recently Browsing   0 members

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