Jump to content

Update listing of locations with AJAX


jds43
 Share

Recommended Posts

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

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();
}

 

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

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

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.

  • Like 1
Link to comment
Share on other sites

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.

 

  • Like 13
Link to comment
Share on other sites

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

  • 2 weeks later...
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

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

@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.

  • Like 1
Link to comment
Share on other sites

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 by kongondo
  • Like 1
Link to comment
Share on other sites

  • 2 weeks later...
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

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 😄.

  • 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
 Share

  • Recently Browsing   0 members

    No registered users viewing this page.

×
×
  • Create New...