How to handle nested attributes in HTMX - htmx

I have a table like structure in my page and each row has data-hx-get attribute pointing to a url where django returns details for that row. But also in the same row I have an edit button where django returns the edit form for that item. I would like the entire row to be clickable and when clicked shows replaces itself with the details and also the edit button to replace the row with the form. It works fine for the users but when the edit button inside the row is clicked, in the console I get htmx:swapError as the row also receives the click event and does what it was supposed to do. The event on the button takes precedence and before the row it changes the content of the row and when the row gets the response, the data-hx-target for that is no more in the page. So, my question is, is there a way to tell htmx, when a nested element has data-hx-get, ignore the parent's hx directive.
<div
class="row item-row"
id="item-row-{{item.pk|unlocalize}}"
data-hx-get="{% url 'some url' item.pk %}
data-hx-swap="outerHTML"
data-hx-trigger="click"
data-hx-target="this">
...
<button
data-hx-get="{% url 'editurl' item.pk %}"
data-hx-swap="outerHTML"
data-hx-trigger="click"></button>
</div>

You can use the consume modifier for hx-trigger https://htmx.org/attributes/hx-trigger/. This will prevent the click event from bubbling up to the parent row.
<div
class="row item-row"
id="item-row-{{item.pk|unlocalize}}"
data-hx-get="{% url 'some url' item.pk %}"
data-hx-swap="outerHTML"
data-hx-trigger="click"
data-hx-target="this">
...
<button
data-hx-get="{% url 'editurl' item.pk %}"
data-hx-swap="outerHTML"
data-hx-trigger="click consume"></button>
</div>

Related

htmx:afterSettle not working with hx-trigger

This code does not trigger a swap event, even though I can see that the afterSettle event is firing in the console.
<div id="product-gallery" hx-trigger="htmx:afterSettle" hx-get="{% url 'products' %}" hx-swap="outerHTML">
This works, but loops forever of course, with:
<div id="product-gallery" hx-trigger="load" hx-get="{% url 'products' %}" hx-swap="outerHTML">
I can see from htmx.logAll() that the htmx:afterSettle even is firing, it's just not triggering the above element. Have also tried htmx:afterSwap, which is also logged by logAll()
I'm trying to reload the gallery after a form has been swapped out (the form is inside this parent product-gallery div). Which I was hoping I could achieve by adding a from constraint:
<div id="product-gallery" hx-get="{% url 'products' %}" hx-swap="outerHTML" hx-trigger="afterSettle from:.product-form">
Structure is:
<div id="product-gallery">
<div id="product-form-1">
<form>
...
</form>
</div>
...
</div>
Update - it works! Followed solution 3 from https://htmx.org/examples/update-other-content/:
I added a header to my response in the form update view:
if form.is_valid():
form.save()
context = dict()
context['form'] = form
response = render(self.request, 'form_product.html', context)
response['HX-Trigger'] = 'productUpdate'
return response
Then I listen for this event in the gallery div:
<div id="product-gallery" hx-get="{% url 'products' %}" hx-swap="outerHTML" hx-trigger="productUpdate from:body">
The one bit of js I retain is for closing forms when they are valid:
htmx.on("htmx:afterSwap", function(evt) {
const eventIdTarget = evt['target'].id;
if (eventIdTarget === 'product-gallery') {
if ($("[id^=product-form] .alert-warning").length === 0) {
$.magnificPopup.close();
}
}
})
If you have troubles with http redirects, then this might help you:
If you want a response which was triggered via htmx to do a full page reload, then you should not return a http redirect response (302, aka as HttpResponseRedirect in Django).
You need to set the hx-redirect response header: https://htmx.org/reference/#response_headers
If you set hx-redirect and set the http response code to 302, then htmx will do a redirect on ajax-level (not on the full screen).
Next thing which might confuse new users: if you are used to the old post/redirect/get pattern, then there are good news: This is not needed any more.
If the client sends a http-post, and all data validates, you should return a http 2xx response containing the new HTML. There is no need for the outdated redirect/get dance.
If you think the htmx docs could get improved, then you might want to create a pull request to improve the docs.
AFAIK you can't use "afterSettle" like this: hx-trigger="htmx:afterSettle".
If you want to update a second part of the page, then you can use OOB (out-off-band):
The hx-swap-oob attribute allows you to specify that some content in a response should be swapped into the DOM somewhere other than the target, that is "Out of Band". This allows you to piggy back updates to other element updates on a response.
https://htmx.org/attributes/hx-swap-oob/
More about Update other content

Accordion has a default active panel, but it won't open

I am using a frontend library called Element UI to create some accordions on my website. I got an array of objects that I like to create accordions for. FYI, from the element ui website: the v-model used on the accordion specifies the currently active panel. The name attribute is the unique identification of the panel. That means I can do:
<el-collapse v-model="activeName" accordion>
<el-collapse-item title="Consistency" name="1">
<div>content content content</div>
</el-collapse-item>
<el-collapse-item title="Consistency" name="2">
<div>more content more content more content</div>
</el-collapse-item>
</el-collapse>
One this loads, the first accordion will open since in the data object, activeName is set to 1:
data() {
return {
activeName: '1',
}
}
Now I thought I'd just loop through the array and create accordions for the items in my array, and binding the name attribute to the index + 1, so that the first item will have the name attribute equal to 1, the second to 2 etc. So:
<el-collapse v-model="activeName" accordion>
<el-collapse-item
v-for="(item, index) in experience"
:title="item.company"
:name="index + 1"
:key="index"
>
<div> content content content </div>
</el-collapse-item>
</el-collapse>
But for some reason, when the page loads, the first item in the accordion won't open automatically. They're all closed by default. I created a codesandbox with the problem that you can see for yourself here: codesandbox
The problem is that when you run a for loop and assign name, it's a number and not a string.
:name="index+1" <---- This is a number
But, activeName is a string. So, the values don't match and that's why the accordian does not open on page load.
Here's an updated sandbox: https://codesandbox.io/s/vue-template-ysm79
I changed activeName to a number. The for loop accordian will now open and the normal HTML accordians won't.

Is there a way to delay the ngx-bootstrap accordion opening?

<accordion [closeOthers]=true>
<accordion-group *ngFor="let activity of activities" [heading]="activity.Name" (click)="openPanel(activity)" (isOpenChange)="openStatusChange($event)">
<ul *ngFor="let chemical of chemicals">
<li>{{chemical.BrandName}}</li>
</ul>
<div *ngIf="!chemicals?.length > 0">No chemicals associated with this activity type.</div>
</accordion-group>
</accordion>
When the accordian header is clicked, it opens and runs and fires 'openPanel()' which is an http.get, which then populates the panel. If the array returns empty, the *ngIf will display the "no associated stuff" message.
The problem is there is a very slight lag between the time the accordion opens and the array is filled, so the chemicals array is always empty when the accordion opens. This makes it so the "no associated stuff" message appears for about half a second, then the list populates. So I am wondering if there is a way to either delay the opening until the array is populated, or suggestions welcome.
You could use a boolean flag or function for the ngIf. Set the flag when the http promise returns as successful, that way the ngIf only triggers after the http call promise is resolved.
something like:
http.get('url').subscribe(response => {
show = true;
});

vue.js - Change text based on default/clicked class

Given the following:
<div id="#my-container">
<div class="title">Companies</div>
<div class="tab active tab-apple">Apple</div>
<div class="tab tab-google">Google</div>
</div>
When page is loaded without any tab clicks yet, whichever tab with the default active class, needs to go in the .title div. For the example above, <div class="title">Apple</div>
On click of a tab, the class is switched to active, and vue.js needs to update the .title div once again.
How can this be done with vue.js? I've tried but not able to get it to work as intended.
The answer by David is one way to do it. But Vuejs offers in-line computations for this. So, no need to hook into any CSS event. Here's some code to explain:
Create a data property active_tab, just like David mentioned. And then bind it's value just like he's done it. In your tabs, add an click event and at that event, assign appropriate value to active_tab.
<div class="tab active tab-apple" #click="active_tab = Apple">Apple</div>
<div class="tab tab-google" #click="active_tab = Google">Google</div>
Now, to dynamically assign the active class to the respective tab, make the class attribute, a computed property, like this:
<div
:class="['tab', active_tab == 'Apple' ? 'active' : '', 'tab-apple']"
>
Apple
</div>
What this code is basically doing is, :class makes class a computed property. Then the commas in the array divide the statement. So, the computation will always add tab and tab-apple classes. But, only if active_tab == 'Apple' then ? add 'active' else : add ''
Not sure which CSS framework you are using, but normally I hook into the events thrown by the tab switching (many CSS frameworks provide this access). Once hooked into it, you can write a Vue custom directive that will take that event and use it to update a VM attribute that indicates which tab is active.
Then you can use normal mustache templating to get it into your template:
<div class="title">{{ active_tab }}</div>

Modal: using modal within elements that were loaded via AJAX after document.load()

What happened: I have a page where user has to click on a button and selection is displayed in modal. When they clicked on what ever they wanted div element on the page is updated, via AJAX, which shows what they have selected and has an option "view item".
I have defined that "view item" should open up another modal view which will have all the information on the item, and now, referring to the above paragraph, rather then opening modal view user is being transferred to that page?
Question:
Is their a walk around for modal to be activated on the elements that were loaded after document load stage?
Example code: On initial load user is presented with following code, at this example item was set prior to load:
<div id="dynamic-area">
Item 1
view selection
</div>
<input type="radio" name="selection" id="selection" value="" data-toggle="modal" data-remote="/selection/search/1" />Change selection
When user have picked a different item after clicking on "change selection" #dynamic-area was fully updated with a data that was fetched via AJAX on the selected item.
So lets say, if user have picked item 3, we would have code below:
<div id="dynamic-area">
Item 3
view selection
</div>
<input type="radio" name="selection" id="selection" value="" data-toggle="modal" data-remote="/selection/search/1" />Change selection
Now if user to click on the "view selection", user is being to the page 'selection/view/3' when user should see modal.
After extending this function action worked as it should