ekwoka / alpine-plugins

Mono repo for various Alpine plugin packages
8 stars 2 forks source link

Children, teleport and fx ideas #5

Open josh-tt opened 1 year ago

josh-tt commented 1 year ago

Hi Eric, other day I found a need for children and saw it was in your todo. I took a stab at it and also added some other bits (teleport and transitions). In the end I bailed on it as it was easier to just handle things in vanilla, so forgive the mess, but worked ok. Thought I'd leave it here in case there are some ideas for you. Hope you're well `/**

export default function (Alpine) { Alpine.directive('ajax', async (el, { expression, modifiers }, { effect, evaluateLater }) => { const target = evaluateLater(expression); let query;

    **// New: Check if the 'transition' modifier is present, if it is we add a class to the element**
    let applyTransition = false;
    if (modifiers.includes('transition')) {
        applyTransition = true;
    }

    if (modifiers.includes('query')) query = modifiers[modifiers.indexOf(modifiers.includes('class') ? 'class' : 'query') + 1];
    if (modifiers.includes('class')) query = '.' + query;

    effect(() => {
        target(async (target) => {
            if (!target) return el.dispatchEvent(new CustomEvent('halted', { detail: 'Target is not defined', ...eventDefaults }));
            try {
                const response = await fetch(target, { mode: 'no-cors' });
                if (!response.ok) throw new Error(response.statusText);
                const content = await response.text();
                const doc = xParser.parseFromString(content, 'text/html');

                **// NEW: Changed to let so we can reassign it**
                let selector = query ? (modifiers.includes('all') ? doc.body.querySelectorAll(query) : doc.body.querySelector(query)) : doc.body;
                if (!selector) throw new Error('Selected element not found');

                **// NEW: Children modifier**
                if (modifiers.includes('children')) {
                    if (selector instanceof NodeList) {
                        if (![...selector].some(node => node.children.length)) throw new Error('Selected elements have no children');
                        selector = [...selector].flatMap(node => [...node.children]);
                        console.log('Children modifier, selector: ' + selector);
                    } else {
                        if (!selector.children.length) throw new Error('Selected element has no children');
                        console.log('Not node list handled, selector: ' + selector.children);
                        selector = [...selector.children];
                    }
                }

                el.dispatchEvent(new Event('load', eventDefaults));

                **// NEW: Teleports to the queried location**
                if (modifiers.includes('teleport')) {
                    console.log('Teleporting to query location, query: ' + query);
                    // Uses the default query (todo: Should add unique modifier to teleport to a custom location?)
                    const teleportTarget = document.querySelector(query);
                    if (!teleportTarget) throw new Error('Teleport target not found. Make sure you have a query modifier with a valid selector');
                    if (Array.isArray(selector)) {
                        selector.forEach(child => {

                            // Check if the 'transition' modifier is present
                            if (applyTransition) {
                                child.setAttribute('x-fade-in', '300');
                            }

                            teleportTarget.appendChild(child.cloneNode(true));
                        });

                        // Wait a tick to remove the originals after we move them
                        setTimeout(() => selector.forEach(child => child.remove()), 0);
                    } else {
                        // Check if the 'transition' modifier is present
                        if (applyTransition) {
                            selector.setAttribute('x-fade-in', '300');
                        }
                        // Move the element to the teleport target
                        teleportTarget.appendChild(selector.cloneNode(true));
                        // Remove the original after we move them
                        setTimeout(() => selector.forEach(child => child.remove()), 0);
                    }
                }

                if (modifiers.includes('replace')) return el.replaceWith(selector);
                if (modifiers.includes('all')) return el.replaceChildren(...selector);
                if (selector.tagName == 'BODY') return el.replaceChildren(...selector.children);
                return el.replaceChildren(selector);
            } catch (e) {
                console.error(e);
                el.dispatchEvent(new Event('error', { detail: e, ...eventDefaults }));
            }
        });
    });
});

}

const eventDefaults = { bubbles: false };`

ekwoka commented 1 year ago

Interesting.

I think the teleport should just be handled by the x-teleport wrapping the x-ajax, and I think this adds a lot of extra logic it doesn't need. For example, we don't need to clone the new children or remove them from anywhere, since they are brand new disconnected nodes already.

To support children we probably only need to make the

if (selector.tagName == 'BODY') return el.replaceChildren(...selector.children);

into

if (selector.tagName == 'BODY' || modifiers.includes('children')) return el.replaceChildren(...selector.children);

for getting all children children of an all selector

modifiers.includes('all') ? doc.body.querySelectorAll(query) : doc.body.querySelector(query)

// to

modifiers.includes('all') ? doc.body.querySelectorAll( modifiers.includes('children') ? query+'>*' : query) : doc.body.querySelector(query)

That should handle those cases.

Want to test it and make that PR?

josh-tt commented 1 year ago

Nice one, thanks. I will check that out soon and see if I can make sense of it and let you know. I've been experimenting with x-ajax to do some 'load more/infinite scroll and carousel' components from php generated 'paginated pages' from the cms.X ajax in the content lazily for each page into a component and then loop over it using x-for to do stuff. Didn't think it would work so well, but it's quite smooth so far.

ekwoka commented 1 year ago

That's good to hear!

I have not though about actually using this one in production like I have the others.

But this does give me some ideas... 🤔

josh-tt commented 1 year ago

Yeh I love it. Lots of different problems it can solve with few lines and it makes life easy (or at least more interesting). I'm using wordpress (which I hate) so finding ways not to use wordpress while still using wordpress is apparently my mission...

For example using it with query strings on the server side means I can grab different templates depending on the context where I'm calling from, but keep the templates in one place. Like if you have some latest posts that you want to show on the home page with a special look and feel, you just make the home page article template on your blog page accessible with a query condition 'context=home'. All the data is on the blog page already so that's easy to build a lot of variations with the same data.

Or something like a 'minacart'. Build the mini cart on the cart page behind a query string condition, then when you want to get the mini cart or refresh it you can just call it with x-ajax. And it's easy to refresh it with events.

It's also good for data as well. I made a command palette style search with the load more/infinite scroll component. Fetch all the posts lazily, query select and extract text content and build an array of data from the content to search/filter.

I think I'm abusing it a quite a bit, but it makes things simple and is a single solution for a lot of different things.

josh-tt commented 1 year ago

Hi Erik, I tried to submit some changes, but not sure they ever went through.

In any case, it needed some more work and effort try to cover all the cases. This is what I have if you'd like to check it out and seems to be doing the trick, though required a bit more code.

There is a test file which should help to illustrate things. Just note the directive is named ajax-mod to run alongside the original for testing purposes.

Src: https://github.com/josh-tt/tt-x-ajax-mod/blob/main/x-ajax-mod.js Tests setup: https://github.com/josh-tt/tt-x-ajax-mod/blob/main/x-ajax-test.html