Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Children, teleport and fx ideas #5

Open
josh-tt opened this issue May 18, 2023 · 5 comments
Open

Children, teleport and fx ideas #5

josh-tt opened this issue May 18, 2023 · 5 comments

Comments

@josh-tt
Copy link

josh-tt commented May 18, 2023

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
`/**

  • WIP: Modification of x-ajax directive based on ewkoka/x-ajax that adds teleport and children modifiers
  • For example x-ajax.query.class.ajax.children.teleport gets the 'ajax' div's children fetched and teleports them to the ajax div
    */
    const xParser = new DOMParser();

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
Copy link
Owner

ekwoka commented May 18, 2023

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
Copy link
Author

josh-tt commented May 20, 2023

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
Copy link
Owner

ekwoka commented May 20, 2023

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
Copy link
Author

josh-tt commented May 20, 2023

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
Copy link
Author

josh-tt commented Jul 22, 2023

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants