jds43 Posted August 12, 2021 Share Posted August 12, 2021 Hello, I have a listing of locations where the user is able to click a link within each card to add/remove. The session is altered by query strings based on the id of the location. It functions well, but requires the page to be reloaded. <a href='{$page_url}?id=$item->id'> I'd like to use AJAX for adding or removing locations, updating the session and reloading the listing without page reload. I created a template ajax.php and added to PW with a hidden page using the template. The template just outputs placeholder markup wrapped in $config->ajax condition. The basic script I've tested reveals the placeholder markup when clicked, but I'll need the listing to display on page load and refreshed on clicking of the add/remove link within each location. <script> function loadDemo() { const xhttp = new XMLHttpRequest(); xhttp.onload = function() { document.getElementById("demo").innerHTML = this.responseText; } xhttp.open("GET", "/ajax-test/", true); xhttp.send(); } </script> I've referred to many forum posts and can't quite find a solution for this situation. Can anyone lend some guidance for using AJAX for adding or removing locations, updating the session and reloading the listing of locations by clicking a link with a query string? Link to comment Share on other sites More sharing options...
Jan Romero Posted August 13, 2021 Share Posted August 13, 2021 10 hours ago, jds43 said: I have a listing of locations where the user is able to click a link within each card to add/remove. The session is altered by query strings based on the id of the location. It functions well, but requires the page to be reloaded. That seems good because it’ll continue to work if JavaScript is unavailable. I would keep it. On the server I would keep everything the same and add the $config->ajax condition to send back different output for ajax requests. For example, for a deletion it would suffice to send back success or failure http headers. In your JavaScript you could then hijack all the delete links and open them asynchronously instead: function deleteAsync(event) { event.preventDefault(); const listItem = event.currentTarget.closest('li'); //assuming currentTarget is the clicked link and it’s inside the list item listItem.style.display = 'none'; //immediately hide the item to make the action appear instantaneous const xhttp = new XMLHttpRequest(); xhttp.onload = function() { if (this.status < 200 || this.status >= 300) this.listItem.style.display = 'initial'; //deletion failed, unhide the item and maybe show an error message else this.listItem.remove(); //only really remove the item if deletion was successful }; xhttp.listItem = listItem; //add the item to the request to be able to access it in the onload callback xhttp.open('GET', event.currentTarget.href, true); xhttp.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); //you need this for $config->ajax to work xhttp.send(); } 1 1 Link to comment Share on other sites More sharing options...
kongondo Posted August 13, 2021 Share Posted August 13, 2021 Just throwing in htmx as an alternative here. If you are interested, I could put together a rough example. 6 Link to comment Share on other sites More sharing options...
jds43 Posted August 13, 2021 Author Share Posted August 13, 2021 Thanks @Jan Romero This is so cool! It works like a charm. However, I have an alert that counts the matches that will need to be refreshed also. Any ideas? $matches = $pages->find("id=$selector, template=place, limit=20"); if($matches->count) { echo "<div class='uk-alert-primary' data-uk-alert> <a class='uk-alert-close' data-uk-close></a> <h2 class='uk-text-center'>$matches->count ".(($matches->count == 1) ? 'item' : 'items')." in Your Itinerary:</h2> </div>"; } Link to comment Share on other sites More sharing options...
jds43 Posted August 13, 2021 Author Share Posted August 13, 2021 Hello @kongondo I'm certainly curious about htmx. Yes please, to preparing a rough example. Link to comment Share on other sites More sharing options...
Jan Romero Posted August 13, 2021 Share Posted August 13, 2021 You're going to have to wrap the number in a tag, something like <span id='total-items'>{$matches->count}</span>, then keep a reference to that element around in your Javascript and update it as necessary. When deleting you might just take the innerText and decrement it, or actually count the list items (probably the most robust option). You might even calculate the new total on the server and put it into the responseText, but I would advise against it. 1 Link to comment Share on other sites More sharing options...
jds43 Posted August 13, 2021 Author Share Posted August 13, 2021 Got it @Jan Romero I counted the card-wrap elements and updated the innerHtml of the span after the cards were removed by the request. Thank you very much! 1 Link to comment Share on other sites More sharing options...
kongondo Posted August 14, 2021 Share Posted August 14, 2021 On 8/13/2021 at 3:59 PM, jds43 said: Hello @kongondo I'm certainly curious about htmx. Yes please, to preparing a rough example. Sure... If you haven't read deeply into htmx, the main premise is that the server is the single source of truth regarding both data and markup, i.e. whole application state. If we need to update either data or markup, the server handles that. We just need to tell it what action to take. Validation has to pass, of course, before the server will oblige ?. Below is a quick example that demonstrates updating the markup using htmx based on user actions. Only 'remove' locations is demonstrated in this example. I wasn't sure whether your app shows the user both their removed and added locations and whether they can reset the session to have all locations reloaded afresh. It doesn't matter much as it wouldn't change much of the logic in the example. Secondly, note that this example makes use of alpine.js and tailwind css just for the pizzaz. These are not required by htmx. To let ProcessWire recognise htmx requests, please refer to this thread. Depending on the approach you take from there, you might not even need a JavaScript file! In the example below, we do have a JavaScript file just because we want to use alpine.js (for notifications) and we need htmx to talk to alpine.js. We also need the JavaScript file to tell htmx to add XMLHttpRequest to its request headers so that ProcessWire's $config->ajax will understand the request. First, let's see a demo then we'll see how easy our work is using htmx. Just for this demo, I have included htmx (and alpine.js and tailwind css via their respective CDNs [in production, you want to purge your tailwind css ?]) in my _main.php. Inside the template I am using for this demo, I have the following code. Here, I have removed the tailwind classes used in the demo so we can focus on htmx. Note that you don't need a dedicated template file for this to work. It will work with any template. As long as htmx is loaded in the page view and your template (in this case, the current page's template) is listening to ajax requests. In the template file, the main htmx magic happens here: <?php namespace ProcessWire; $out = "<a hx-post='./' hx-target='#locations' hx-vals='{\"location_add_id\": \"{$page->id}\"}' hx-include='._post_token' hx-indicator='#locations_spinner_indicator'>Add</a>" . "<a hx-post='./' hx-target='#locations' hx-vals='{\"location_remove_id\": \"{$page->id}\"}' hx-include='._post_token' hx-indicator='#locations_spinner_indicator'>Remove</a>"; echo $out; Let's go through the htmx attributes: hx-post This tells htmx where to send its ajax request. In this case, we are sending it to the same page we are viewing (./) and are using POST. We could have used hx-get if we wanted to (GET). hx-target This tells htmx which markup to replace/swap. The default is to replace the markup of the element from which htmx was called. However, hx-target can be used to specify the element to replace. In this example, we are replacing the whole listing so we target its wrapper element which has the id locations. hx-vals This is optional but we need it in our example. We want to tell the server which location (ID) has been added/removed. If we had a form element, we could have used it for this instead. Since we don't have one, we are telling htmx to process the (JSON) value of hx-vals and send that together with its request. Note: the escape slashes are so we can have raw, valid JSON in the attribute as required by htmx. hx-include This is also optional but important in our case. It tells htmx to include input elements found via the selector in this attribute in its ajax request. In our example, we are telling htmx to include the CSRF token we set on the server together with its request. hx-indicator Also optional. Tells htmx to show/hide this element to show the user that something happened. In this case we use a spinner. We could have used a progress indicator as well, .e.g., if we were uploading a file. That's it really! No event listeners, no handlers! We can add (and I did add one), event listeners on htmx events in order to do something after the event. In this example, htmx fires a custom event which alpine.js is listening to in order to show notifications after the DOM has settled. The notification type (success, error, etc) and the message are all coming back from the server but not as JSON. The markup to update the page is also coming back from the server. htmx receives it and plugs it into the DOM per the hx-target (also see hx-swap) attribute value. In this example, we are updating the whole listing. If we wanted, we could update just the location that was removed, e.g. add it back but with some removed 'indicator', e.g. greyed-out. In this example we use anchor tags as htmx triggers. For htmx, it doesn't matter; button, div, p, li, whatever valid HTML element would work. You already have a backend logic that's working for you but I show an excerpt of mine here, for completeness. Inside my-template-file.php (the template file for the template for the current page, in this example), I have the following code: <?php namespace ProcessWire; if ($config->ajax) { // check CSRF if (!$session->CSRF->hasValidToken()) { // form submission is NOT valid throw new WireException('CSRF check failed!'); } // ................ more code // e.g. check removed locations in the session, etc // get previously removed locations (keeping things in sync) $removedLocations = $session->get('removedLocations'); // REMOVING LOCATION if ((int) $input->post->location_remove_id) { $mode = 'remove'; $notice = "Removed"; $id = (int) $input->post->location_remove_id; } else if ((int) $input->post->location_add_id) { // ADDING LOCATION $mode = 'add'; $notice = 'Added'; $id = (int) $input->post->location_add_id; } // ................ more code // e.g. check if we really have a page by that id, SET $noticeType, $options and update session 'removedLocations' etc // build final content //----------- // @note: buildLocationsCards() is a function that does what it says on the tin. We use it in both the ajax response here and also below, in non-ajax content, when the page loads // the $options array contains a key 'skip_pages_ids' with an array of IDs of locations (pages) that have been removed in this session. We skip these in buildLocationCards(). $out = buildLocationCards($page, $options); // @note - here we always return one input only // we use the values in this input in JS to pass event details to alpine.js to show the correct notification and the notification message. $out .= "<input type='hidden' id='location_notice' name='location_notice' data-notice-type='{$noticeType}' value='{$notice}'>"; echo $out; $this->halt(); } // NON-AJAX CONTENT BELOW.... That's all there is to it ?. I am happy to share the full code if anyone wants to play with this further. 14 Link to comment Share on other sites More sharing options...
netcarver Posted August 16, 2021 Share Posted August 16, 2021 On 8/14/2021 at 5:40 PM, kongondo said: I am happy to share the full code if anyone wants to play with this further. Please do. And thank-you for what you've already shared. I just started using this today and it's looking good so far. Link to comment Share on other sites More sharing options...
jds43 Posted August 16, 2021 Author Share Posted August 16, 2021 Thank you @kongondo That's pretty amazing for a rough example ? Link to comment Share on other sites More sharing options...
jds43 Posted August 26, 2021 Author Share Posted August 26, 2021 On 8/14/2021 at 12:40 PM, kongondo said: I am happy to share the full code if anyone wants to play with this further. Hello @kongondo, could you provide the full code? I'd like to try applying it to my project. Thank you! Link to comment Share on other sites More sharing options...
jds43 Posted August 26, 2021 Author Share Posted August 26, 2021 So, I have htmx working, but it's replacing the deleted card with the whole page. Odd, but I probably have some settings wrong: $out .= "<div hx-post='./' hx-vals='#card-$item_id' hx-target hx-ext='ajax-header' class='".(($row) ? 'uk-width-1-3' : 'uk-width-expand')."'> <div class='image uk-card-media-top uk-background-cover uk-position-relative ".(($row) ? 'rad' : 'rounded-top-left-right')."' $img data-uk-img> <span class='uk-position-top-right uk-position-small uk-preserve' data-uk-icon='icon: heart; ratio: 2'></span> </div> </div>"; Link to comment Share on other sites More sharing options...
netcarver Posted August 27, 2021 Share Posted August 27, 2021 @jds43 That would probably be indicative of your server side code running more than just your ajax section. It's probably not what you are outputting in the rendered htmx you just posted. In the example kongondo posted above, the ajax route ends by calling $this->halt() to finish off PWs processing after echoing out the html. Is your code doing this? NB it is only possible to use halt() in some situations (like template files, IIRC) so you'll need to do that or just call exit(); if in a different context. 1 Link to comment Share on other sites More sharing options...
kongondo Posted August 27, 2021 Share Posted August 27, 2021 (edited) On 8/16/2021 at 10:33 PM, netcarver said: Please do. And thank-you for what you've already shared. I just started using this today and it's looking good so far. 19 hours ago, jds43 said: Hello @kongondo, could you provide the full code? I'd like to try applying it to my project. Thank you! Sorry folks, I've been tied up. Will do so later today. 17 hours ago, jds43 said: but it's replacing the deleted card with the whole page. If you mean the whole page (i.e. including, e.g. the site menu, etc), then what @netcarver said. die() will also work. Yes, if in a module halt() will not work. In some contexts, another process might be returning content as well. If exit() does not work for you, please show us your whole code and/or tell us more about your context. Edited August 27, 2021 by kongondo 1 Link to comment Share on other sites More sharing options...
kongondo Posted August 29, 2021 Share Posted August 29, 2021 Apologies for the delay folks. Here you go: Demo code for Using htmx to Refresh ProcessWire Frontend Content. 2 3 Link to comment Share on other sites More sharing options...
jds43 Posted August 30, 2021 Author Share Posted August 30, 2021 Thanks @kongondo for taking the time to prepare all of this! Link to comment Share on other sites More sharing options...
HMCB Posted September 10, 2021 Share Posted September 10, 2021 On 8/29/2021 at 10:14 AM, kongondo said: Apologies for the delay folks. Here you go: Demo code for Using htmx to Refresh ProcessWire Frontend Content. HTMX is gaining traction especially in Python circles. And recently in .Net I’m seeing more integrations. Thank you very much for doing this. I feel like HTMX is the one tech I’ve been more excited about than anything else in the last 10 years. Link to comment Share on other sites More sharing options...
kongondo Posted September 10, 2021 Share Posted September 10, 2021 1 hour ago, HMCB said: HTMX is gaining traction especially in Python circles. And recently in .Net I’m seeing more integrations. Yes I noticed this as well, especially with respect to Python. I almost glossed over it thinking it was a Python-only tech ?. 1 Link to comment Share on other sites More sharing options...
Recommended Posts
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 accountSign in
Already have an account? Sign in here.
Sign In Now