Jump to content

Quick Add Modal


Pete
 Share

Recommended Posts

I think this is more of a "Pete doesn't understand Alpine yet" issue but wondering if someone can help.

I've got a Buy Now button on a product grid that renders a modal via HTMX. I'm using Padloper demo data code (modded slightly) to display the price/options markup.

For a product without variations I can get that to work.
image.png

But for a product with variations, because the modal content being loaded via HTMX contains alpine x-init that is being added to the DOM after the original page has loaded, the Alpine code won't work so the popup looks like this (it should say "Select an option")

image.png

then if you click on an option it says this:
image.png

Then funnily enough if you close the modal and open it again (or click a "Buy Now" button for another product with variations) it seems to work:
image.png

Also simply closing the modal and clicking Buy Now on the same product, the modal now works too.

The code returned via the HTMX request that loads the modal looks like this, so I suspect it's that initVariants() code that just needs to be changed to not be x-init? Or something else needs re-triggering somewhere along the line - I'm not sure.

 
<div id='padloper_add_single_product' hx-trigger="padloperfetchupdatedcart delay:500ms"
       hx-post="/?action=update-cart" hx-target="#padloper_side_cart"
       hx-vals='{"is_single_product_add": 1, {padloper_cart_update_product_quantity: 2, act: "update-cart"}'>
      <div <?php echo ($isProductWithVariants) ? " x-init='initVariants()'" : ''
?>>
   <?php

   if ($isProductWithVariants) {
      echo renderProductVariants($product);
   }
   echo renderPriceAndAddToCart($product);
   $cart = $padloper->cart;
   $numberOfTitles = $cart->getNumberOfTitles();
   $totalAmount = $cart->getTotalAmount();
   $totalQuantity = $cart->getQuantity();
   ?>
          <div id='cart-summary' class='hidden'>
              <span>Products: <span id='numberOfTitles'><?php echo $numberOfTitles ?></span></span>
              <span>Quantity: <span id='totalQty'><?php echo $totalQuantity ?></span></span>
              <span>Price: <span id='totalAmount'><?php echo $totalAmount ?></span></span>
          </div>
          <div id='cartnotice'></div>
      </div>
  </div>

initVariants is part of the Padloper Demo Data so lives inside this code:
 

Alpine.data("Padloper2DemoData", () => ({ ...

I'm having a lot of fun learning Alpine and HTMX but really having trouble wrapping my head around this and don't want to rebuild it all in jQuery at this stage so any help would be greatly appreciated!

  • Like 1
Link to comment
Share on other sites

Hi @Pete,

Hard to tell without seeing all the relevant bits. Here's a few thoughts.

9 hours ago, Pete said:

I've got a Buy Now button on a product grid that renders a modal via HTMX.

As an aside, I am wondering why you need to use HTMX to render the modal. From what I can see, you already have the product title and price in the product card in the products grid with the buy now buttons. Unless prices change constantly (like stocks), which I don't think they do in your case, the only thing that htmx should be doing in the modal is to listen to the 'add to basket' and optionally, to the increase/decrease in basket. After that, it renders the response (success or fail to add to basket, i.e. the greenish text). Otherwise, I'd have Alpine JS  open and close the modal and optionally render the modal markup. If Alpine JS is not rendering the markup, it would mean modal markup for each product is already done but hidden and their visibility controlled by Alpine, e.g. :class. Either way, I don't see why htmx should be rendering the modal. It also leads to extra hits on the server that you probably don't need.

9 hours ago, Pete said:

The code returned via the HTMX request that loads the modal looks like this, so I suspect it's that initVariants() code that just needs to be changed to not be x-init? Or something else needs re-triggering somewhere along the line - I'm not sure.

I don't think it has to do with x-init. Think of x-init like ProcessWire _init or Process modules init(). It allows you to hook into the lifecycle and do stuff before the DOM is updated. I could be wrong about this with respect to your use case though.

9 hours ago, Pete said:

But for a product with variations, because the modal content being loaded via HTMX contains alpine x-init that is being added to the DOM after the original page has loaded, the Alpine code won't work so the popup looks like this (it should say "Select an option")

This and...

9 hours ago, Pete said:

Then funnily enough if you close the modal and open it again (or click a "Buy Now" button for another product with variations) it seems to work:

this...suggest it could be a race condition. Again, without seeing the modded code, it is hard to tell. There are various options including:

  • use $nextTick (but not sure it applies to this case).
  • Have alpine listen to a custom event, i.e. @mycustomevent='handleCustomEvent()'. You can send a custom event after htmx swap, settle, etc.
  • Use htmx on load event to talk to Alpine. 'on load' is called every time an element is loaded to the DOM
9 hours ago, Pete said:

I'm having a lot of fun learning Alpine and HTMX

Great!

9 hours ago, Pete said:

and don't want to rebuild it all in jQuery at this stage

Not at any stage please! 😁. Just because of this, I can try put together a simple demo for you. But first, clarify why you need htmx to render (instead of just update) the modal 😀.

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

Thanks!

50 minutes ago, kongondo said:

As an aside, I am wondering why you need to use HTMX to render the modal.

Yeah I did initially start rewriting a big PHP function I've used on other projects called buildModal() to use HTMX to just update the necessary bits but then after a while I realised I was reinventing something I'd already built so probably not the best use of my time when I'm trying to finish something in a hurry (plus it was the weekend and I was getting annoyed with myself for working the weekend so gave up too easily 🙂 ). I think my other thought was that the empty markup is only about 4kb so "why not load the whole thing?" especially since you have to replace the content with some sort of markup anyway - but that does add up over time and on this shop repeat customers would genuinely open up that 3-5x per order and there are a lot of orders daily so the 4kb does add up I guess!

50 minutes ago, kongondo said:

There are various options including:

  • use $nextTick (but not sure it applies to this case).
  • Have alpine listen to a custom event, i.e. @mycustomevent='handleCustomEvent()'. You can send a custom event after htmx swap, settle, etc.
  • Use htmx on load event to talk to Alpine. 'on load' is called every time an element is loaded to the DOM

I did read about nextTick and custom events but couldn't wrap my head around it yet. I probably need to watch some videos too - I find the docs for both HTMX and Alpine confusing and a little inaccessible for me at least - I have real trouble learning without lots of real-world examples and the docs probably aren't the best place to find those. If there's a good source of HTMX and Alpine (combined) video tuts out there that folks could recommend I'd love to check them out.

50 minutes ago, kongondo said:

Not at any stage please! 😁. Just because of this, I can try put together a simple demo for you. But first, clarify why you need htmx to render (instead of just update) the modal 😀.

If you're up for doing a simple demo that would be great - as much as possible I'd like to just re-use your functions from Demo 1. I'm happy to change my modal code to just update the modal - I have been playing with it a bit more today. The template for it at the moment is simply:
 

<?php namespace ProcessWire;

/** @var $title */
/** @var $formID */
/** @var $url */
/** @var $content */
/** @var $action */
/** @var $buttons */
/** @var $scripts */
?>
<div id="modal-wrapper" x-data="{modalopen: true}" x-cloak>
    <!-- Modal Backdrop -->
    <div
            id="modal-backdrop"
            x-show="modalopen"
            x-transition:enter="transition ease-out duration-200"
            x-transition:enter-start="transform opacity-0"
            x-transition:enter-end="transform opacity-100"
            x-transition:leave="transition ease-in duration-100"
            x-transition:leave-start="transform opacity-100"
            x-transition:leave-end="transform opacity-0"
            tabindex="-1"
            role="dialog"
            x-bind:aria-hidden="!modalopen"
            class=" z-90 fixed inset-0 overflow-y-auto overflow-x-hidden bg-gray-900 bg-opacity-75 md:p-4 lg:p-8"

    >
        <!-- Modal Dialog -->
        <div
                class="flex flex-col shadow-sm bg-white overflow-hidden w-full max-w-3xl mx-auto"
                x-transition:enter="transition ease-out duration-200"
                x-transition:enter-start="transform opacity-0 scale-125"
                x-transition:enter-end="transform opacity-100 scale-100"
                x-transition:leave="transition ease-in duration-100"
                x-transition:leave-start="transform opacity-100 scale-100"
                x-transition:leave-end="transform opacity-0 scale-125"
                role="document"
        >
            <div id="modal-title" class="py-4 px-5 lg:px-6 w-full bg-gray-50 flex justify-between items-start">
            <?php if ($title) { ?>
                    <h4 class="font-medium">
                  <?php echo $title ?>
                    </h4>
            <?php } ?>
                <div class="-my-4">
                    <button
                            type="button"
                            class="close inline-flex justify-center items-center space-x-2 border font-semibold focus:outline-none px-2 py-1 pt-4 leading-5 text-sm rounded border-transparent text-gray-600 hover:text-gray-400 active:text-gray-600 ml-4"
                            x-on:click="modalopen = false"
                    >
                        <svg class="hi-solid hi-x inline-block w-6 h-6 -mx-1" fill="currentColor" viewBox="0 0 20 20"
                             xmlns="http://www.w3.org/2000/svg">
                            <path fill-rule="evenodd"
                                  d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
                                  clip-rule="evenodd"/>
                        </svg>
                    </button>
                </div>
            </div>
            <form id="<?php echo $formID ?>" class="InputfieldForm ajaxForm my-0" method="POST"
                  action="<?php echo $url ?>" enctype='multipart/form-data'
                  autocomplete="off">
                <div class="p-5 lg:p-6 grow w-full">

                    <div class="block-content pb-3 text-gray-600">
                        <div id="messageBox"></div>
                  <?php echo $content ?>
                    </div>
                </div>
                <div class="py-4 px-5 lg:px-6 w-full bg-gray-50 text-right space-x-1" id="modal-buttons">
               <?php echo $buttons ?>
                </div>
            </form>
        </div>
        <!-- END Modal Dialog -->
    </div>
</div>

<!-- END Modal Backdrop -->
<div id="modal-scripts"><?php echo $scripts ?? '' ?></div>
Link to comment
Share on other sites

Actually yesterday at one point that markup was before the closing body tag in my footer - one of the reasons I gave up on using HTMX to replace parts of it is I couldn't work out how to open the modal itself (change modalopen to true) AFTER the htmx response was finished. I was, of course, trying not to open the modal on the button click and then have a delay whilst the variables were filled in but I suspect there's some simple HTMX/Alpine combo that does the job.

So if you're up for teaching me something ridiculously simple like that I would very much appreciate it too 😄 

Link to comment
Share on other sites

30 minutes ago, Pete said:

I find the docs for both HTMX and Alpine confusing and a little inaccessible for me at least - I have real trouble learning without lots of real-world examples and the docs probably aren't the best place to find those.

I suppose I find them quite easy to grasp because I found htmx and Alpine after I'd been using Vue, Nuxt, etc. Have you seen the htmx demos? I have been thinking about a series of tuts on htmx + alpine + tailwind for ProcessWire. But then again, I have been thinking about a lot of things for years 😁.

I forgot to mention that sometimes some alpine stuff don't work because they are out of scope. Inner/nested x-data takes precedence over outer ones, for instance.

36 minutes ago, Pete said:

you're up for doing a simple demo that would be great

I'll try put something simple together. Even something generic (not Padloper related) I suppose would do. This would cover events; bounce; waiting; doing things after other events; toggling state, oob, etc.

  • Like 1
Link to comment
Share on other sites

This is my problem - I've just been happily using jQuery since v1 really even though there are much more lightweight tools available now for some of the easier bits and I've never learned Vue etc.

I do like HTMX because it makes it easier to see what's happening when you're dealing with simple AJAX requests - what it's firing off, where and so on all written up within the element that it relates to, whereas on a large CRM I've built over the last 6 years it's hard keeping track of everything - scrolling up and down big JS files to find what I've done years ago sometimes. It's not hard finding $(document).on('click', '#elementid'... references really, but seems neater being able to just see it inline in the source code in terms of what's being triggered.

Yes even a generic demo would be good, and I hear you on the list of things you intend to do - I've got about 10 years' worth I've not got round to at this point  😄 although after a long while if theyre still not done then maybe they're no that important?

  • Haha 1
Link to comment
Share on other sites

14 minutes ago, Pete said:

Yes even a generic demo would be good, and I hear you on the list of things you intend to do - I've got about 10 years' worth I've not got round to at this point   😄 although after a long while if theyre still not done then maybe they're no that important?

Hear, hear!

58 minutes ago, Pete said:

If there's a good source of HTMX and Alpine (combined) video tuts out there that folks could recommend I'd love to check them out.

For htmx, BugBytes has some neat htmx stuff. Although backend is Django, theory is the same. Most (at least it used to be that way) htmx stuff out there is related to Python stuff. BugBytes also has some Alpine.js tuts. Perhaps my favourite tutor of them all, Brad at Traversy Media has this Alpine.js crash course too.

Edited by kongondo
  • Thanks 1
Link to comment
Share on other sites

11 hours ago, bernhard said:

This is imho a great video from the creator of Alpine.js:

Thank you - I will check it out! My biggest problem is I never make enough time to watch videos and read tutorials - it only happens "in the heat of battle" (where battle is with a deadline 😆) whereas I need to get my workload to a level where I can set aside an hour or two each week to learn things properly and have a play around outside of a project. I could probably build up a little library of reusable code based on common scenarios that way instead of having to remember which project I did something on that I need again in a hurry a year down the line (I think we've all been there, right?).

Link to comment
Share on other sites

Hi @Pete,

I have started working on this. I have created a repo to demo usage of  my 3 amigos - htmx + Alpine.js + Tailwind CSS  - in ProcessWire. Stuff there will not necessarily be related to Padloper. I thought a repo like this gives everyone an opportunity to see what is achievable, and perhaps submit PRs as well. It is still early days so I haven't committed much. The first demo will be your htmx + Alpine products modal. Will let you know when that drops.

 

  • Like 2
Link to comment
Share on other sites

@Pete,

This is now ready. I have done two demos.

In the first one, Alpine.js populates the modal with values that have been passed to its $store. These include details about a product's variants, client-side calculation of total price, etc. htmx adds to cart and displays success message.

In the second demo, htmx populates the modal with values it fetches when the modal opens (via 'buy now'). It then passes the values (via the returned markup + x-init) to Alpine.js $store. After this, it is the same as the first demo...and htmx updates the cart and displays success notice.

I was hoping to do a video demo + tut but run out of time.  I'll do a better explanation here (or in a different thread) or in a video later. For now, the most important things to note are:

  1. I was hoping to do some very basic examples but got carried away with these demos. I hope to do very basic examples with very minimal markup in future.
  2. daisyUI is used here for its Tailwind CSS components (e.g. the buttons, modals, etc.). Stating the obvious here, but it is not needed in a final project.
  3. Note the folder structure in /templates/. Demos are in their respective folders in /templates/demos/. Although I would have wanted a simpler folder structure so as not to confuse, it wasn't easy to pull that off given that this repo will host various demos. I didn't want to create multiple templates that display the same thing. Hence, the demos .php files do the rendering and the handling of the htmx request for their own demos. This means the template 'products.php' remains clean and just loads the current demo's .php file (a render file, so to speak).
  4. The folder /templates/data/ has JSON of the fields and templates used in the demo, in case you want to test in a separate site.
  5. _func.php has a number of utility functions including one which can fetch dummy products from the Fake Store API, and use these to create products and categories.
  6. Selecting a demo automatically reloads the page. This is achieved via a redirect passed to htmx. 
  7. Viewing the frontend will greet you with this, very colourful shop! I went wild with all the available daisyUI themes. The current theme can be changed using the select at the top left. This is persisted using a plugin by Alpine.js called persist. It plays very nicely with $store and x-data.
  8. Whereas it is very easy to use things like x-data with local data within a component itself (e.g., <div x-data="{ open: false }">), given the size of the demos project as  a whole (now and in future), I decided to handle and store most of the Alpine.js stuff in a main.js file. This separation of concerns helps in the long run.
  9. All libraries are pulled from their respective CDNs.
  10. There are lots of comments in the demo files. Whilst this makes the files longer, hopefully you also get to understand what's going on.
  11. Most things are necessarily named verbosely. Things should be very clear.
  12. The demo site itself is not finished. E.g. currently, we only have a very basic template if viewing a single product.
  13. There is very little attempt to hide my ineptitude with respect to CSS 😁.

1488134872_htmx_alpine_tailwind_demos_2023-04-12012236.thumb.png.1495daa8bebdd02651b433f714887c74.png

1835615986_htmx_alpine_tailwind_demos_2023-04-12012236(2).png.db316cb36f25d8c821cba0a647b89e7b.png

640019471_htmx_alpine_tailwind_demos_2023-04-12012236(3).png.a5ae8c2552303c0f79aec11dbc571bdd.png

1142906279_htmx_alpine_tailwind_demos_2023-04-12012236(4).png.69cd3a36cd7852ab1640a071baafe181.png

2107476041_htmx_alpine_tailwind_demos_2023-04-12012236(5).thumb.png.b3e9018c077d6d696d5c5b8d884fb0ec.png

1837196640_htmx_alpine_tailwind_demos_2023-04-12012236(6).thumb.png.095fb0323a7a6421e0d83b1ab2ed13e9.png

1450249887_htmx_alpine_tailwind_demos_2023-04-12012236(7).thumb.png.bed871fcbeac5966934c873f9e561d80.png

Any questions, just holla.

  • Like 3
Link to comment
Share on other sites

Thanks again - I've got the demo working. The HTMX demo works with 2 dummy variations under a product, but the Alpine demo doesn't show the options. I don't think it matters too much though as I've still got a lot to look at adapting this to my project first - I'll let you know how I get on.

Link to comment
Share on other sites

9 hours ago, Pete said:

The HTMX demo works with 2 dummy variations under a product,

Glad it worked!

9 hours ago, Pete said:

but the Alpine demo doesn't show the options

That's strange. Are the options definitely sent to the browser? That's line #138 here in /demos/demo_alpine_renders_modal/products-alpine-renders-modal.php . If you dump $allProductsVariants here in line #96, do you get anything?

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