bigskysoftware / htmx

</> htmx - high power tools for HTML
https://htmx.org
Other
37.47k stars 1.27k forks source link

How to swap existing or insert a new element? #2166

Open yopoyka opened 8 months ago

yopoyka commented 8 months ago

The problem:

I have an element with id task-list.

<div id="task-list" hx-ext="ws" ws-connect="/updates">
    <div id="task-(unique id)"></div>
    ...
</div>

It contains a list of elements representing tasks. Each of them has a unique id. Task-list connects to a websocket endpoint and listens for updates. Each update consists of html that wraps a task in order to swap it correctly.

<div hx-swap-oob="beforeend:#task-list">
    <div id="task-(unique id)"></div>
</div> 

It works for additions and deletions of tasks but not for updates.

I'm trying to do the following:

If task with id exists in the list swap it Else add the new task to the list

I don't see a way to achieve this behavior without extending htxm.

itepastra commented 8 months ago

if you want to update an existing task, you can send the new task like <div id="task-(unique id)" hx-swap-oob="outerHTML"></div> instead of the wrapped task. It will swap the task inline.

yopoyka commented 8 months ago

Thanks. But it doesn't solve the problem when task doesn't exist.

ChrisLR commented 7 months ago

Any luck finding a solution? The closest I got so far is to include have a hx-swap-oob delete div preceding the hx-swap-oob beforeend div. It works but the replaced element will be moved. I would expect beforeend to replace the appropriate id first and only append if its not found.

yopoyka commented 7 months ago

No. I've came up with the same solution as yours. You can try to sort items using js on the client if it fits the application. I've also tried to play with extension api as it allows you to extend swap mechanism but I did not have much time to explore it more. I suggest you to look into it.

andryyy commented 7 months ago

Hey, have you tried idiomorph?

If it works, you may be happy with HTMX v2 coming up. Iirc it will inherit some of the morph magic from it.

jurriaan commented 6 months ago

I've had a similar issue where I wanted to prepend elements to a list (using sse instead of websockets).

Hacked my way around the issue using a small htmx extension:

let internalApi = null;
htmx.defineExtension('oob-if-exists', {
    init: function (api) {
        internalApi = api;
    },
    transformResponse: function (text, xhr, elt) {
        const fragment = internalApi.makeFragment(text);

        const swapAttr = elt.getAttribute('hx-swap');
        if (swapAttr == 'afterbegin' || swapAttr == 'beforeend') {
            const elements = htmx.findAll(fragment, "[hx-swap-oob=if-exists]");

            for (const element of elements) {
                const selector = '#' + element.id;
                const existingElement = htmx.find(selector);

                if (!!existingElement) {
                    // Move element to the desired spot and swap the HTML
                    existingElement.parentNode.insertAdjacentElement(swapAttr, existingElement);
                    element.setAttribute('hx-swap-oob', 'innerHTML');
                } else {
                    // Just create the element
                    element.removeAttribute('hx-swap-oob');
                }
            }
        }

        const htmlContent = [].map.call(fragment.childNodes, x => x.outerHTML).join('');
        return htmlContent;
    }
});

This is the source of the page:

<ol class="task-list" hx-ext="oob-if-exists" sse-swap="update-task" hx-swap="afterbegin">
</ol>

With something like this for the tasks:

<li id="task-1" hx-swap-oob="if-exists">Test</li>

This extension removes the hx-swap-oob attribute if its id isn't in the document yet. This will make sure the entries are created if they don't yet exist, but swapped when they are.

It's a bit hacky, but seems to work fine for my use-case and can probably be adapted for yours.

schungx commented 6 months ago

What you need is really a binary swap target specification... If selector matches anything do one swap, otherwise do another swap.

So at the very least you'd need two selectors to specify both branches. Currently htmx doesn't seem to allow for that.

Maybe something to be added like hx-alt-target that triggers when the main target returns nothing...

LifeLiveOn commented 1 month ago

Hey! Here is how I did it; if adding items with hx-swap="before end"

you could use this javascript code to remove the first child or the last, depending on your use case

<script>
    function handleNewItem() {
        const container = document.getElementById(containerId);
        const lastChild = container.lastElementChild;
        if (lastChild) {
            const elements = document.querySelectorAll(`#${lastChild.id}`);
            if (elements.length > 1) {
                elements[0].parentNode.removeChild(elements[0]);
            }
        }
    }
</script>

What it does is search for all the elements with that ID usually two HTML elements with the same ID will appear when create We either remove the first one or the last one when inserting it! If a duplicate is found

hx-on::after-request=function();