Open doeixd opened 5 months ago
actually, maybe just have a submodlue called buffed where the beefed up versions are.
maybe only handle events + state in the default module.
maybe: on.cleanup(() => {}) on('custom:event', (e, data) => {}) on.attr('hello', () => {})
submodule that handles mounting / unmounting when routes match.
integration with my new templating library
Certainly! Here are a few examples of how the library might look in use after implementing some of the proposed ideas and todos:
import { watch } from '@doeixd/watch';
watch('.counter', ({ on, state, el }) => {
state.count = 0;
on('click', '.increment', () => {
state.count++;
el.querySelector('.count').textContent = state.count;
});
on('click', '.decrement', () => {
state.count--;
el.querySelector('.count').textContent = state.count;
});
on.attr('data-reset', (newValue) => {
if (newValue === 'true') {
state.count = 0;
el.querySelector('.count').textContent = state.count;
}
});
on.cleanup(() => {
console.log('Counter component cleaned up');
});
});
import { RouterWatch } from '@doeixd/watch/router';
const router = new RouterWatch();
router.on('/home', () => {
watch('.home-component', ({ on, state }) => {
// Home component logic
});
});
router.on('/products/:id', ({ params }) => {
watch('.product-details', ({ on, state, el }) => {
state.productId = params.id;
on('click', '.add-to-cart', () => {
// Add to cart logic
});
el.trigger('product:loaded', { id: state.productId });
});
});
router.start();
import { on } from '@doeixd/watch/global';
on.global('click', (event) => {
console.log('Clicked somewhere in the document');
});
on.outsideOf('.modal', (event) => {
// Close modal logic
});
const cleanup = on.key('Escape', () => {
// Handle Escape key press
});
// Later, when no longer needed:
cleanup();
import { find } from '@doeixd/watch/buffed';
find('.user-profile')
.on('click', '.edit-button', (e) => {
// Handle edit button click
})
.on.attr('data-status', (newStatus) => {
// React to status change
})
.style({ color: 'blue' })
.trigger('profile:updated');
import { watch } from '@doeixd/watch';
import { createStore } from '@doeixd/watch/store';
const store = createStore({
count: 0,
users: [],
});
watch('#app', ({ on, el }) => {
const render = () => {
el.innerHTML = `
<div>Count: ${store.state.count}</div>
<button id="increment">Increment</button>
<ul>
${store.state.users.map(user => `<li>${user.name}</li>`).join('')}
</ul>
`;
};
store.subscribe(render);
render();
on('click', '#increment', () => {
store.dispatch({ type: 'INCREMENT' });
});
on('custom:addUser', (e, userData) => {
store.dispatch({ type: 'ADD_USER', payload: userData });
});
});
import { watch } from '@doeixd/watch';
watch((match, el, deep, check) => {
if (el.hasAttribute('data-interactive')) {
match(el);
check(el.querySelectorAll('[data-interactive]'));
}
}, ({ on, state, el }) => {
on('click', () => {
el.classList.toggle('active');
});
on.lazy('mousemove', () => {
// This event listener is only attached when the mouse first moves over the element
console.log('Mouse moved!');
});
});
These examples showcase various potential features:
These demonstrations illustrate how the library could evolve to provide a more powerful, flexible, and ergonomic API for DOM manipulation and event handling while maintaining its core simplicity.
Certainly! I'll demonstrate how to create a more modular and reusable system using higher-order functions to wrap the setup function. This approach will allow for easy composition of behaviors like lazy hydration, error boundaries, conditional execution, and data fetching. Here's an expanded example:
import { watch, on, roles } from '@doeixd/watch';
// Higher-order function for lazy hydration
const lazyHydrate = (setupFn) => (args) => {
const { el } = args;
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
setupFn(args);
observer.disconnect();
}
});
observer.observe(el);
};
// Higher-order function for error boundaries
const withErrorBoundary = (setupFn) => (args) => {
try {
return setupFn(args);
} catch (error) {
console.error('Error in component:', error);
args.el.innerHTML = '<div class="error">An error occurred</div>';
}
};
// Higher-order function for conditional execution
const whenCondition = (condition) => (setupFn) => (args) => {
if (condition(args)) {
return setupFn(args);
}
};
// Higher-order function for route matching
const onRoute = (route) => (setupFn) => (args) => {
const checkRoute = () => {
if (window.location.pathname === route) {
setupFn(args);
}
};
window.addEventListener('popstate', checkRoute);
checkRoute();
};
// Higher-order function for data fetching
const withData = (fetchFn) => (setupFn) => async (args) => {
args.el.innerHTML = '<div class="loading">Loading...</div>';
try {
const data = await fetchFn();
return setupFn({ ...args, data });
} catch (error) {
args.el.innerHTML = '<div class="error">Failed to load data</div>';
}
};
// Example component setup functions
const counterSetup = ({ el, on, state }) => {
state.count = 0;
const display = el.querySelector('.count');
on('.increment', 'click', () => {
state.count++;
display.textContent = state.count;
});
on('.decrement', 'click', () => {
state.count--;
display.textContent = state.count;
});
};
const userProfileSetup = ({ el, data }) => {
const { name, email, avatar } = data;
el.innerHTML = `
<img src="${avatar}" alt="${name}">
<h2>${name}</h2>
<p>${email}</p>
`;
};
// Composing behaviors
const lazyCounter = lazyHydrate(withErrorBoundary(counterSetup));
const conditionalUserProfile = whenCondition(
({ el }) => el.dataset.userType === 'premium'
)(
withErrorBoundary(
withData(() => fetch('/api/user').then(r => r.json()))(userProfileSetup)
)
);
// Using the composed components
watch('.lazy-counter', lazyCounter);
watch('.user-profile', conditionalUserProfile);
// Complex example: Blog post with comments
const blogPostSetup = ({ el, data, on }) => {
const { post, comments } = data;
const { main, section } = roles;
const postContent = main('.post-content');
const commentsSection = section('.comments-section');
postContent.innerHTML = `
<h1>${post.title}</h1>
<p>${post.content}</p>
`;
const renderComments = () => {
commentsSection.innerHTML = comments.map(comment => `
<div class="comment">
<h3>${comment.author}</h3>
<p>${comment.content}</p>
</div>
`).join('');
};
renderComments();
on('.add-comment', 'submit', async (e) => {
e.preventDefault();
const form = e.target;
const newComment = {
author: form.author.value,
content: form.content.value
};
comments.push(newComment);
renderComments();
form.reset();
});
};
const enhancedBlogPost = onRoute('/blog/:id')(
lazyHydrate(
withErrorBoundary(
withData(() => {
const id = window.location.pathname.split('/').pop();
return Promise.all([
fetch(`/api/post/${id}`).then(r => r.json()),
fetch(`/api/comments/${id}`).then(r => r.json())
]).then(([post, comments]) => ({ post, comments }));
})(blogPostSetup)
)
)
);
watch('.blog-post', enhancedBlogPost);
// Reusable modal component
const createModal = (content) => {
const modalSetup = ({ el, on }) => {
el.innerHTML = `
<div class="modal-overlay">
<div class="modal-content">
${content}
<button class="close-modal">Close</button>
</div>
</div>
`;
on('.close-modal', 'click', () => {
el.remove();
});
on('.modal-overlay', 'click', (e) => {
if (e.target === el.querySelector('.modal-overlay')) {
el.remove();
}
});
};
return withErrorBoundary(modalSetup);
};
// Usage of reusable modal
const showTermsModal = () => {
const modalContent = '<h2>Terms and Conditions</h2><p>Lorem ipsum...</p>';
const termsModal = createModal(modalContent);
const modalElement = document.createElement('div');
document.body.appendChild(modalElement);
watch(modalElement, termsModal);
};
// Global navigation component with active route highlighting
const navigationSetup = ({ el, on }) => {
const links = el.querySelectorAll('a');
const updateActiveLink = () => {
links.forEach(link => {
if (link.getAttribute('href') === window.location.pathname) {
link.classList.add('active');
} else {
link.classList.remove('active');
}
});
};
on('a', 'click', (e) => {
e.preventDefault();
const href = e.target.getAttribute('href');
history.pushState(null, '', href);
updateActiveLink();
});
window.addEventListener('popstate', updateActiveLink);
updateActiveLink();
};
watch('nav', withErrorBoundary(navigationSetup));
This example demonstrates:
Higher-order functions for common behaviors:
lazyHydrate
for intersection-based lazy loadingwithErrorBoundary
for error handlingwhenCondition
for conditional executiononRoute
for route-based renderingwithData
for data fetching and loading statesComposition of these behaviors to create complex components:
Reusable component creation:
Global navigation component with active route highlighting
Use of roles helper for semantic element selection
Async data fetching and error handling
Event delegation and dynamic content updates
This approach allows for highly modular and reusable code. You can easily compose different behaviors and create complex components by combining these higher-order functions. This pattern promotes separation of concerns and makes it easier to add new functionality or modify existing behavior without changing the core component logic.
For example, if you wanted to add analytics tracking to any component, you could create a new higher-order function:
const withAnalytics = (eventName) => (setupFn) => (args) => {
const result = setupFn(args);
// Send analytics event
analytics.track(eventName, { componentId: args.el.id });
return result;
};
// Usage
const analyticsBlogPost = withAnalytics('blogPostViewed')(enhancedBlogPost);
watch('.blog-post', analyticsBlogPost);
This modular approach allows for great flexibility and scalability in building complex web applications while keeping the core library simple and focused on its primary responsibilities.
make ergonomics of events better make the cleanup function get all the event listeners return a cleanup fn from the on function remove unnecessary stuff lazy events? global on fn, with didClickOutsideof + cleanup. find / findAll simplify / return proxy with beefed up
on
fn add roles helper, or maybe just have the on fn take a optional selector? Or reuse /expose createOnFunction actually, just make el a proxy with a call that is equivalent to upgradedfind
clean up / github ci add note about performance add scoping / skipping capabilities. maybe make parent a selector, and only let it be scoped under that. remove unneeded stuff add trigger to things returned by find to trigger events. show example of redux / elm like using watch on an app elhave watch function return an object proxy that will be either the instance / array of instances, and all the custom events will be methods on the returned element, and then everything else will pass through to the element.
maybe separate it like:
actually, i dont like this. maybe this could all happen in the setup fn. and if you want this functionality, you can add it. just have watch return whatever is the result of the setup fn? if you want many, you'll have to bring some outside state yourself. This could be done with the wrapper fn? Happy I made that.
What happens if the parent of a watch doesn't exist? when that element is removed, should we clean up? or what happens if it does exist, but isnt in the document?
submodule called:
helpers
that assists in things like mapArrays, or providing hook-like api or integration with nano stores / etc? Conditional setup / on functions?how does skipping work? maybe the watch fn can return a skip fn / map that will allow you to add elements / conditions that will be skipped. This will require a hook into the createObserver matching, which can be provided to watchOptions. Actually, I think it'd be better if that was passed as the first argument instead.
match
will call the setup fn. Just make it very clear that the matcher fn will only get the parent nodes, you will have to call the match on the added child elements yourself if you want that behavior. maybe if they returndeep
then the children of the matched elements will be added to the list of elements to check? or maybe just give them acheck
fn to add more to check? with the defaultcheck()
being all the children of the currently assesed el.github pages docs.